diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/runtime_evaluated_decorators_3.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/runtime_evaluated_decorators_3.py index 799e7fe4b5f747..fd433772f851a6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/runtime_evaluated_decorators_3.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/runtime_evaluated_decorators_3.py @@ -4,6 +4,8 @@ from array import array from dataclasses import dataclass from uuid import UUID # TCH003 +from collections.abc import Sequence +from pydantic import validate_call import attrs from attrs import frozen @@ -22,3 +24,8 @@ class B: @dataclass class C: x: UUID + + +@validate_call(config={'arbitrary_types_allowed': True}) +def test(user: Sequence): + ... diff --git a/crates/ruff_linter/src/checkers/ast/annotation.rs b/crates/ruff_linter/src/checkers/ast/annotation.rs index aca5fc6c629ff7..753fd3f30e93f1 100644 --- a/crates/ruff_linter/src/checkers/ast/annotation.rs +++ b/crates/ruff_linter/src/checkers/ast/annotation.rs @@ -28,21 +28,29 @@ pub(super) enum AnnotationContext { 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 field) or a function scope, and that class or function is marked as + // runtime-required, treat the annotation as runtime-required. + match semantic.current_scope().kind { + ScopeKind::Class(class_def) + if 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; + ) => + { + 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 diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs index 47ce35214b0eb6..5cee3fcd5807a0 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs @@ -3,7 +3,7 @@ use anyhow::Result; use ruff_diagnostics::Edit; use ruff_python_ast::call_path::from_qualified_name; 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_semantic::{ 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( class_def: &ast::StmtClassDef, base_classes: &[String], @@ -52,7 +65,7 @@ pub(crate) fn runtime_required_class( if runtime_required_base_class(class_def, base_classes, semantic) { return true; } - if runtime_required_decorators(class_def, decorators, semantic) { + if runtime_required_decorators(&class_def.decorator_list, decorators, semantic) { return true; } false @@ -72,7 +85,7 @@ fn runtime_required_base_class( } fn runtime_required_decorators( - class_def: &ast::StmtClassDef, + decorator_list: &[Decorator], decorators: &[String], semantic: &SemanticModel, ) -> bool { @@ -80,7 +93,7 @@ fn runtime_required_decorators( return false; } - class_def.decorator_list.iter().any(|decorator| { + decorator_list.iter().any(|decorator| { semantic .resolve_call_path(map_callable(&decorator.expression)) .is_some_and(|call_path| { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index 2390486e4fc81e..700f27973f594d 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -195,6 +195,7 @@ mod tests { runtime_required_decorators: vec![ "attrs.define".to_string(), "attrs.frozen".to_string(), + "pydantic.validate_call".to_string(), ], ..Default::default() }, diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_decorators_3.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_decorators_3.py.snap index aba3fd362e73a4..fc68794213d2e2 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_decorators_3.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_decorators_3.py.snap @@ -7,8 +7,8 @@ runtime_evaluated_decorators_3.py:6:18: TCH003 [*] Move standard library import 5 | from dataclasses import dataclass 6 | from uuid import UUID # TCH003 | ^^^^ TCH003 -7 | -8 | import attrs +7 | from collections.abc import Sequence +8 | from pydantic import validate_call | = 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 5 5 | from dataclasses import dataclass 6 |-from uuid import UUID # TCH003 -7 6 | -8 7 | import attrs -9 8 | from attrs import frozen - 9 |+from typing import TYPE_CHECKING - 10 |+ - 11 |+if TYPE_CHECKING: - 12 |+ from uuid import UUID -10 13 | -11 14 | -12 15 | @attrs.define(auto_attribs=True) +7 6 | 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 uuid import UUID +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) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 169b8c3da5060a..6aaa21113b4b55 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -1632,13 +1632,16 @@ pub struct Flake8TypeCheckingOptions { )] pub runtime_evaluated_base_classes: Option>, - /// Exempt classes decorated with any of the enumerated decorators from - /// needing to be moved into type-checking blocks. + /// Exempt classes and functions decorated with any of the enumerated + /// 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( default = "[]", value_type = "list[str]", example = r#" - runtime-evaluated-decorators = ["attrs.define", "attrs.frozen"] + runtime-evaluated-decorators = ["pydantic.validate_call", "attrs.define"] "# )] pub runtime_evaluated_decorators: Option>,