mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 15:15:33 +00:00
Respect runtime-required decorators on functions (#9317)
## Summary This PR modifies the semantics of `runtime-evaluated-decorators` to respect decorators on both classes _and_ functions. Historically, this only respected classes, since the common use-case is (e.g.) `pydantic.BaseModel` -- but functions are equally valid. Closes https://github.com/astral-sh/ruff/issues/9312. ## Test Plan `cargo test`
This commit is contained in:
parent
97e9d3c54f
commit
94727996e8
7 changed files with 92 additions and 31 deletions
|
@ -4,6 +4,8 @@ import datetime
|
||||||
from array import array
|
from array import array
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from uuid import UUID # TCH003
|
from uuid import UUID # TCH003
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from pydantic import validate_call
|
||||||
|
|
||||||
import attrs
|
import attrs
|
||||||
from attrs import frozen
|
from attrs import frozen
|
||||||
|
@ -22,3 +24,8 @@ class B:
|
||||||
@dataclass
|
@dataclass
|
||||||
class C:
|
class C:
|
||||||
x: UUID
|
x: UUID
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call(config={'arbitrary_types_allowed': True})
|
||||||
|
def test(user: Sequence):
|
||||||
|
...
|
||||||
|
|
|
@ -28,21 +28,29 @@ pub(super) enum AnnotationContext {
|
||||||
impl AnnotationContext {
|
impl AnnotationContext {
|
||||||
pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self {
|
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
|
// 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.
|
// class field) or a function scope, and that class or function is marked as
|
||||||
if semantic
|
// runtime-required, treat the annotation as runtime-required.
|
||||||
.current_scope()
|
match semantic.current_scope().kind {
|
||||||
.kind
|
ScopeKind::Class(class_def)
|
||||||
.as_class()
|
if flake8_type_checking::helpers::runtime_required_class(
|
||||||
.is_some_and(|class_def| {
|
|
||||||
flake8_type_checking::helpers::runtime_required_class(
|
|
||||||
class_def,
|
class_def,
|
||||||
&settings.flake8_type_checking.runtime_required_base_classes,
|
&settings.flake8_type_checking.runtime_required_base_classes,
|
||||||
&settings.flake8_type_checking.runtime_required_decorators,
|
&settings.flake8_type_checking.runtime_required_decorators,
|
||||||
semantic,
|
semantic,
|
||||||
)
|
) =>
|
||||||
})
|
{
|
||||||
{
|
return Self::RuntimeRequired
|
||||||
return Self::RuntimeRequired;
|
}
|
||||||
|
ScopeKind::Function(function_def)
|
||||||
|
if flake8_type_checking::helpers::runtime_required_function(
|
||||||
|
function_def,
|
||||||
|
&settings.flake8_type_checking.runtime_required_decorators,
|
||||||
|
semantic,
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
return Self::RuntimeRequired
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If `__future__` annotations are enabled, then annotations are never evaluated
|
// If `__future__` annotations are enabled, then annotations are never evaluated
|
||||||
|
|
|
@ -3,7 +3,7 @@ use anyhow::Result;
|
||||||
use ruff_diagnostics::Edit;
|
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;
|
use ruff_python_ast::helpers::map_callable;
|
||||||
use ruff_python_ast::{self as ast, Expr};
|
use ruff_python_ast::{self as ast, Decorator, Expr};
|
||||||
use ruff_python_codegen::{Generator, Stylist};
|
use ruff_python_codegen::{Generator, Stylist};
|
||||||
use ruff_python_semantic::{
|
use ruff_python_semantic::{
|
||||||
analyze, Binding, BindingKind, NodeId, ResolvedReference, SemanticModel,
|
analyze, Binding, BindingKind, NodeId, ResolvedReference, SemanticModel,
|
||||||
|
@ -43,6 +43,19 @@ pub(crate) fn is_valid_runtime_import(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if a function's parameters should be treated as runtime-required.
|
||||||
|
pub(crate) fn runtime_required_function(
|
||||||
|
function_def: &ast::StmtFunctionDef,
|
||||||
|
decorators: &[String],
|
||||||
|
semantic: &SemanticModel,
|
||||||
|
) -> bool {
|
||||||
|
if runtime_required_decorators(&function_def.decorator_list, decorators, semantic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if a class's assignments should be treated as runtime-required.
|
||||||
pub(crate) fn runtime_required_class(
|
pub(crate) fn runtime_required_class(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
base_classes: &[String],
|
base_classes: &[String],
|
||||||
|
@ -52,7 +65,7 @@ pub(crate) fn runtime_required_class(
|
||||||
if runtime_required_base_class(class_def, base_classes, semantic) {
|
if runtime_required_base_class(class_def, base_classes, semantic) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if runtime_required_decorators(class_def, decorators, semantic) {
|
if runtime_required_decorators(&class_def.decorator_list, decorators, semantic) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
@ -72,7 +85,7 @@ fn runtime_required_base_class(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runtime_required_decorators(
|
fn runtime_required_decorators(
|
||||||
class_def: &ast::StmtClassDef,
|
decorator_list: &[Decorator],
|
||||||
decorators: &[String],
|
decorators: &[String],
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
@ -80,7 +93,7 @@ fn runtime_required_decorators(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class_def.decorator_list.iter().any(|decorator| {
|
decorator_list.iter().any(|decorator| {
|
||||||
semantic
|
semantic
|
||||||
.resolve_call_path(map_callable(&decorator.expression))
|
.resolve_call_path(map_callable(&decorator.expression))
|
||||||
.is_some_and(|call_path| {
|
.is_some_and(|call_path| {
|
||||||
|
|
|
@ -195,6 +195,7 @@ mod tests {
|
||||||
runtime_required_decorators: vec![
|
runtime_required_decorators: vec![
|
||||||
"attrs.define".to_string(),
|
"attrs.define".to_string(),
|
||||||
"attrs.frozen".to_string(),
|
"attrs.frozen".to_string(),
|
||||||
|
"pydantic.validate_call".to_string(),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,8 +7,8 @@ runtime_evaluated_decorators_3.py:6:18: TCH003 [*] Move standard library import
|
||||||
5 | from dataclasses import dataclass
|
5 | from dataclasses import dataclass
|
||||||
6 | from uuid import UUID # TCH003
|
6 | from uuid import UUID # TCH003
|
||||||
| ^^^^ TCH003
|
| ^^^^ TCH003
|
||||||
7 |
|
7 | from collections.abc import Sequence
|
||||||
8 | import attrs
|
8 | from pydantic import validate_call
|
||||||
|
|
|
|
||||||
= help: Move into type-checking block
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
@ -17,15 +17,44 @@ runtime_evaluated_decorators_3.py:6:18: TCH003 [*] Move standard library import
|
||||||
4 4 | from array import array
|
4 4 | from array import array
|
||||||
5 5 | from dataclasses import dataclass
|
5 5 | from dataclasses import dataclass
|
||||||
6 |-from uuid import UUID # TCH003
|
6 |-from uuid import UUID # TCH003
|
||||||
7 6 |
|
7 6 | from collections.abc import Sequence
|
||||||
8 7 | import attrs
|
8 7 | from pydantic import validate_call
|
||||||
9 8 | from attrs import frozen
|
9 8 |
|
||||||
9 |+from typing import TYPE_CHECKING
|
10 9 | import attrs
|
||||||
10 |+
|
11 10 | from attrs import frozen
|
||||||
11 |+if TYPE_CHECKING:
|
11 |+from typing import TYPE_CHECKING
|
||||||
12 |+ from uuid import UUID
|
12 |+
|
||||||
10 13 |
|
13 |+if TYPE_CHECKING:
|
||||||
11 14 |
|
14 |+ from uuid import UUID
|
||||||
12 15 | @attrs.define(auto_attribs=True)
|
12 15 |
|
||||||
|
13 16 |
|
||||||
|
14 17 | @attrs.define(auto_attribs=True)
|
||||||
|
|
||||||
|
runtime_evaluated_decorators_3.py:7:29: TCH003 [*] Move standard library import `collections.abc.Sequence` into a type-checking block
|
||||||
|
|
|
||||||
|
5 | from dataclasses import dataclass
|
||||||
|
6 | from uuid import UUID # TCH003
|
||||||
|
7 | from collections.abc import Sequence
|
||||||
|
| ^^^^^^^^ TCH003
|
||||||
|
8 | from pydantic import validate_call
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
4 4 | from array import array
|
||||||
|
5 5 | from dataclasses import dataclass
|
||||||
|
6 6 | from uuid import UUID # TCH003
|
||||||
|
7 |-from collections.abc import Sequence
|
||||||
|
8 7 | from pydantic import validate_call
|
||||||
|
9 8 |
|
||||||
|
10 9 | import attrs
|
||||||
|
11 10 | from attrs import frozen
|
||||||
|
11 |+from typing import TYPE_CHECKING
|
||||||
|
12 |+
|
||||||
|
13 |+if TYPE_CHECKING:
|
||||||
|
14 |+ from collections.abc import Sequence
|
||||||
|
12 15 |
|
||||||
|
13 16 |
|
||||||
|
14 17 | @attrs.define(auto_attribs=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1632,13 +1632,16 @@ pub struct Flake8TypeCheckingOptions {
|
||||||
)]
|
)]
|
||||||
pub runtime_evaluated_base_classes: Option<Vec<String>>,
|
pub runtime_evaluated_base_classes: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Exempt classes decorated with any of the enumerated decorators from
|
/// Exempt classes and functions decorated with any of the enumerated
|
||||||
/// needing to be moved into type-checking blocks.
|
/// decorators from being moved into type-checking blocks.
|
||||||
|
///
|
||||||
|
/// Common examples include Pydantic's `@pydantic.validate_call` decorator
|
||||||
|
/// (for functions) and attrs' `@attrs.define` decorator (for classes).
|
||||||
#[option(
|
#[option(
|
||||||
default = "[]",
|
default = "[]",
|
||||||
value_type = "list[str]",
|
value_type = "list[str]",
|
||||||
example = r#"
|
example = r#"
|
||||||
runtime-evaluated-decorators = ["attrs.define", "attrs.frozen"]
|
runtime-evaluated-decorators = ["pydantic.validate_call", "attrs.define"]
|
||||||
"#
|
"#
|
||||||
)]
|
)]
|
||||||
pub runtime_evaluated_decorators: Option<Vec<String>>,
|
pub runtime_evaluated_decorators: Option<Vec<String>>,
|
||||||
|
|
2
ruff.schema.json
generated
2
ruff.schema.json
generated
|
@ -1222,7 +1222,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtime-evaluated-decorators": {
|
"runtime-evaluated-decorators": {
|
||||||
"description": "Exempt classes decorated with any of the enumerated decorators from needing to be moved into type-checking blocks.",
|
"description": "Exempt classes and functions decorated with any of the enumerated decorators from being moved into type-checking blocks.\n\nCommon examples include Pydantic's `@pydantic.validate_call` decorator (for functions) and attrs' `@attrs.define` decorator (for classes).",
|
||||||
"type": [
|
"type": [
|
||||||
"array",
|
"array",
|
||||||
"null"
|
"null"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue