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:
Charlie Marsh 2023-12-29 22:14:53 -04:00 committed by GitHub
parent 97e9d3c54f
commit 94727996e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 31 deletions

View file

@ -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):
...

View file

@ -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

View file

@ -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| {

View file

@ -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()
}, },

View file

@ -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)

View file

@ -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
View file

@ -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"