-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow
flake8-type-checking
rules to automatically quote runtime-eva…
…luated references (#6001) ## Summary This allows us to fix usages like: ```python from pandas import DataFrame def baz() -> DataFrame: ... ``` By quoting the `DataFrame` in `-> DataFrame`. Without quotes, moving `from pandas import DataFrame` into an `if TYPE_CHECKING:` block will fail at runtime, since Python tries to evaluate the annotation to add it to the function's `__annotations__`. Unfortunately, this does require us to split our "annotation kind" flags into three categories, rather than two: - `typing-only`: The annotation is only evaluated at type-checking-time. - `runtime-evaluated`: Python will evaluate the annotation at runtime (like above) -- but we're willing to quote it. - `runtime-required`: Python will evaluate the annotation at runtime (like above), and some library (like Pydantic) needs it to be available at runtime, so we _can't_ quote it. This functionality is gated behind a setting (`flake8-type-checking.quote-annotations`). Closes #5559.
- Loading branch information
1 parent
4d2ee5b
commit 1a65e54
Showing
18 changed files
with
1,033 additions
and
207 deletions.
There are no files selected for viewing
67 changes: 67 additions & 0 deletions
67
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/quote.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
def f(): | ||
from pandas import DataFrame | ||
|
||
def baz() -> DataFrame: | ||
... | ||
|
||
|
||
def f(): | ||
from pandas import DataFrame | ||
|
||
def baz() -> DataFrame[int]: | ||
... | ||
|
||
|
||
def f(): | ||
from pandas import DataFrame | ||
|
||
def baz() -> DataFrame["int"]: | ||
... | ||
|
||
|
||
def f(): | ||
import pandas as pd | ||
|
||
def baz() -> pd.DataFrame: | ||
... | ||
|
||
|
||
def f(): | ||
import pandas as pd | ||
|
||
def baz() -> pd.DataFrame.Extra: | ||
... | ||
|
||
|
||
def f(): | ||
import pandas as pd | ||
|
||
def baz() -> pd.DataFrame | int: | ||
... | ||
|
||
|
||
|
||
def f(): | ||
from pandas import DataFrame | ||
|
||
def baz() -> DataFrame(): | ||
... | ||
|
||
|
||
def f(): | ||
from typing import Literal | ||
|
||
from pandas import DataFrame | ||
|
||
def baz() -> DataFrame[Literal["int"]]: | ||
... | ||
|
||
|
||
def f(): | ||
from typing import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from pandas import DataFrame | ||
|
||
def func(value: DataFrame): | ||
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
use ruff_python_semantic::{ScopeKind, SemanticModel}; | ||
|
||
use crate::rules::flake8_type_checking; | ||
use crate::settings::LinterSettings; | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
pub(super) enum AnnotationContext { | ||
/// Python will evaluate the annotation at runtime, but it's not _required_ and, as such, could | ||
/// be quoted to convert it into a typing-only annotation. | ||
/// | ||
/// For example: | ||
/// ```python | ||
/// from pandas import DataFrame | ||
/// | ||
/// def foo() -> DataFrame: | ||
/// ... | ||
/// ``` | ||
/// | ||
/// Above, Python will evaluate `DataFrame` at runtime in order to add it to `__annotations__`. | ||
RuntimeEvaluated, | ||
/// Python will evaluate the annotation at runtime, and it's required to be available at | ||
/// runtime, as a library (like Pydantic) needs access to it. | ||
RuntimeRequired, | ||
/// The annotation is only evaluated at type-checking time. | ||
TypingOnly, | ||
} | ||
|
||
impl AnnotationContext { | ||
pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self { | ||
// If the annotation is in a class scope (e.g., an annotated assignment for a | ||
// class field), and that class is marked as annotation as runtime-required. | ||
if semantic | ||
.current_scope() | ||
.kind | ||
.as_class() | ||
.is_some_and(|class_def| { | ||
flake8_type_checking::helpers::runtime_required_class( | ||
class_def, | ||
&settings.flake8_type_checking.runtime_required_base_classes, | ||
&settings.flake8_type_checking.runtime_required_decorators, | ||
semantic, | ||
) | ||
}) | ||
{ | ||
return Self::RuntimeRequired; | ||
} | ||
|
||
// If `__future__` annotations are enabled, then annotations are never evaluated | ||
// at runtime, so we can treat them as typing-only. | ||
if semantic.future_annotations() { | ||
return Self::TypingOnly; | ||
} | ||
|
||
// Otherwise, if we're in a class or module scope, then the annotation needs to | ||
// be available at runtime. | ||
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements | ||
if matches!( | ||
semantic.current_scope().kind, | ||
ScopeKind::Class(_) | ScopeKind::Module | ||
) { | ||
return Self::RuntimeEvaluated; | ||
} | ||
|
||
Self::TypingOnly | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.