mirror of https://github.com/astral-sh/ruff
Allow `flake8-type-checking` rules to automatically quote runtime-evaluated 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 https://github.com/astral-sh/ruff/issues/5559.
This commit is contained in:
parent
4d2ee5bf98
commit
1a65e544c5
|
|
@ -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):
|
||||||
|
...
|
||||||
|
|
@ -59,6 +59,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||||
flake8_type_checking::helpers::is_valid_runtime_import(
|
flake8_type_checking::helpers::is_valid_runtime_import(
|
||||||
binding,
|
binding,
|
||||||
&checker.semantic,
|
&checker.semantic,
|
||||||
|
&checker.settings.flake8_type_checking,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,7 @@ use ruff_python_semantic::{
|
||||||
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
|
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
|
|
||||||
|
use crate::checkers::ast::annotation::AnnotationContext;
|
||||||
use crate::checkers::ast::deferred::Deferred;
|
use crate::checkers::ast::deferred::Deferred;
|
||||||
use crate::docstrings::extraction::ExtractionTarget;
|
use crate::docstrings::extraction::ExtractionTarget;
|
||||||
use crate::importer::Importer;
|
use crate::importer::Importer;
|
||||||
|
|
@ -68,6 +69,7 @@ use crate::settings::{flags, LinterSettings};
|
||||||
use crate::{docstrings, noqa};
|
use crate::{docstrings, noqa};
|
||||||
|
|
||||||
mod analyze;
|
mod analyze;
|
||||||
|
mod annotation;
|
||||||
mod deferred;
|
mod deferred;
|
||||||
|
|
||||||
pub(crate) struct Checker<'a> {
|
pub(crate) struct Checker<'a> {
|
||||||
|
|
@ -515,8 +517,10 @@ where
|
||||||
.chain(¶meters.kwonlyargs)
|
.chain(¶meters.kwonlyargs)
|
||||||
{
|
{
|
||||||
if let Some(expr) = ¶meter_with_default.parameter.annotation {
|
if let Some(expr) = ¶meter_with_default.parameter.annotation {
|
||||||
if runtime_annotation || singledispatch {
|
if singledispatch {
|
||||||
self.visit_runtime_annotation(expr);
|
self.visit_runtime_required_annotation(expr);
|
||||||
|
} else if runtime_annotation {
|
||||||
|
self.visit_runtime_evaluated_annotation(expr);
|
||||||
} else {
|
} else {
|
||||||
self.visit_annotation(expr);
|
self.visit_annotation(expr);
|
||||||
};
|
};
|
||||||
|
|
@ -529,7 +533,7 @@ where
|
||||||
if let Some(arg) = ¶meters.vararg {
|
if let Some(arg) = ¶meters.vararg {
|
||||||
if let Some(expr) = &arg.annotation {
|
if let Some(expr) = &arg.annotation {
|
||||||
if runtime_annotation {
|
if runtime_annotation {
|
||||||
self.visit_runtime_annotation(expr);
|
self.visit_runtime_evaluated_annotation(expr);
|
||||||
} else {
|
} else {
|
||||||
self.visit_annotation(expr);
|
self.visit_annotation(expr);
|
||||||
};
|
};
|
||||||
|
|
@ -538,7 +542,7 @@ where
|
||||||
if let Some(arg) = ¶meters.kwarg {
|
if let Some(arg) = ¶meters.kwarg {
|
||||||
if let Some(expr) = &arg.annotation {
|
if let Some(expr) = &arg.annotation {
|
||||||
if runtime_annotation {
|
if runtime_annotation {
|
||||||
self.visit_runtime_annotation(expr);
|
self.visit_runtime_evaluated_annotation(expr);
|
||||||
} else {
|
} else {
|
||||||
self.visit_annotation(expr);
|
self.visit_annotation(expr);
|
||||||
};
|
};
|
||||||
|
|
@ -546,7 +550,7 @@ where
|
||||||
}
|
}
|
||||||
for expr in returns {
|
for expr in returns {
|
||||||
if runtime_annotation {
|
if runtime_annotation {
|
||||||
self.visit_runtime_annotation(expr);
|
self.visit_runtime_evaluated_annotation(expr);
|
||||||
} else {
|
} else {
|
||||||
self.visit_annotation(expr);
|
self.visit_annotation(expr);
|
||||||
};
|
};
|
||||||
|
|
@ -677,40 +681,16 @@ where
|
||||||
value,
|
value,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
// If we're in a class or module scope, then the annotation needs to be
|
match AnnotationContext::from_model(&self.semantic, self.settings) {
|
||||||
// available at runtime.
|
AnnotationContext::RuntimeRequired => {
|
||||||
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
|
self.visit_runtime_required_annotation(annotation);
|
||||||
let runtime_annotation = if self.semantic.future_annotations() {
|
}
|
||||||
self.semantic
|
AnnotationContext::RuntimeEvaluated => {
|
||||||
.current_scope()
|
self.visit_runtime_evaluated_annotation(annotation);
|
||||||
.kind
|
}
|
||||||
.as_class()
|
AnnotationContext::TypingOnly => self.visit_annotation(annotation),
|
||||||
.is_some_and(|class_def| {
|
|
||||||
flake8_type_checking::helpers::runtime_evaluated_class(
|
|
||||||
class_def,
|
|
||||||
&self
|
|
||||||
.settings
|
|
||||||
.flake8_type_checking
|
|
||||||
.runtime_evaluated_base_classes,
|
|
||||||
&self
|
|
||||||
.settings
|
|
||||||
.flake8_type_checking
|
|
||||||
.runtime_evaluated_decorators,
|
|
||||||
&self.semantic,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
matches!(
|
|
||||||
self.semantic.current_scope().kind,
|
|
||||||
ScopeKind::Class(_) | ScopeKind::Module
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if runtime_annotation {
|
|
||||||
self.visit_runtime_annotation(annotation);
|
|
||||||
} else {
|
|
||||||
self.visit_annotation(annotation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(expr) = value {
|
if let Some(expr) = value {
|
||||||
if self.semantic.match_typing_expr(annotation, "TypeAlias") {
|
if self.semantic.match_typing_expr(annotation, "TypeAlias") {
|
||||||
self.visit_type_definition(expr);
|
self.visit_type_definition(expr);
|
||||||
|
|
@ -1527,10 +1507,18 @@ impl<'a> Checker<'a> {
|
||||||
self.semantic.flags = snapshot;
|
self.semantic.flags = snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Visit an [`Expr`], and treat it as a runtime-required type annotation.
|
/// Visit an [`Expr`], and treat it as a runtime-evaluated type annotation.
|
||||||
fn visit_runtime_annotation(&mut self, expr: &'a Expr) {
|
fn visit_runtime_evaluated_annotation(&mut self, expr: &'a Expr) {
|
||||||
let snapshot = self.semantic.flags;
|
let snapshot = self.semantic.flags;
|
||||||
self.semantic.flags |= SemanticModelFlags::RUNTIME_ANNOTATION;
|
self.semantic.flags |= SemanticModelFlags::RUNTIME_EVALUATED_ANNOTATION;
|
||||||
|
self.visit_type_definition(expr);
|
||||||
|
self.semantic.flags = snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Visit an [`Expr`], and treat it as a runtime-required type annotation.
|
||||||
|
fn visit_runtime_required_annotation(&mut self, expr: &'a Expr) {
|
||||||
|
let snapshot = self.semantic.flags;
|
||||||
|
self.semantic.flags |= SemanticModelFlags::RUNTIME_REQUIRED_ANNOTATION;
|
||||||
self.visit_type_definition(expr);
|
self.visit_type_definition(expr);
|
||||||
self.semantic.flags = snapshot;
|
self.semantic.flags = snapshot;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,35 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
|
use ruff_diagnostics::Edit;
|
||||||
use ruff_python_ast::call_path::from_qualified_name;
|
use ruff_python_ast::call_path::from_qualified_name;
|
||||||
use ruff_python_ast::helpers::{map_callable, map_subscript};
|
use ruff_python_ast::helpers::{map_callable, map_subscript};
|
||||||
use ruff_python_ast::{self as ast, Expr};
|
use ruff_python_ast::{self as ast, Expr};
|
||||||
use ruff_python_semantic::{Binding, BindingId, BindingKind, SemanticModel};
|
use ruff_python_codegen::Stylist;
|
||||||
use rustc_hash::FxHashSet;
|
use ruff_python_semantic::{
|
||||||
|
Binding, BindingId, BindingKind, NodeId, ResolvedReference, SemanticModel,
|
||||||
|
};
|
||||||
|
use ruff_source_file::Locator;
|
||||||
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
pub(crate) fn is_valid_runtime_import(binding: &Binding, semantic: &SemanticModel) -> bool {
|
use crate::rules::flake8_type_checking::settings::Settings;
|
||||||
|
|
||||||
|
/// Returns `true` if the [`ResolvedReference`] is in a typing-only context _or_ a runtime-evaluated
|
||||||
|
/// context (with quoting enabled).
|
||||||
|
pub(crate) fn is_typing_reference(reference: &ResolvedReference, settings: &Settings) -> bool {
|
||||||
|
reference.in_type_checking_block()
|
||||||
|
|| reference.in_typing_only_annotation()
|
||||||
|
|| reference.in_complex_string_type_definition()
|
||||||
|
|| reference.in_simple_string_type_definition()
|
||||||
|
|| (settings.quote_annotations && reference.in_runtime_evaluated_annotation())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the [`Binding`] represents a runtime-required import.
|
||||||
|
pub(crate) fn is_valid_runtime_import(
|
||||||
|
binding: &Binding,
|
||||||
|
semantic: &SemanticModel,
|
||||||
|
settings: &Settings,
|
||||||
|
) -> bool {
|
||||||
if matches!(
|
if matches!(
|
||||||
binding.kind,
|
binding.kind,
|
||||||
BindingKind::Import(..) | BindingKind::FromImport(..) | BindingKind::SubmoduleImport(..)
|
BindingKind::Import(..) | BindingKind::FromImport(..) | BindingKind::SubmoduleImport(..)
|
||||||
|
|
@ -12,28 +37,29 @@ pub(crate) fn is_valid_runtime_import(binding: &Binding, semantic: &SemanticMode
|
||||||
binding.context.is_runtime()
|
binding.context.is_runtime()
|
||||||
&& binding
|
&& binding
|
||||||
.references()
|
.references()
|
||||||
.any(|reference_id| semantic.reference(reference_id).context().is_runtime())
|
.map(|reference_id| semantic.reference(reference_id))
|
||||||
|
.any(|reference| !is_typing_reference(reference, settings))
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn runtime_evaluated_class(
|
pub(crate) fn runtime_required_class(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
base_classes: &[String],
|
base_classes: &[String],
|
||||||
decorators: &[String],
|
decorators: &[String],
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if runtime_evaluated_base_class(class_def, base_classes, semantic) {
|
if runtime_required_base_class(class_def, base_classes, semantic) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if runtime_evaluated_decorators(class_def, decorators, semantic) {
|
if runtime_required_decorators(class_def, decorators, semantic) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runtime_evaluated_base_class(
|
fn runtime_required_base_class(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
base_classes: &[String],
|
base_classes: &[String],
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
|
|
@ -45,7 +71,7 @@ fn runtime_evaluated_base_class(
|
||||||
seen: &mut FxHashSet<BindingId>,
|
seen: &mut FxHashSet<BindingId>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
class_def.bases().iter().any(|expr| {
|
class_def.bases().iter().any(|expr| {
|
||||||
// If the base class is itself runtime-evaluated, then this is too.
|
// If the base class is itself runtime-required, then this is too.
|
||||||
// Ex) `class Foo(BaseModel): ...`
|
// Ex) `class Foo(BaseModel): ...`
|
||||||
if semantic
|
if semantic
|
||||||
.resolve_call_path(map_subscript(expr))
|
.resolve_call_path(map_subscript(expr))
|
||||||
|
|
@ -58,7 +84,7 @@ fn runtime_evaluated_base_class(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the base class extends a runtime-evaluated class, then this does too.
|
// If the base class extends a runtime-required class, then this does too.
|
||||||
// Ex) `class Bar(BaseModel): ...; class Foo(Bar): ...`
|
// Ex) `class Bar(BaseModel): ...; class Foo(Bar): ...`
|
||||||
if let Some(id) = semantic.lookup_attribute(map_subscript(expr)) {
|
if let Some(id) = semantic.lookup_attribute(map_subscript(expr)) {
|
||||||
if seen.insert(id) {
|
if seen.insert(id) {
|
||||||
|
|
@ -86,7 +112,7 @@ fn runtime_evaluated_base_class(
|
||||||
inner(class_def, base_classes, semantic, &mut FxHashSet::default())
|
inner(class_def, base_classes, semantic, &mut FxHashSet::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runtime_evaluated_decorators(
|
fn runtime_required_decorators(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
decorators: &[String],
|
decorators: &[String],
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
|
|
@ -174,3 +200,75 @@ pub(crate) fn is_singledispatch_implementation(
|
||||||
is_singledispatch_interface(function_def, semantic)
|
is_singledispatch_interface(function_def, semantic)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrap a type annotation in quotes.
|
||||||
|
///
|
||||||
|
/// This requires more than just wrapping the reference itself in quotes. For example:
|
||||||
|
/// - When quoting `Series` in `Series[pd.Timestamp]`, we want `"Series[pd.Timestamp]"`.
|
||||||
|
/// - When quoting `kubernetes` in `kubernetes.SecurityContext`, we want `"kubernetes.SecurityContext"`.
|
||||||
|
/// - When quoting `Series` in `Series["pd.Timestamp"]`, we want `"Series[pd.Timestamp]"`. (This is currently unsupported.)
|
||||||
|
/// - When quoting `Series` in `Series[Literal["pd.Timestamp"]]`, we want `"Series[Literal['pd.Timestamp']]"`. (This is currently unsupported.)
|
||||||
|
///
|
||||||
|
/// In general, when expanding a component of a call chain, we want to quote the entire call chain.
|
||||||
|
pub(crate) fn quote_annotation(
|
||||||
|
node_id: NodeId,
|
||||||
|
semantic: &SemanticModel,
|
||||||
|
locator: &Locator,
|
||||||
|
stylist: &Stylist,
|
||||||
|
) -> Result<Edit> {
|
||||||
|
let expr = semantic.expression(node_id).expect("Expression not found");
|
||||||
|
if let Some(parent_id) = semantic.parent_expression_id(node_id) {
|
||||||
|
match semantic.expression(parent_id) {
|
||||||
|
Some(Expr::Subscript(parent)) => {
|
||||||
|
if expr == parent.value.as_ref() {
|
||||||
|
// If we're quoting the value of a subscript, we need to quote the entire
|
||||||
|
// expression. For example, when quoting `DataFrame` in `DataFrame[int]`, we
|
||||||
|
// should generate `"DataFrame[int]"`.
|
||||||
|
return quote_annotation(parent_id, semantic, locator, stylist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Expr::Attribute(parent)) => {
|
||||||
|
if expr == parent.value.as_ref() {
|
||||||
|
// If we're quoting the value of an attribute, we need to quote the entire
|
||||||
|
// expression. For example, when quoting `DataFrame` in `pd.DataFrame`, we
|
||||||
|
// should generate `"pd.DataFrame"`.
|
||||||
|
return quote_annotation(parent_id, semantic, locator, stylist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Expr::Call(parent)) => {
|
||||||
|
if expr == parent.func.as_ref() {
|
||||||
|
// If we're quoting the function of a call, we need to quote the entire
|
||||||
|
// expression. For example, when quoting `DataFrame` in `DataFrame()`, we
|
||||||
|
// should generate `"DataFrame()"`.
|
||||||
|
return quote_annotation(parent_id, semantic, locator, stylist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Expr::BinOp(parent)) => {
|
||||||
|
if parent.op.is_bit_or() {
|
||||||
|
// If we're quoting the left or right side of a binary operation, we need to
|
||||||
|
// quote the entire expression. For example, when quoting `DataFrame` in
|
||||||
|
// `DataFrame | Series`, we should generate `"DataFrame | Series"`.
|
||||||
|
return quote_annotation(parent_id, semantic, locator, stylist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let annotation = locator.slice(expr);
|
||||||
|
|
||||||
|
// If the annotation already contains a quote, avoid attempting to re-quote it. For example:
|
||||||
|
// ```python
|
||||||
|
// from typing import Literal
|
||||||
|
//
|
||||||
|
// Set[Literal["Foo"]]
|
||||||
|
// ```
|
||||||
|
if annotation.contains('\'') || annotation.contains('"') {
|
||||||
|
return Err(anyhow::anyhow!("Annotation already contains a quote"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're quoting a name, we need to quote the entire expression.
|
||||||
|
let quote = stylist.quote();
|
||||||
|
let annotation = format!("{quote}{annotation}{quote}");
|
||||||
|
Ok(Edit::range_replacement(annotation, expr.range()))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
use ruff_python_semantic::{AnyImport, Binding, ResolvedReferenceId};
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
|
/// An import with its surrounding context.
|
||||||
|
pub(crate) struct ImportBinding<'a> {
|
||||||
|
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
|
||||||
|
pub(crate) import: AnyImport<'a>,
|
||||||
|
/// The binding for the imported symbol.
|
||||||
|
pub(crate) binding: &'a Binding<'a>,
|
||||||
|
/// The first reference to the imported symbol.
|
||||||
|
pub(crate) reference_id: ResolvedReferenceId,
|
||||||
|
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
|
||||||
|
pub(crate) range: TextRange,
|
||||||
|
/// The range of the import's parent statement.
|
||||||
|
pub(crate) parent_range: Option<TextRange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ranged for ImportBinding<'_> {
|
||||||
|
fn range(&self) -> TextRange {
|
||||||
|
self.range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//! Rules from [flake8-type-checking](https://pypi.org/project/flake8-type-checking/).
|
//! Rules from [flake8-type-checking](https://pypi.org/project/flake8-type-checking/).
|
||||||
pub(crate) mod helpers;
|
pub(crate) mod helpers;
|
||||||
|
mod imports;
|
||||||
pub(crate) mod rules;
|
pub(crate) mod rules;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
|
|
@ -33,10 +34,12 @@ mod tests {
|
||||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_7.py"))]
|
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_7.py"))]
|
||||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"))]
|
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"))]
|
||||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"))]
|
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"))]
|
||||||
|
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
|
||||||
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
|
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
|
||||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
|
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
|
||||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))]
|
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))]
|
||||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
|
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
|
||||||
|
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote.py"))]
|
||||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("singledispatch.py"))]
|
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("singledispatch.py"))]
|
||||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
|
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
|
||||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_1.py"))]
|
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_1.py"))]
|
||||||
|
|
@ -51,6 +54,24 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
|
||||||
|
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote.py"))]
|
||||||
|
fn quote(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
|
let snapshot = format!("quote_{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||||
|
let diagnostics = test_path(
|
||||||
|
Path::new("flake8_type_checking").join(path).as_path(),
|
||||||
|
&settings::LinterSettings {
|
||||||
|
flake8_type_checking: super::settings::Settings {
|
||||||
|
quote_annotations: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..settings::LinterSettings::for_rule(rule_code)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
assert_messages!(snapshot, diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
|
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
|
||||||
fn strict(rule_code: Rule, path: &Path) -> Result<()> {
|
fn strict(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let diagnostics = test_path(
|
let diagnostics = test_path(
|
||||||
|
|
@ -109,7 +130,7 @@ mod tests {
|
||||||
Path::new("flake8_type_checking").join(path).as_path(),
|
Path::new("flake8_type_checking").join(path).as_path(),
|
||||||
&settings::LinterSettings {
|
&settings::LinterSettings {
|
||||||
flake8_type_checking: super::settings::Settings {
|
flake8_type_checking: super::settings::Settings {
|
||||||
runtime_evaluated_base_classes: vec![
|
runtime_required_base_classes: vec![
|
||||||
"pydantic.BaseModel".to_string(),
|
"pydantic.BaseModel".to_string(),
|
||||||
"sqlalchemy.orm.DeclarativeBase".to_string(),
|
"sqlalchemy.orm.DeclarativeBase".to_string(),
|
||||||
],
|
],
|
||||||
|
|
@ -140,7 +161,7 @@ mod tests {
|
||||||
Path::new("flake8_type_checking").join(path).as_path(),
|
Path::new("flake8_type_checking").join(path).as_path(),
|
||||||
&settings::LinterSettings {
|
&settings::LinterSettings {
|
||||||
flake8_type_checking: super::settings::Settings {
|
flake8_type_checking: super::settings::Settings {
|
||||||
runtime_evaluated_decorators: vec![
|
runtime_required_decorators: vec![
|
||||||
"attrs.define".to_string(),
|
"attrs.define".to_string(),
|
||||||
"attrs.frozen".to_string(),
|
"attrs.frozen".to_string(),
|
||||||
],
|
],
|
||||||
|
|
@ -165,7 +186,7 @@ mod tests {
|
||||||
Path::new("flake8_type_checking").join(path).as_path(),
|
Path::new("flake8_type_checking").join(path).as_path(),
|
||||||
&settings::LinterSettings {
|
&settings::LinterSettings {
|
||||||
flake8_type_checking: super::settings::Settings {
|
flake8_type_checking: super::settings::Settings {
|
||||||
runtime_evaluated_base_classes: vec!["module.direct.MyBaseClass".to_string()],
|
runtime_required_base_classes: vec!["module.direct.MyBaseClass".to_string()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..settings::LinterSettings::for_rule(rule_code)
|
..settings::LinterSettings::for_rule(rule_code)
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@ use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_semantic::{AnyImport, Imported, NodeId, ResolvedReferenceId, Scope};
|
use ruff_python_semantic::{Imported, NodeId, Scope};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::codes::Rule;
|
use crate::codes::Rule;
|
||||||
use crate::fix;
|
use crate::fix;
|
||||||
use crate::importer::ImportedMembers;
|
use crate::importer::ImportedMembers;
|
||||||
|
use crate::rules::flake8_type_checking::helpers::quote_annotation;
|
||||||
|
use crate::rules::flake8_type_checking::imports::ImportBinding;
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for runtime imports defined in a type-checking block.
|
/// Checks for runtime imports defined in a type-checking block.
|
||||||
|
|
@ -20,6 +22,10 @@ use crate::importer::ImportedMembers;
|
||||||
/// The type-checking block is not executed at runtime, so the import will not
|
/// The type-checking block is not executed at runtime, so the import will not
|
||||||
/// be available at runtime.
|
/// be available at runtime.
|
||||||
///
|
///
|
||||||
|
/// If [`flake8-type-checking.quote-annotations`] is set to `true`,
|
||||||
|
/// annotations will be wrapped in quotes if doing so would enable the
|
||||||
|
/// corresponding import to remain in the type-checking block.
|
||||||
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
/// ```python
|
/// ```python
|
||||||
/// from typing import TYPE_CHECKING
|
/// from typing import TYPE_CHECKING
|
||||||
|
|
@ -41,11 +47,15 @@ use crate::importer::ImportedMembers;
|
||||||
/// foo.bar()
|
/// foo.bar()
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
/// ## Options
|
||||||
|
/// - `flake8-type-checking.quote-annotations`
|
||||||
|
///
|
||||||
/// ## References
|
/// ## References
|
||||||
/// - [PEP 535](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
/// - [PEP 535](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||||
#[violation]
|
#[violation]
|
||||||
pub struct RuntimeImportInTypeCheckingBlock {
|
pub struct RuntimeImportInTypeCheckingBlock {
|
||||||
qualified_name: String,
|
qualified_name: String,
|
||||||
|
strategy: Strategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Violation for RuntimeImportInTypeCheckingBlock {
|
impl Violation for RuntimeImportInTypeCheckingBlock {
|
||||||
|
|
@ -53,17 +63,39 @@ impl Violation for RuntimeImportInTypeCheckingBlock {
|
||||||
|
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
let RuntimeImportInTypeCheckingBlock { qualified_name } = self;
|
let Self {
|
||||||
format!(
|
qualified_name,
|
||||||
"Move import `{qualified_name}` out of type-checking block. Import is used for more than type hinting."
|
strategy,
|
||||||
)
|
} = self;
|
||||||
|
match strategy {
|
||||||
|
Strategy::MoveImport => format!(
|
||||||
|
"Move import `{qualified_name}` out of type-checking block. Import is used for more than type hinting."
|
||||||
|
),
|
||||||
|
Strategy::QuoteUsages => format!(
|
||||||
|
"Quote references to `{qualified_name}`. Import is in a type-checking block."
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fix_title(&self) -> Option<String> {
|
fn fix_title(&self) -> Option<String> {
|
||||||
Some("Move out of type-checking block".to_string())
|
let Self { strategy, .. } = self;
|
||||||
|
match strategy {
|
||||||
|
Strategy::MoveImport => Some("Move out of type-checking block".to_string()),
|
||||||
|
Strategy::QuoteUsages => Some("Quote references".to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
enum Action {
|
||||||
|
/// The import should be moved out of the type-checking block.
|
||||||
|
Move,
|
||||||
|
/// All usages of the import should be wrapped in quotes.
|
||||||
|
Quote,
|
||||||
|
/// The import should be ignored.
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
/// TCH004
|
/// TCH004
|
||||||
pub(crate) fn runtime_import_in_type_checking_block(
|
pub(crate) fn runtime_import_in_type_checking_block(
|
||||||
checker: &Checker,
|
checker: &Checker,
|
||||||
|
|
@ -71,8 +103,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||||
diagnostics: &mut Vec<Diagnostic>,
|
diagnostics: &mut Vec<Diagnostic>,
|
||||||
) {
|
) {
|
||||||
// Collect all runtime imports by statement.
|
// Collect all runtime imports by statement.
|
||||||
let mut errors_by_statement: FxHashMap<NodeId, Vec<ImportBinding>> = FxHashMap::default();
|
let mut actions: FxHashMap<(NodeId, Action), Vec<ImportBinding>> = FxHashMap::default();
|
||||||
let mut ignores_by_statement: FxHashMap<NodeId, Vec<ImportBinding>> = FxHashMap::default();
|
|
||||||
|
|
||||||
for binding_id in scope.binding_ids() {
|
for binding_id in scope.binding_ids() {
|
||||||
let binding = checker.semantic().binding(binding_id);
|
let binding = checker.semantic().binding(binding_id);
|
||||||
|
|
@ -101,6 +132,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||||
let import = ImportBinding {
|
let import = ImportBinding {
|
||||||
import,
|
import,
|
||||||
reference_id,
|
reference_id,
|
||||||
|
binding,
|
||||||
range: binding.range(),
|
range: binding.range(),
|
||||||
parent_range: binding.parent_range(checker.semantic()),
|
parent_range: binding.parent_range(checker.semantic()),
|
||||||
};
|
};
|
||||||
|
|
@ -113,86 +145,153 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
ignores_by_statement
|
actions
|
||||||
.entry(node_id)
|
.entry((node_id, Action::Ignore))
|
||||||
.or_default()
|
.or_default()
|
||||||
.push(import);
|
.push(import);
|
||||||
} else {
|
} else {
|
||||||
errors_by_statement.entry(node_id).or_default().push(import);
|
// Determine whether the member should be fixed by moving the import out of the
|
||||||
|
// type-checking block, or by quoting its references.
|
||||||
|
if checker.settings.flake8_type_checking.quote_annotations
|
||||||
|
&& binding.references().all(|reference_id| {
|
||||||
|
let reference = checker.semantic().reference(reference_id);
|
||||||
|
reference.context().is_typing()
|
||||||
|
|| reference.in_runtime_evaluated_annotation()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
actions
|
||||||
|
.entry((node_id, Action::Quote))
|
||||||
|
.or_default()
|
||||||
|
.push(import);
|
||||||
|
} else {
|
||||||
|
actions
|
||||||
|
.entry((node_id, Action::Move))
|
||||||
|
.or_default()
|
||||||
|
.push(import);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a diagnostic for every import, but share a fix across all imports within the same
|
for ((node_id, action), imports) in actions {
|
||||||
// statement (excluding those that are ignored).
|
match action {
|
||||||
for (node_id, imports) in errors_by_statement {
|
// Generate a diagnostic for every import, but share a fix across all imports within the same
|
||||||
let fix = fix_imports(checker, node_id, &imports).ok();
|
// statement (excluding those that are ignored).
|
||||||
|
Action::Move => {
|
||||||
|
let fix = move_imports(checker, node_id, &imports).ok();
|
||||||
|
|
||||||
for ImportBinding {
|
for ImportBinding {
|
||||||
import,
|
import,
|
||||||
range,
|
range,
|
||||||
parent_range,
|
parent_range,
|
||||||
..
|
..
|
||||||
} in imports
|
} in imports
|
||||||
{
|
{
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
RuntimeImportInTypeCheckingBlock {
|
RuntimeImportInTypeCheckingBlock {
|
||||||
qualified_name: import.qualified_name(),
|
qualified_name: import.qualified_name(),
|
||||||
},
|
strategy: Strategy::MoveImport,
|
||||||
range,
|
},
|
||||||
);
|
range,
|
||||||
if let Some(range) = parent_range {
|
);
|
||||||
diagnostic.set_parent(range.start());
|
if let Some(range) = parent_range {
|
||||||
|
diagnostic.set_parent(range.start());
|
||||||
|
}
|
||||||
|
if let Some(fix) = fix.as_ref() {
|
||||||
|
diagnostic.set_fix(fix.clone());
|
||||||
|
}
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(fix) = fix.as_ref() {
|
|
||||||
diagnostic.set_fix(fix.clone());
|
|
||||||
}
|
|
||||||
diagnostics.push(diagnostic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Separately, generate a diagnostic for every _ignored_ import, to ensure that the
|
// Generate a diagnostic for every import, but share a fix across all imports within the same
|
||||||
// suppression comments aren't marked as unused.
|
// statement (excluding those that are ignored).
|
||||||
for ImportBinding {
|
Action::Quote => {
|
||||||
import,
|
let fix = quote_imports(checker, node_id, &imports).ok();
|
||||||
range,
|
|
||||||
parent_range,
|
for ImportBinding {
|
||||||
..
|
import,
|
||||||
} in ignores_by_statement.into_values().flatten()
|
range,
|
||||||
{
|
parent_range,
|
||||||
let mut diagnostic = Diagnostic::new(
|
..
|
||||||
RuntimeImportInTypeCheckingBlock {
|
} in imports
|
||||||
qualified_name: import.qualified_name(),
|
{
|
||||||
},
|
let mut diagnostic = Diagnostic::new(
|
||||||
range,
|
RuntimeImportInTypeCheckingBlock {
|
||||||
);
|
qualified_name: import.qualified_name(),
|
||||||
if let Some(range) = parent_range {
|
strategy: Strategy::QuoteUsages,
|
||||||
diagnostic.set_parent(range.start());
|
},
|
||||||
|
range,
|
||||||
|
);
|
||||||
|
if let Some(range) = parent_range {
|
||||||
|
diagnostic.set_parent(range.start());
|
||||||
|
}
|
||||||
|
if let Some(fix) = fix.as_ref() {
|
||||||
|
diagnostic.set_fix(fix.clone());
|
||||||
|
}
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separately, generate a diagnostic for every _ignored_ import, to ensure that the
|
||||||
|
// suppression comments aren't marked as unused.
|
||||||
|
Action::Ignore => {
|
||||||
|
for ImportBinding {
|
||||||
|
import,
|
||||||
|
range,
|
||||||
|
parent_range,
|
||||||
|
..
|
||||||
|
} in imports
|
||||||
|
{
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
RuntimeImportInTypeCheckingBlock {
|
||||||
|
qualified_name: import.qualified_name(),
|
||||||
|
strategy: Strategy::MoveImport,
|
||||||
|
},
|
||||||
|
range,
|
||||||
|
);
|
||||||
|
if let Some(range) = parent_range {
|
||||||
|
diagnostic.set_parent(range.start());
|
||||||
|
}
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
diagnostics.push(diagnostic);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A runtime-required import with its surrounding context.
|
/// Generate a [`Fix`] to quote runtime usages for imports in a type-checking block.
|
||||||
struct ImportBinding<'a> {
|
fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
|
||||||
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
|
let mut quote_reference_edits = imports
|
||||||
import: AnyImport<'a>,
|
.iter()
|
||||||
/// The first reference to the imported symbol.
|
.flat_map(|ImportBinding { binding, .. }| {
|
||||||
reference_id: ResolvedReferenceId,
|
binding.references.iter().filter_map(|reference_id| {
|
||||||
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
|
let reference = checker.semantic().reference(*reference_id);
|
||||||
range: TextRange,
|
if reference.context().is_runtime() {
|
||||||
/// The range of the import's parent statement.
|
Some(quote_annotation(
|
||||||
parent_range: Option<TextRange>,
|
reference.expression_id()?,
|
||||||
}
|
checker.semantic(),
|
||||||
|
checker.locator(),
|
||||||
impl Ranged for ImportBinding<'_> {
|
checker.stylist(),
|
||||||
fn range(&self) -> TextRange {
|
))
|
||||||
self.range
|
} else {
|
||||||
}
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
let quote_reference_edit = quote_reference_edits
|
||||||
|
.pop()
|
||||||
|
.expect("Expected at least one reference");
|
||||||
|
Ok(
|
||||||
|
Fix::unsafe_edits(quote_reference_edit, quote_reference_edits).isolate(Checker::isolation(
|
||||||
|
checker.semantic().parent_statement_id(node_id),
|
||||||
|
)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a [`Fix`] to remove runtime imports from a type-checking block.
|
/// Generate a [`Fix`] to remove runtime imports from a type-checking block.
|
||||||
fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
|
fn move_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
|
||||||
let statement = checker.semantic().statement(node_id);
|
let statement = checker.semantic().statement(node_id);
|
||||||
let parent = checker.semantic().parent_statement(node_id);
|
let parent = checker.semantic().parent_statement(node_id);
|
||||||
|
|
||||||
|
|
@ -236,3 +335,18 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Strategy {
|
||||||
|
/// The import should be moved out of the type-checking block.
|
||||||
|
///
|
||||||
|
/// This is required when at least one reference to the symbol is in a runtime-required context.
|
||||||
|
/// For example, given `from foo import Bar`, `x = Bar()` would be runtime-required.
|
||||||
|
MoveImport,
|
||||||
|
/// All usages of the import should be wrapped in quotes.
|
||||||
|
///
|
||||||
|
/// This is acceptable when all references to the symbol are in a runtime-evaluated, but not
|
||||||
|
/// runtime-required context. For example, given `from foo import Bar`, `x: Bar` would be
|
||||||
|
/// runtime-evaluated, but not runtime-required.
|
||||||
|
QuoteUsages,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@ use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix, FixAvailability, Violation};
|
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix, FixAvailability, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_semantic::{AnyImport, Binding, Imported, NodeId, ResolvedReferenceId, Scope};
|
use ruff_python_semantic::{Binding, Imported, NodeId, Scope};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::codes::Rule;
|
use crate::codes::Rule;
|
||||||
use crate::fix;
|
use crate::fix;
|
||||||
use crate::importer::ImportedMembers;
|
use crate::importer::ImportedMembers;
|
||||||
|
use crate::rules::flake8_type_checking::helpers::{is_typing_reference, quote_annotation};
|
||||||
|
use crate::rules::flake8_type_checking::imports::ImportBinding;
|
||||||
use crate::rules::isort::{categorize, ImportSection, ImportType};
|
use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
|
|
@ -24,6 +26,10 @@ use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||||
/// instead be imported conditionally under an `if TYPE_CHECKING:` block to
|
/// instead be imported conditionally under an `if TYPE_CHECKING:` block to
|
||||||
/// minimize runtime overhead.
|
/// minimize runtime overhead.
|
||||||
///
|
///
|
||||||
|
/// If [`flake8-type-checking.quote-annotations`] is set to `true`,
|
||||||
|
/// annotations will be wrapped in quotes if doing so would enable the
|
||||||
|
/// corresponding import to be moved into an `if TYPE_CHECKING:` block.
|
||||||
|
///
|
||||||
/// If a class _requires_ that type annotations be available at runtime (as is
|
/// If a class _requires_ that type annotations be available at runtime (as is
|
||||||
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
|
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
|
||||||
/// the [`flake8-type-checking.runtime-evaluated-base-classes`] and
|
/// the [`flake8-type-checking.runtime-evaluated-base-classes`] and
|
||||||
|
|
@ -56,6 +62,7 @@ use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Options
|
/// ## Options
|
||||||
|
/// - `flake8-type-checking.quote-annotations`
|
||||||
/// - `flake8-type-checking.runtime-evaluated-base-classes`
|
/// - `flake8-type-checking.runtime-evaluated-base-classes`
|
||||||
/// - `flake8-type-checking.runtime-evaluated-decorators`
|
/// - `flake8-type-checking.runtime-evaluated-decorators`
|
||||||
///
|
///
|
||||||
|
|
@ -92,6 +99,10 @@ impl Violation for TypingOnlyFirstPartyImport {
|
||||||
/// instead be imported conditionally under an `if TYPE_CHECKING:` block to
|
/// instead be imported conditionally under an `if TYPE_CHECKING:` block to
|
||||||
/// minimize runtime overhead.
|
/// minimize runtime overhead.
|
||||||
///
|
///
|
||||||
|
/// If [`flake8-type-checking.quote-annotations`] is set to `true`,
|
||||||
|
/// annotations will be wrapped in quotes if doing so would enable the
|
||||||
|
/// corresponding import to be moved into an `if TYPE_CHECKING:` block.
|
||||||
|
///
|
||||||
/// If a class _requires_ that type annotations be available at runtime (as is
|
/// If a class _requires_ that type annotations be available at runtime (as is
|
||||||
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
|
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
|
||||||
/// the [`flake8-type-checking.runtime-evaluated-base-classes`] and
|
/// the [`flake8-type-checking.runtime-evaluated-base-classes`] and
|
||||||
|
|
@ -124,6 +135,7 @@ impl Violation for TypingOnlyFirstPartyImport {
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Options
|
/// ## Options
|
||||||
|
/// - `flake8-type-checking.quote-annotations`
|
||||||
/// - `flake8-type-checking.runtime-evaluated-base-classes`
|
/// - `flake8-type-checking.runtime-evaluated-base-classes`
|
||||||
/// - `flake8-type-checking.runtime-evaluated-decorators`
|
/// - `flake8-type-checking.runtime-evaluated-decorators`
|
||||||
///
|
///
|
||||||
|
|
@ -160,6 +172,10 @@ impl Violation for TypingOnlyThirdPartyImport {
|
||||||
/// instead be imported conditionally under an `if TYPE_CHECKING:` block to
|
/// instead be imported conditionally under an `if TYPE_CHECKING:` block to
|
||||||
/// minimize runtime overhead.
|
/// minimize runtime overhead.
|
||||||
///
|
///
|
||||||
|
/// If [`flake8-type-checking.quote-annotations`] is set to `true`,
|
||||||
|
/// annotations will be wrapped in quotes if doing so would enable the
|
||||||
|
/// corresponding import to be moved into an `if TYPE_CHECKING:` block.
|
||||||
|
///
|
||||||
/// If a class _requires_ that type annotations be available at runtime (as is
|
/// If a class _requires_ that type annotations be available at runtime (as is
|
||||||
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
|
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
|
||||||
/// the [`flake8-type-checking.runtime-evaluated-base-classes`] and
|
/// the [`flake8-type-checking.runtime-evaluated-base-classes`] and
|
||||||
|
|
@ -192,6 +208,7 @@ impl Violation for TypingOnlyThirdPartyImport {
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Options
|
/// ## Options
|
||||||
|
/// - `flake8-type-checking.quote-annotations`
|
||||||
/// - `flake8-type-checking.runtime-evaluated-base-classes`
|
/// - `flake8-type-checking.runtime-evaluated-base-classes`
|
||||||
/// - `flake8-type-checking.runtime-evaluated-decorators`
|
/// - `flake8-type-checking.runtime-evaluated-decorators`
|
||||||
///
|
///
|
||||||
|
|
@ -253,13 +270,12 @@ pub(crate) fn typing_only_runtime_import(
|
||||||
};
|
};
|
||||||
|
|
||||||
if binding.context.is_runtime()
|
if binding.context.is_runtime()
|
||||||
&& binding.references().all(|reference_id| {
|
&& binding
|
||||||
checker
|
.references()
|
||||||
.semantic()
|
.map(|reference_id| checker.semantic().reference(reference_id))
|
||||||
.reference(reference_id)
|
.all(|reference| {
|
||||||
.context()
|
is_typing_reference(reference, &checker.settings.flake8_type_checking)
|
||||||
.is_typing()
|
})
|
||||||
})
|
|
||||||
{
|
{
|
||||||
let qualified_name = import.qualified_name();
|
let qualified_name = import.qualified_name();
|
||||||
|
|
||||||
|
|
@ -310,6 +326,7 @@ pub(crate) fn typing_only_runtime_import(
|
||||||
let import = ImportBinding {
|
let import = ImportBinding {
|
||||||
import,
|
import,
|
||||||
reference_id,
|
reference_id,
|
||||||
|
binding,
|
||||||
range: binding.range(),
|
range: binding.range(),
|
||||||
parent_range: binding.parent_range(checker.semantic()),
|
parent_range: binding.parent_range(checker.semantic()),
|
||||||
};
|
};
|
||||||
|
|
@ -376,24 +393,6 @@ pub(crate) fn typing_only_runtime_import(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A runtime-required import with its surrounding context.
|
|
||||||
struct ImportBinding<'a> {
|
|
||||||
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
|
|
||||||
import: AnyImport<'a>,
|
|
||||||
/// The first reference to the imported symbol.
|
|
||||||
reference_id: ResolvedReferenceId,
|
|
||||||
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
|
|
||||||
range: TextRange,
|
|
||||||
/// The range of the import's parent statement.
|
|
||||||
parent_range: Option<TextRange>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ranged for ImportBinding<'_> {
|
|
||||||
fn range(&self) -> TextRange {
|
|
||||||
self.range
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the [`Rule`] for the given import type.
|
/// Return the [`Rule`] for the given import type.
|
||||||
fn rule_for(import_type: ImportType) -> Rule {
|
fn rule_for(import_type: ImportType) -> Rule {
|
||||||
match import_type {
|
match import_type {
|
||||||
|
|
@ -482,9 +481,34 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
|
||||||
checker.source_type,
|
checker.source_type,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(
|
// Step 3) Quote any runtime usages of the referenced symbol.
|
||||||
Fix::unsafe_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
|
let quote_reference_edits = imports
|
||||||
Checker::isolation(checker.semantic().parent_statement_id(node_id)),
|
.iter()
|
||||||
),
|
.flat_map(|ImportBinding { binding, .. }| {
|
||||||
|
binding.references.iter().filter_map(|reference_id| {
|
||||||
|
let reference = checker.semantic().reference(*reference_id);
|
||||||
|
if reference.context().is_runtime() {
|
||||||
|
Some(quote_annotation(
|
||||||
|
reference.expression_id()?,
|
||||||
|
checker.semantic(),
|
||||||
|
checker.locator(),
|
||||||
|
checker.stylist(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
Ok(Fix::unsafe_edits(
|
||||||
|
remove_import_edit,
|
||||||
|
add_import_edit
|
||||||
|
.into_edits()
|
||||||
|
.into_iter()
|
||||||
|
.chain(quote_reference_edits),
|
||||||
)
|
)
|
||||||
|
.isolate(Checker::isolation(
|
||||||
|
checker.semantic().parent_statement_id(node_id),
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,19 @@ use ruff_macros::CacheKey;
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub strict: bool,
|
pub strict: bool,
|
||||||
pub exempt_modules: Vec<String>,
|
pub exempt_modules: Vec<String>,
|
||||||
pub runtime_evaluated_base_classes: Vec<String>,
|
pub runtime_required_base_classes: Vec<String>,
|
||||||
pub runtime_evaluated_decorators: Vec<String>,
|
pub runtime_required_decorators: Vec<String>,
|
||||||
|
pub quote_annotations: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
strict: false,
|
strict: false,
|
||||||
exempt_modules: vec!["typing".to_string()],
|
exempt_modules: vec!["typing".to_string(), "typing_extensions".to_string()],
|
||||||
runtime_evaluated_base_classes: vec![],
|
runtime_required_base_classes: vec![],
|
||||||
runtime_evaluated_decorators: vec![],
|
runtime_required_decorators: vec![],
|
||||||
|
quote_annotations: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||||
|
---
|
||||||
|
quote.py:64:28: TCH004 [*] Quote references to `pandas.DataFrame`. Import is in a type-checking block.
|
||||||
|
|
|
||||||
|
63 | if TYPE_CHECKING:
|
||||||
|
64 | from pandas import DataFrame
|
||||||
|
| ^^^^^^^^^ TCH004
|
||||||
|
65 |
|
||||||
|
66 | def func(value: DataFrame):
|
||||||
|
|
|
||||||
|
= help: Quote references
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
63 63 | if TYPE_CHECKING:
|
||||||
|
64 64 | from pandas import DataFrame
|
||||||
|
65 65 |
|
||||||
|
66 |- def func(value: DataFrame):
|
||||||
|
66 |+ def func(value: "DataFrame"):
|
||||||
|
67 67 | ...
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||||
|
---
|
||||||
|
quote.py:2:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
||||||
|
|
|
||||||
|
1 | def f():
|
||||||
|
2 | from pandas import DataFrame
|
||||||
|
| ^^^^^^^^^ TCH002
|
||||||
|
3 |
|
||||||
|
4 | def baz() -> DataFrame:
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |-def f():
|
||||||
|
1 |+from typing import TYPE_CHECKING
|
||||||
|
2 |+
|
||||||
|
3 |+if TYPE_CHECKING:
|
||||||
|
2 4 | from pandas import DataFrame
|
||||||
|
5 |+def f():
|
||||||
|
3 6 |
|
||||||
|
4 |- def baz() -> DataFrame:
|
||||||
|
7 |+ def baz() -> "DataFrame":
|
||||||
|
5 8 | ...
|
||||||
|
6 9 |
|
||||||
|
7 10 |
|
||||||
|
|
||||||
|
quote.py:9:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
||||||
|
|
|
||||||
|
8 | def f():
|
||||||
|
9 | from pandas import DataFrame
|
||||||
|
| ^^^^^^^^^ TCH002
|
||||||
|
10 |
|
||||||
|
11 | def baz() -> DataFrame[int]:
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |+from typing import TYPE_CHECKING
|
||||||
|
2 |+
|
||||||
|
3 |+if TYPE_CHECKING:
|
||||||
|
4 |+ from pandas import DataFrame
|
||||||
|
1 5 | def f():
|
||||||
|
2 6 | from pandas import DataFrame
|
||||||
|
3 7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
6 10 |
|
||||||
|
7 11 |
|
||||||
|
8 12 | def f():
|
||||||
|
9 |- from pandas import DataFrame
|
||||||
|
10 13 |
|
||||||
|
11 |- def baz() -> DataFrame[int]:
|
||||||
|
14 |+ def baz() -> "DataFrame[int]":
|
||||||
|
12 15 | ...
|
||||||
|
13 16 |
|
||||||
|
14 17 |
|
||||||
|
|
||||||
|
quote.py:16:24: TCH002 Move third-party import `pandas.DataFrame` into a type-checking block
|
||||||
|
|
|
||||||
|
15 | def f():
|
||||||
|
16 | from pandas import DataFrame
|
||||||
|
| ^^^^^^^^^ TCH002
|
||||||
|
17 |
|
||||||
|
18 | def baz() -> DataFrame["int"]:
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
quote.py:23:22: TCH002 [*] Move third-party import `pandas` into a type-checking block
|
||||||
|
|
|
||||||
|
22 | def f():
|
||||||
|
23 | import pandas as pd
|
||||||
|
| ^^ TCH002
|
||||||
|
24 |
|
||||||
|
25 | def baz() -> pd.DataFrame:
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |+from typing import TYPE_CHECKING
|
||||||
|
2 |+
|
||||||
|
3 |+if TYPE_CHECKING:
|
||||||
|
4 |+ import pandas as pd
|
||||||
|
1 5 | def f():
|
||||||
|
2 6 | from pandas import DataFrame
|
||||||
|
3 7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
20 24 |
|
||||||
|
21 25 |
|
||||||
|
22 26 | def f():
|
||||||
|
23 |- import pandas as pd
|
||||||
|
24 27 |
|
||||||
|
25 |- def baz() -> pd.DataFrame:
|
||||||
|
28 |+ def baz() -> "pd.DataFrame":
|
||||||
|
26 29 | ...
|
||||||
|
27 30 |
|
||||||
|
28 31 |
|
||||||
|
|
||||||
|
quote.py:30:22: TCH002 [*] Move third-party import `pandas` into a type-checking block
|
||||||
|
|
|
||||||
|
29 | def f():
|
||||||
|
30 | import pandas as pd
|
||||||
|
| ^^ TCH002
|
||||||
|
31 |
|
||||||
|
32 | def baz() -> pd.DataFrame.Extra:
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |+from typing import TYPE_CHECKING
|
||||||
|
2 |+
|
||||||
|
3 |+if TYPE_CHECKING:
|
||||||
|
4 |+ import pandas as pd
|
||||||
|
1 5 | def f():
|
||||||
|
2 6 | from pandas import DataFrame
|
||||||
|
3 7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
27 31 |
|
||||||
|
28 32 |
|
||||||
|
29 33 | def f():
|
||||||
|
30 |- import pandas as pd
|
||||||
|
31 34 |
|
||||||
|
32 |- def baz() -> pd.DataFrame.Extra:
|
||||||
|
35 |+ def baz() -> "pd.DataFrame.Extra":
|
||||||
|
33 36 | ...
|
||||||
|
34 37 |
|
||||||
|
35 38 |
|
||||||
|
|
||||||
|
quote.py:37:22: TCH002 [*] Move third-party import `pandas` into a type-checking block
|
||||||
|
|
|
||||||
|
36 | def f():
|
||||||
|
37 | import pandas as pd
|
||||||
|
| ^^ TCH002
|
||||||
|
38 |
|
||||||
|
39 | def baz() -> pd.DataFrame | int:
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |+from typing import TYPE_CHECKING
|
||||||
|
2 |+
|
||||||
|
3 |+if TYPE_CHECKING:
|
||||||
|
4 |+ import pandas as pd
|
||||||
|
1 5 | def f():
|
||||||
|
2 6 | from pandas import DataFrame
|
||||||
|
3 7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
34 38 |
|
||||||
|
35 39 |
|
||||||
|
36 40 | def f():
|
||||||
|
37 |- import pandas as pd
|
||||||
|
38 41 |
|
||||||
|
39 |- def baz() -> pd.DataFrame | int:
|
||||||
|
42 |+ def baz() -> "pd.DataFrame | int":
|
||||||
|
40 43 | ...
|
||||||
|
41 44 |
|
||||||
|
42 45 |
|
||||||
|
|
||||||
|
quote.py:45:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
||||||
|
|
|
||||||
|
44 | def f():
|
||||||
|
45 | from pandas import DataFrame
|
||||||
|
| ^^^^^^^^^ TCH002
|
||||||
|
46 |
|
||||||
|
47 | def baz() -> DataFrame():
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |+from typing import TYPE_CHECKING
|
||||||
|
2 |+
|
||||||
|
3 |+if TYPE_CHECKING:
|
||||||
|
4 |+ from pandas import DataFrame
|
||||||
|
1 5 | def f():
|
||||||
|
2 6 | from pandas import DataFrame
|
||||||
|
3 7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
42 46 |
|
||||||
|
43 47 |
|
||||||
|
44 48 | def f():
|
||||||
|
45 |- from pandas import DataFrame
|
||||||
|
46 49 |
|
||||||
|
47 |- def baz() -> DataFrame():
|
||||||
|
50 |+ def baz() -> "DataFrame()":
|
||||||
|
48 51 | ...
|
||||||
|
49 52 |
|
||||||
|
50 53 |
|
||||||
|
|
||||||
|
quote.py:54:24: TCH002 Move third-party import `pandas.DataFrame` into a type-checking block
|
||||||
|
|
|
||||||
|
52 | from typing import Literal
|
||||||
|
53 |
|
||||||
|
54 | from pandas import DataFrame
|
||||||
|
| ^^^^^^^^^ TCH002
|
||||||
|
55 |
|
||||||
|
56 | def baz() -> DataFrame[Literal["int"]]:
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||||
|
---
|
||||||
|
quote.py:64:28: TCH004 [*] Move import `pandas.DataFrame` out of type-checking block. Import is used for more than type hinting.
|
||||||
|
|
|
||||||
|
63 | if TYPE_CHECKING:
|
||||||
|
64 | from pandas import DataFrame
|
||||||
|
| ^^^^^^^^^ TCH004
|
||||||
|
65 |
|
||||||
|
66 | def func(value: DataFrame):
|
||||||
|
|
|
||||||
|
= help: Move out of type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |+from pandas import DataFrame
|
||||||
|
1 2 | def f():
|
||||||
|
2 3 | from pandas import DataFrame
|
||||||
|
3 4 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
61 62 | from typing import TYPE_CHECKING
|
||||||
|
62 63 |
|
||||||
|
63 64 | if TYPE_CHECKING:
|
||||||
|
64 |- from pandas import DataFrame
|
||||||
|
65 |+ pass
|
||||||
|
65 66 |
|
||||||
|
66 67 | def func(value: DataFrame):
|
||||||
|
67 68 | ...
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||||
|
---
|
||||||
|
|
||||||
|
|
@ -291,9 +291,12 @@ impl<'a> SemanticModel<'a> {
|
||||||
if let Some(binding_id) = self.scopes.global().get(name.id.as_str()) {
|
if let Some(binding_id) = self.scopes.global().get(name.id.as_str()) {
|
||||||
if !self.bindings[binding_id].is_unbound() {
|
if !self.bindings[binding_id].is_unbound() {
|
||||||
// Mark the binding as used.
|
// Mark the binding as used.
|
||||||
let reference_id =
|
let reference_id = self.resolved_references.push(
|
||||||
self.resolved_references
|
ScopeId::global(),
|
||||||
.push(ScopeId::global(), name.range, self.flags);
|
self.node_id,
|
||||||
|
name.range,
|
||||||
|
self.flags,
|
||||||
|
);
|
||||||
self.bindings[binding_id].references.push(reference_id);
|
self.bindings[binding_id].references.push(reference_id);
|
||||||
|
|
||||||
// Mark any submodule aliases as used.
|
// Mark any submodule aliases as used.
|
||||||
|
|
@ -302,6 +305,7 @@ impl<'a> SemanticModel<'a> {
|
||||||
{
|
{
|
||||||
let reference_id = self.resolved_references.push(
|
let reference_id = self.resolved_references.push(
|
||||||
ScopeId::global(),
|
ScopeId::global(),
|
||||||
|
self.node_id,
|
||||||
name.range,
|
name.range,
|
||||||
self.flags,
|
self.flags,
|
||||||
);
|
);
|
||||||
|
|
@ -356,18 +360,24 @@ impl<'a> SemanticModel<'a> {
|
||||||
|
|
||||||
if let Some(binding_id) = scope.get(name.id.as_str()) {
|
if let Some(binding_id) = scope.get(name.id.as_str()) {
|
||||||
// Mark the binding as used.
|
// Mark the binding as used.
|
||||||
let reference_id =
|
let reference_id = self.resolved_references.push(
|
||||||
self.resolved_references
|
self.scope_id,
|
||||||
.push(self.scope_id, name.range, self.flags);
|
self.node_id,
|
||||||
|
name.range,
|
||||||
|
self.flags,
|
||||||
|
);
|
||||||
self.bindings[binding_id].references.push(reference_id);
|
self.bindings[binding_id].references.push(reference_id);
|
||||||
|
|
||||||
// Mark any submodule aliases as used.
|
// Mark any submodule aliases as used.
|
||||||
if let Some(binding_id) =
|
if let Some(binding_id) =
|
||||||
self.resolve_submodule(name.id.as_str(), scope_id, binding_id)
|
self.resolve_submodule(name.id.as_str(), scope_id, binding_id)
|
||||||
{
|
{
|
||||||
let reference_id =
|
let reference_id = self.resolved_references.push(
|
||||||
self.resolved_references
|
self.scope_id,
|
||||||
.push(self.scope_id, name.range, self.flags);
|
self.node_id,
|
||||||
|
name.range,
|
||||||
|
self.flags,
|
||||||
|
);
|
||||||
self.bindings[binding_id].references.push(reference_id);
|
self.bindings[binding_id].references.push(reference_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,9 +441,12 @@ impl<'a> SemanticModel<'a> {
|
||||||
// The `x` in `print(x)` should resolve to the `x` in `x = 1`.
|
// The `x` in `print(x)` should resolve to the `x` in `x = 1`.
|
||||||
BindingKind::UnboundException(Some(binding_id)) => {
|
BindingKind::UnboundException(Some(binding_id)) => {
|
||||||
// Mark the binding as used.
|
// Mark the binding as used.
|
||||||
let reference_id =
|
let reference_id = self.resolved_references.push(
|
||||||
self.resolved_references
|
self.scope_id,
|
||||||
.push(self.scope_id, name.range, self.flags);
|
self.node_id,
|
||||||
|
name.range,
|
||||||
|
self.flags,
|
||||||
|
);
|
||||||
self.bindings[binding_id].references.push(reference_id);
|
self.bindings[binding_id].references.push(reference_id);
|
||||||
|
|
||||||
// Mark any submodule aliases as used.
|
// Mark any submodule aliases as used.
|
||||||
|
|
@ -442,6 +455,7 @@ impl<'a> SemanticModel<'a> {
|
||||||
{
|
{
|
||||||
let reference_id = self.resolved_references.push(
|
let reference_id = self.resolved_references.push(
|
||||||
self.scope_id,
|
self.scope_id,
|
||||||
|
self.node_id,
|
||||||
name.range,
|
name.range,
|
||||||
self.flags,
|
self.flags,
|
||||||
);
|
);
|
||||||
|
|
@ -979,6 +993,23 @@ impl<'a> SemanticModel<'a> {
|
||||||
&self.nodes[node_id]
|
&self.nodes[node_id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Given a [`Expr`], return its parent, if any.
|
||||||
|
#[inline]
|
||||||
|
pub fn parent_expression(&self, node_id: NodeId) -> Option<&'a Expr> {
|
||||||
|
self.nodes
|
||||||
|
.ancestor_ids(node_id)
|
||||||
|
.filter_map(|id| self.nodes[id].as_expression())
|
||||||
|
.nth(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a [`NodeId`], return the [`NodeId`] of the parent expression, if any.
|
||||||
|
pub fn parent_expression_id(&self, node_id: NodeId) -> Option<NodeId> {
|
||||||
|
self.nodes
|
||||||
|
.ancestor_ids(node_id)
|
||||||
|
.filter(|id| self.nodes[*id].is_expression())
|
||||||
|
.nth(1)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
|
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn statement(&self, node_id: NodeId) -> &'a Stmt {
|
pub fn statement(&self, node_id: NodeId) -> &'a Stmt {
|
||||||
|
|
@ -1007,11 +1038,10 @@ impl<'a> SemanticModel<'a> {
|
||||||
|
|
||||||
/// Return the [`Expr`] corresponding to the given [`NodeId`].
|
/// Return the [`Expr`] corresponding to the given [`NodeId`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn expression(&self, node_id: NodeId) -> &'a Expr {
|
pub fn expression(&self, node_id: NodeId) -> Option<&'a Expr> {
|
||||||
self.nodes
|
self.nodes
|
||||||
.ancestor_ids(node_id)
|
.ancestor_ids(node_id)
|
||||||
.find_map(|id| self.nodes[id].as_expression())
|
.find_map(|id| self.nodes[id].as_expression())
|
||||||
.expect("No expression found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an [`Iterator`] over the expressions, starting from the given [`NodeId`].
|
/// Returns an [`Iterator`] over the expressions, starting from the given [`NodeId`].
|
||||||
|
|
@ -1186,17 +1216,17 @@ impl<'a> SemanticModel<'a> {
|
||||||
|
|
||||||
/// Add a reference to the given [`BindingId`] in the local scope.
|
/// Add a reference to the given [`BindingId`] in the local scope.
|
||||||
pub fn add_local_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
pub fn add_local_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
||||||
let reference_id = self
|
let reference_id =
|
||||||
.resolved_references
|
self.resolved_references
|
||||||
.push(self.scope_id, range, self.flags);
|
.push(self.scope_id, self.node_id, range, self.flags);
|
||||||
self.bindings[binding_id].references.push(reference_id);
|
self.bindings[binding_id].references.push(reference_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a reference to the given [`BindingId`] in the global scope.
|
/// Add a reference to the given [`BindingId`] in the global scope.
|
||||||
pub fn add_global_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
pub fn add_global_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
||||||
let reference_id = self
|
let reference_id =
|
||||||
.resolved_references
|
self.resolved_references
|
||||||
.push(ScopeId::global(), range, self.flags);
|
.push(ScopeId::global(), self.node_id, range, self.flags);
|
||||||
self.bindings[binding_id].references.push(reference_id);
|
self.bindings[binding_id].references.push(reference_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1299,10 +1329,16 @@ impl<'a> SemanticModel<'a> {
|
||||||
.intersects(SemanticModelFlags::TYPING_ONLY_ANNOTATION)
|
.intersects(SemanticModelFlags::TYPING_ONLY_ANNOTATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the model is in a runtime-required type annotation.
|
/// Return `true` if the context is in a runtime-evaluated type annotation.
|
||||||
pub const fn in_runtime_annotation(&self) -> bool {
|
pub const fn in_runtime_evaluated_annotation(&self) -> bool {
|
||||||
self.flags
|
self.flags
|
||||||
.intersects(SemanticModelFlags::RUNTIME_ANNOTATION)
|
.intersects(SemanticModelFlags::RUNTIME_EVALUATED_ANNOTATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the context is in a runtime-required type annotation.
|
||||||
|
pub const fn in_runtime_required_annotation(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::RUNTIME_REQUIRED_ANNOTATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the model is in a type definition.
|
/// Return `true` if the model is in a type definition.
|
||||||
|
|
@ -1474,8 +1510,9 @@ impl ShadowedBinding {
|
||||||
bitflags! {
|
bitflags! {
|
||||||
/// Flags indicating the current model state.
|
/// Flags indicating the current model state.
|
||||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
||||||
pub struct SemanticModelFlags: u16 {
|
pub struct SemanticModelFlags: u32 {
|
||||||
/// The model is in a typing-time-only type annotation.
|
/// The model is in a type annotation that will only be evaluated when running a type
|
||||||
|
/// checker.
|
||||||
///
|
///
|
||||||
/// For example, the model could be visiting `int` in:
|
/// For example, the model could be visiting `int` in:
|
||||||
/// ```python
|
/// ```python
|
||||||
|
|
@ -1490,7 +1527,7 @@ bitflags! {
|
||||||
/// are any annotated assignments in module or class scopes.
|
/// are any annotated assignments in module or class scopes.
|
||||||
const TYPING_ONLY_ANNOTATION = 1 << 0;
|
const TYPING_ONLY_ANNOTATION = 1 << 0;
|
||||||
|
|
||||||
/// The model is in a runtime type annotation.
|
/// The model is in a type annotation that will be evaluated at runtime.
|
||||||
///
|
///
|
||||||
/// For example, the model could be visiting `int` in:
|
/// For example, the model could be visiting `int` in:
|
||||||
/// ```python
|
/// ```python
|
||||||
|
|
@ -1504,7 +1541,27 @@ bitflags! {
|
||||||
/// If `from __future__ import annotations` is used, all annotations are evaluated at
|
/// If `from __future__ import annotations` is used, all annotations are evaluated at
|
||||||
/// typing time. Otherwise, all function argument annotations are evaluated at runtime, as
|
/// typing time. Otherwise, all function argument annotations are evaluated at runtime, as
|
||||||
/// are any annotated assignments in module or class scopes.
|
/// are any annotated assignments in module or class scopes.
|
||||||
const RUNTIME_ANNOTATION = 1 << 1;
|
const RUNTIME_EVALUATED_ANNOTATION = 1 << 1;
|
||||||
|
|
||||||
|
/// The model is in a type annotation that is _required_ to be available at runtime.
|
||||||
|
///
|
||||||
|
/// For example, the context could be visiting `int` in:
|
||||||
|
/// ```python
|
||||||
|
/// from pydantic import BaseModel
|
||||||
|
///
|
||||||
|
/// class Foo(BaseModel):
|
||||||
|
/// x: int
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In this case, Pydantic requires that the type annotation be available at runtime
|
||||||
|
/// in order to perform runtime type-checking.
|
||||||
|
///
|
||||||
|
/// Unlike [`RUNTIME_EVALUATED_ANNOTATION`], annotations that are marked as
|
||||||
|
/// [`RUNTIME_REQUIRED_ANNOTATION`] cannot be deferred to typing time via conversion to a
|
||||||
|
/// forward reference (e.g., by wrapping the type in quotes), as the annotations are not
|
||||||
|
/// only required by the Python interpreter, but by runtime type checkers too.
|
||||||
|
const RUNTIME_REQUIRED_ANNOTATION = 1 << 2;
|
||||||
|
|
||||||
|
|
||||||
/// The model is in a type definition.
|
/// The model is in a type definition.
|
||||||
///
|
///
|
||||||
|
|
@ -1518,7 +1575,7 @@ bitflags! {
|
||||||
/// All type annotations are also type definitions, but the converse is not true.
|
/// All type annotations are also type definitions, but the converse is not true.
|
||||||
/// In our example, `int` is a type definition but not a type annotation, as it
|
/// In our example, `int` is a type definition but not a type annotation, as it
|
||||||
/// doesn't appear in a type annotation context, but rather in a type definition.
|
/// doesn't appear in a type annotation context, but rather in a type definition.
|
||||||
const TYPE_DEFINITION = 1 << 2;
|
const TYPE_DEFINITION = 1 << 3;
|
||||||
|
|
||||||
/// The model is in a (deferred) "simple" string type definition.
|
/// The model is in a (deferred) "simple" string type definition.
|
||||||
///
|
///
|
||||||
|
|
@ -1529,7 +1586,7 @@ bitflags! {
|
||||||
///
|
///
|
||||||
/// "Simple" string type definitions are those that consist of a single string literal,
|
/// "Simple" string type definitions are those that consist of a single string literal,
|
||||||
/// as opposed to an implicitly concatenated string literal.
|
/// as opposed to an implicitly concatenated string literal.
|
||||||
const SIMPLE_STRING_TYPE_DEFINITION = 1 << 3;
|
const SIMPLE_STRING_TYPE_DEFINITION = 1 << 4;
|
||||||
|
|
||||||
/// The model is in a (deferred) "complex" string type definition.
|
/// The model is in a (deferred) "complex" string type definition.
|
||||||
///
|
///
|
||||||
|
|
@ -1540,7 +1597,7 @@ bitflags! {
|
||||||
///
|
///
|
||||||
/// "Complex" string type definitions are those that consist of a implicitly concatenated
|
/// "Complex" string type definitions are those that consist of a implicitly concatenated
|
||||||
/// string literals. These are uncommon but valid.
|
/// string literals. These are uncommon but valid.
|
||||||
const COMPLEX_STRING_TYPE_DEFINITION = 1 << 4;
|
const COMPLEX_STRING_TYPE_DEFINITION = 1 << 5;
|
||||||
|
|
||||||
/// The model is in a (deferred) `__future__` type definition.
|
/// The model is in a (deferred) `__future__` type definition.
|
||||||
///
|
///
|
||||||
|
|
@ -1553,7 +1610,7 @@ bitflags! {
|
||||||
///
|
///
|
||||||
/// `__future__`-style type annotations are only enabled if the `annotations` feature
|
/// `__future__`-style type annotations are only enabled if the `annotations` feature
|
||||||
/// is enabled via `from __future__ import annotations`.
|
/// is enabled via `from __future__ import annotations`.
|
||||||
const FUTURE_TYPE_DEFINITION = 1 << 5;
|
const FUTURE_TYPE_DEFINITION = 1 << 6;
|
||||||
|
|
||||||
/// The model is in an exception handler.
|
/// The model is in an exception handler.
|
||||||
///
|
///
|
||||||
|
|
@ -1564,7 +1621,7 @@ bitflags! {
|
||||||
/// except Exception:
|
/// except Exception:
|
||||||
/// x: int = 1
|
/// x: int = 1
|
||||||
/// ```
|
/// ```
|
||||||
const EXCEPTION_HANDLER = 1 << 6;
|
const EXCEPTION_HANDLER = 1 << 7;
|
||||||
|
|
||||||
/// The model is in an f-string.
|
/// The model is in an f-string.
|
||||||
///
|
///
|
||||||
|
|
@ -1572,7 +1629,7 @@ bitflags! {
|
||||||
/// ```python
|
/// ```python
|
||||||
/// f'{x}'
|
/// f'{x}'
|
||||||
/// ```
|
/// ```
|
||||||
const F_STRING = 1 << 7;
|
const F_STRING = 1 << 8;
|
||||||
|
|
||||||
/// The model is in a boolean test.
|
/// The model is in a boolean test.
|
||||||
///
|
///
|
||||||
|
|
@ -1584,7 +1641,7 @@ bitflags! {
|
||||||
///
|
///
|
||||||
/// The implication is that the actual value returned by the current expression is
|
/// The implication is that the actual value returned by the current expression is
|
||||||
/// not used, only its truthiness.
|
/// not used, only its truthiness.
|
||||||
const BOOLEAN_TEST = 1 << 8;
|
const BOOLEAN_TEST = 1 << 9;
|
||||||
|
|
||||||
/// The model is in a `typing::Literal` annotation.
|
/// The model is in a `typing::Literal` annotation.
|
||||||
///
|
///
|
||||||
|
|
@ -1593,7 +1650,7 @@ bitflags! {
|
||||||
/// def f(x: Literal["A", "B", "C"]):
|
/// def f(x: Literal["A", "B", "C"]):
|
||||||
/// ...
|
/// ...
|
||||||
/// ```
|
/// ```
|
||||||
const TYPING_LITERAL = 1 << 9;
|
const TYPING_LITERAL = 1 << 10;
|
||||||
|
|
||||||
/// The model is in a subscript expression.
|
/// The model is in a subscript expression.
|
||||||
///
|
///
|
||||||
|
|
@ -1601,7 +1658,7 @@ bitflags! {
|
||||||
/// ```python
|
/// ```python
|
||||||
/// x["a"]["b"]
|
/// x["a"]["b"]
|
||||||
/// ```
|
/// ```
|
||||||
const SUBSCRIPT = 1 << 10;
|
const SUBSCRIPT = 1 << 11;
|
||||||
|
|
||||||
/// The model is in a type-checking block.
|
/// The model is in a type-checking block.
|
||||||
///
|
///
|
||||||
|
|
@ -1613,7 +1670,7 @@ bitflags! {
|
||||||
/// if TYPE_CHECKING:
|
/// if TYPE_CHECKING:
|
||||||
/// x: int = 1
|
/// x: int = 1
|
||||||
/// ```
|
/// ```
|
||||||
const TYPE_CHECKING_BLOCK = 1 << 11;
|
const TYPE_CHECKING_BLOCK = 1 << 12;
|
||||||
|
|
||||||
/// The model has traversed past the "top-of-file" import boundary.
|
/// The model has traversed past the "top-of-file" import boundary.
|
||||||
///
|
///
|
||||||
|
|
@ -1626,7 +1683,7 @@ bitflags! {
|
||||||
///
|
///
|
||||||
/// x: int = 1
|
/// x: int = 1
|
||||||
/// ```
|
/// ```
|
||||||
const IMPORT_BOUNDARY = 1 << 12;
|
const IMPORT_BOUNDARY = 1 << 13;
|
||||||
|
|
||||||
/// The model has traversed past the `__future__` import boundary.
|
/// The model has traversed past the `__future__` import boundary.
|
||||||
///
|
///
|
||||||
|
|
@ -1641,7 +1698,7 @@ bitflags! {
|
||||||
///
|
///
|
||||||
/// Python considers it a syntax error to import from `__future__` after
|
/// Python considers it a syntax error to import from `__future__` after
|
||||||
/// any other non-`__future__`-importing statements.
|
/// any other non-`__future__`-importing statements.
|
||||||
const FUTURES_BOUNDARY = 1 << 13;
|
const FUTURES_BOUNDARY = 1 << 14;
|
||||||
|
|
||||||
/// `__future__`-style type annotations are enabled in this model.
|
/// `__future__`-style type annotations are enabled in this model.
|
||||||
///
|
///
|
||||||
|
|
@ -1653,7 +1710,7 @@ bitflags! {
|
||||||
/// def f(x: int) -> int:
|
/// def f(x: int) -> int:
|
||||||
/// ...
|
/// ...
|
||||||
/// ```
|
/// ```
|
||||||
const FUTURE_ANNOTATIONS = 1 << 14;
|
const FUTURE_ANNOTATIONS = 1 << 15;
|
||||||
|
|
||||||
/// The model is in a type parameter definition.
|
/// The model is in a type parameter definition.
|
||||||
///
|
///
|
||||||
|
|
@ -1663,10 +1720,11 @@ bitflags! {
|
||||||
///
|
///
|
||||||
/// Record = TypeVar("Record")
|
/// Record = TypeVar("Record")
|
||||||
///
|
///
|
||||||
const TYPE_PARAM_DEFINITION = 1 << 15;
|
const TYPE_PARAM_DEFINITION = 1 << 16;
|
||||||
|
|
||||||
/// The context is in any type annotation.
|
/// The context is in any type annotation.
|
||||||
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_ANNOTATION.bits();
|
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
|
||||||
|
|
||||||
|
|
||||||
/// The context is in any string type definition.
|
/// The context is in any string type definition.
|
||||||
const STRING_TYPE_DEFINITION = Self::SIMPLE_STRING_TYPE_DEFINITION.bits()
|
const STRING_TYPE_DEFINITION = Self::SIMPLE_STRING_TYPE_DEFINITION.bits()
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,14 @@ use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use crate::context::ExecutionContext;
|
use crate::context::ExecutionContext;
|
||||||
use crate::scope::ScopeId;
|
use crate::scope::ScopeId;
|
||||||
use crate::{Exceptions, SemanticModelFlags};
|
use crate::{Exceptions, NodeId, SemanticModelFlags};
|
||||||
|
|
||||||
/// A resolved read reference to a name in a program.
|
/// A resolved read reference to a name in a program.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ResolvedReference {
|
pub struct ResolvedReference {
|
||||||
|
/// The expression that the reference occurs in. `None` if the reference is a global
|
||||||
|
/// reference or a reference via an augmented assignment.
|
||||||
|
node_id: Option<NodeId>,
|
||||||
/// The scope in which the reference is defined.
|
/// The scope in which the reference is defined.
|
||||||
scope_id: ScopeId,
|
scope_id: ScopeId,
|
||||||
/// The range of the reference in the source code.
|
/// The range of the reference in the source code.
|
||||||
|
|
@ -22,6 +25,11 @@ pub struct ResolvedReference {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedReference {
|
impl ResolvedReference {
|
||||||
|
/// The expression that the reference occurs in.
|
||||||
|
pub const fn expression_id(&self) -> Option<NodeId> {
|
||||||
|
self.node_id
|
||||||
|
}
|
||||||
|
|
||||||
/// The scope in which the reference is defined.
|
/// The scope in which the reference is defined.
|
||||||
pub const fn scope_id(&self) -> ScopeId {
|
pub const fn scope_id(&self) -> ScopeId {
|
||||||
self.scope_id
|
self.scope_id
|
||||||
|
|
@ -35,6 +43,48 @@ impl ResolvedReference {
|
||||||
ExecutionContext::Runtime
|
ExecutionContext::Runtime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the context is in a typing-only type annotation.
|
||||||
|
pub const fn in_typing_only_annotation(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::TYPING_ONLY_ANNOTATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the context is in a runtime-required type annotation.
|
||||||
|
pub const fn in_runtime_evaluated_annotation(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::RUNTIME_EVALUATED_ANNOTATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the context is in a "simple" string type definition.
|
||||||
|
pub const fn in_simple_string_type_definition(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::SIMPLE_STRING_TYPE_DEFINITION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the context is in a "complex" string type definition.
|
||||||
|
pub const fn in_complex_string_type_definition(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::COMPLEX_STRING_TYPE_DEFINITION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the context is in a `__future__` type definition.
|
||||||
|
pub const fn in_future_type_definition(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::FUTURE_TYPE_DEFINITION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the context is in any kind of deferred type definition.
|
||||||
|
pub const fn in_deferred_type_definition(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::DEFERRED_TYPE_DEFINITION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the context is in a type-checking block.
|
||||||
|
pub const fn in_type_checking_block(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::TYPE_CHECKING_BLOCK)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ranged for ResolvedReference {
|
impl Ranged for ResolvedReference {
|
||||||
|
|
@ -57,10 +107,12 @@ impl ResolvedReferences {
|
||||||
pub(crate) fn push(
|
pub(crate) fn push(
|
||||||
&mut self,
|
&mut self,
|
||||||
scope_id: ScopeId,
|
scope_id: ScopeId,
|
||||||
|
node_id: Option<NodeId>,
|
||||||
range: TextRange,
|
range: TextRange,
|
||||||
flags: SemanticModelFlags,
|
flags: SemanticModelFlags,
|
||||||
) -> ResolvedReferenceId {
|
) -> ResolvedReferenceId {
|
||||||
self.0.push(ResolvedReference {
|
self.0.push(ResolvedReference {
|
||||||
|
node_id,
|
||||||
scope_id,
|
scope_id,
|
||||||
range,
|
range,
|
||||||
flags,
|
flags,
|
||||||
|
|
|
||||||
|
|
@ -1642,6 +1642,57 @@ pub struct Flake8TypeCheckingOptions {
|
||||||
"#
|
"#
|
||||||
)]
|
)]
|
||||||
pub runtime_evaluated_decorators: Option<Vec<String>>,
|
pub runtime_evaluated_decorators: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Whether to add quotes around type annotations, if doing so would allow
|
||||||
|
/// the corresponding import to be moved into a type-checking block.
|
||||||
|
///
|
||||||
|
/// For example, in the following, Python requires that `Sequence` be
|
||||||
|
/// available at runtime, despite the fact that it's only used in a type
|
||||||
|
/// annotation:
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// from collections.abc import Sequence
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// def func(value: Sequence[int]) -> None:
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In other words, moving `from collections.abc import Sequence` into an
|
||||||
|
/// `if TYPE_CHECKING:` block above would cause a runtime error, as the
|
||||||
|
/// type would no longer be available at runtime.
|
||||||
|
///
|
||||||
|
/// By default, Ruff will respect such runtime semantics and avoid moving
|
||||||
|
/// the import to prevent such runtime errors.
|
||||||
|
///
|
||||||
|
/// Setting `quote-annotations` to `true` will instruct Ruff to add quotes
|
||||||
|
/// around the annotation (e.g., `"Sequence[int]"`), which in turn enables
|
||||||
|
/// Ruff to move the import into an `if TYPE_CHECKING:` block, like so:
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// from typing import TYPE_CHECKING
|
||||||
|
///
|
||||||
|
/// if TYPE_CHECKING:
|
||||||
|
/// from collections.abc import Sequence
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// def func(value: "Sequence[int]") -> None:
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note that this setting has no effect when `from __future__ import annotations`
|
||||||
|
/// is present, as `__future__` annotations are always treated equivalently
|
||||||
|
/// to quoted annotations.
|
||||||
|
#[option(
|
||||||
|
default = "false",
|
||||||
|
value_type = "bool",
|
||||||
|
example = r#"
|
||||||
|
# Add quotes around type annotations, if doing so would allow
|
||||||
|
# an import to be moved into a type-checking block.
|
||||||
|
quote-annotations = true
|
||||||
|
"#
|
||||||
|
)]
|
||||||
|
pub quote_annotations: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flake8TypeCheckingOptions {
|
impl Flake8TypeCheckingOptions {
|
||||||
|
|
@ -1651,8 +1702,9 @@ impl Flake8TypeCheckingOptions {
|
||||||
exempt_modules: self
|
exempt_modules: self
|
||||||
.exempt_modules
|
.exempt_modules
|
||||||
.unwrap_or_else(|| vec!["typing".to_string()]),
|
.unwrap_or_else(|| vec!["typing".to_string()]),
|
||||||
runtime_evaluated_base_classes: self.runtime_evaluated_base_classes.unwrap_or_default(),
|
runtime_required_base_classes: self.runtime_evaluated_base_classes.unwrap_or_default(),
|
||||||
runtime_evaluated_decorators: self.runtime_evaluated_decorators.unwrap_or_default(),
|
runtime_required_decorators: self.runtime_evaluated_decorators.unwrap_or_default(),
|
||||||
|
quote_annotations: self.quote_annotations.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1194,6 +1194,13 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"quote-annotations": {
|
||||||
|
"description": "Whether to add quotes around type annotations, if doing so would allow the corresponding import to be moved into a type-checking block.\n\nFor example, in the following, Python requires that `Sequence` be available at runtime, despite the fact that it's only used in a type annotation:\n\n```python from collections.abc import Sequence\n\ndef func(value: Sequence[int]) -> None: ... ```\n\nIn other words, moving `from collections.abc import Sequence` into an `if TYPE_CHECKING:` block above would cause a runtime error, as the type would no longer be available at runtime.\n\nBy default, Ruff will respect such runtime semantics and avoid moving the import to prevent such runtime errors.\n\nSetting `quote-annotations` to `true` will instruct Ruff to add quotes around the annotation (e.g., `\"Sequence[int]\"`), which in turn enables Ruff to move the import into an `if TYPE_CHECKING:` block, like so:\n\n```python from typing import TYPE_CHECKING\n\nif TYPE_CHECKING: from collections.abc import Sequence\n\ndef func(value: \"Sequence[int]\") -> None: ... ```\n\nNote that this setting has no effect when `from __future__ import annotations` is present, as `__future__` annotations are always treated equivalently to quoted annotations.",
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"runtime-evaluated-base-classes": {
|
"runtime-evaluated-base-classes": {
|
||||||
"description": "Exempt classes that list any of the enumerated classes as a base class from needing to be moved into type-checking blocks.\n\nCommon examples include Pydantic's `pydantic.BaseModel` and SQLAlchemy's `sqlalchemy.orm.DeclarativeBase`, but can also support user-defined classes that inherit from those base classes. For example, if you define a common `DeclarativeBase` subclass that's used throughout your project (e.g., `class Base(DeclarativeBase) ...` in `base.py`), you can add it to this list (`runtime-evaluated-base-classes = [\"base.Base\"]`) to exempt models from being moved into type-checking blocks.",
|
"description": "Exempt classes that list any of the enumerated classes as a base class from needing to be moved into type-checking blocks.\n\nCommon examples include Pydantic's `pydantic.BaseModel` and SQLAlchemy's `sqlalchemy.orm.DeclarativeBase`, but can also support user-defined classes that inherit from those base classes. For example, if you define a common `DeclarativeBase` subclass that's used throughout your project (e.g., `class Base(DeclarativeBase) ...` in `base.py`), you can add it to this list (`runtime-evaluated-base-classes = [\"base.Base\"]`) to exempt models from being moved into type-checking blocks.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue