mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:10:09 +00:00
[flake8-type-checking
] Improve flexibility of runtime-evaluated-decorators
(#15204)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
7ca3f9515c
commit
1ef0f615f1
12 changed files with 192 additions and 10 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2969,6 +2969,7 @@ dependencies = [
|
||||||
"rustc-hash 2.1.0",
|
"rustc-hash 2.1.0",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
25
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/module/app.py
vendored
Normal file
25
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/module/app.py
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import fastapi
|
||||||
|
from fastapi import FastAPI as Api
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import datetime # TC004
|
||||||
|
from array import array # TC004
|
||||||
|
|
||||||
|
app = fastapi.FastAPI("First application")
|
||||||
|
|
||||||
|
class AppContainer:
|
||||||
|
app = Api("Second application")
|
||||||
|
|
||||||
|
app_container = AppContainer()
|
||||||
|
|
||||||
|
@app.put("/datetime")
|
||||||
|
def set_datetime(value: datetime.datetime):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app_container.app.get("/array")
|
||||||
|
def get_array() -> array:
|
||||||
|
pass
|
14
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/module/routes.py
vendored
Normal file
14
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/module/routes.py
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib # OK
|
||||||
|
from datetime import date # OK
|
||||||
|
|
||||||
|
from module.app import app, app_container
|
||||||
|
|
||||||
|
@app.get("/path")
|
||||||
|
def get_path() -> pathlib.Path:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app_container.app.put("/date")
|
||||||
|
def set_date(d: date):
|
||||||
|
pass
|
|
@ -98,12 +98,31 @@ fn runtime_required_decorators(
|
||||||
}
|
}
|
||||||
|
|
||||||
decorator_list.iter().any(|decorator| {
|
decorator_list.iter().any(|decorator| {
|
||||||
|
let expression = map_callable(&decorator.expression);
|
||||||
semantic
|
semantic
|
||||||
.resolve_qualified_name(map_callable(&decorator.expression))
|
// First try to resolve the qualified name normally for cases like:
|
||||||
|
// ```python
|
||||||
|
// from mymodule import app
|
||||||
|
//
|
||||||
|
// @app.get(...)
|
||||||
|
// def test(): ...
|
||||||
|
// ```
|
||||||
|
.resolve_qualified_name(expression)
|
||||||
|
// If we can't resolve the name, then try resolving the assignment
|
||||||
|
// in order to support cases like:
|
||||||
|
// ```python
|
||||||
|
// from fastapi import FastAPI
|
||||||
|
//
|
||||||
|
// app = FastAPI()
|
||||||
|
//
|
||||||
|
// @app.get(...)
|
||||||
|
// def test(): ...
|
||||||
|
// ```
|
||||||
|
.or_else(|| analyze::typing::resolve_assignment(expression, semantic))
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|qualified_name| {
|
||||||
decorators
|
decorators
|
||||||
.iter()
|
.iter()
|
||||||
.any(|base_class| QualifiedName::from_dotted_name(base_class) == qualified_name)
|
.any(|decorator| QualifiedName::from_dotted_name(decorator) == qualified_name)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,6 +259,33 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("module/app.py"))]
|
||||||
|
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("module/routes.py"))]
|
||||||
|
fn decorator_same_file(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
|
let snapshot = format!("{}_{}", 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 {
|
||||||
|
runtime_required_decorators: vec![
|
||||||
|
"fastapi.FastAPI.get".to_string(),
|
||||||
|
"fastapi.FastAPI.put".to_string(),
|
||||||
|
"module.app.AppContainer.app.get".to_string(),
|
||||||
|
"module.app.AppContainer.app.put".to_string(),
|
||||||
|
"module.app.app.get".to_string(),
|
||||||
|
"module.app.app.put".to_string(),
|
||||||
|
"module.app.app_container.app.get".to_string(),
|
||||||
|
"module.app.app_container.app.put".to_string(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..settings::LinterSettings::for_rule(rule_code)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
assert_messages!(snapshot, diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test_case(
|
#[test_case(
|
||||||
r"
|
r"
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||||
|
---
|
||||||
|
app.py:9:12: TC004 [*] Move import `datetime` out of type-checking block. Import is used for more than type hinting.
|
||||||
|
|
|
||||||
|
8 | if TYPE_CHECKING:
|
||||||
|
9 | import datetime # TC004
|
||||||
|
| ^^^^^^^^ TC004
|
||||||
|
10 | from array import array # TC004
|
||||||
|
|
|
||||||
|
= help: Move out of type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
4 4 |
|
||||||
|
5 5 | import fastapi
|
||||||
|
6 6 | from fastapi import FastAPI as Api
|
||||||
|
7 |+import datetime
|
||||||
|
7 8 |
|
||||||
|
8 9 | if TYPE_CHECKING:
|
||||||
|
9 |- import datetime # TC004
|
||||||
|
10 10 | from array import array # TC004
|
||||||
|
11 11 |
|
||||||
|
12 12 | app = fastapi.FastAPI("First application")
|
||||||
|
|
||||||
|
app.py:10:23: TC004 [*] Move import `array.array` out of type-checking block. Import is used for more than type hinting.
|
||||||
|
|
|
||||||
|
8 | if TYPE_CHECKING:
|
||||||
|
9 | import datetime # TC004
|
||||||
|
10 | from array import array # TC004
|
||||||
|
| ^^^^^ TC004
|
||||||
|
11 |
|
||||||
|
12 | app = fastapi.FastAPI("First application")
|
||||||
|
|
|
||||||
|
= help: Move out of type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
4 4 |
|
||||||
|
5 5 | import fastapi
|
||||||
|
6 6 | from fastapi import FastAPI as Api
|
||||||
|
7 |+from array import array
|
||||||
|
7 8 |
|
||||||
|
8 9 | if TYPE_CHECKING:
|
||||||
|
9 10 | import datetime # TC004
|
||||||
|
10 |- from array import array # TC004
|
||||||
|
11 11 |
|
||||||
|
12 12 | app = fastapi.FastAPI("First application")
|
||||||
|
13 13 |
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||||
|
---
|
||||||
|
|
|
@ -277,6 +277,14 @@ impl<'a> QualifiedName<'a> {
|
||||||
inner.push(member);
|
inner.push(member);
|
||||||
Self(inner)
|
Self(inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extends the qualified name using the given members.
|
||||||
|
#[must_use]
|
||||||
|
pub fn extend_members<T: IntoIterator<Item = &'a str>>(self, members: T) -> Self {
|
||||||
|
let mut inner = self.0;
|
||||||
|
inner.extend(members);
|
||||||
|
Self(inner)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for QualifiedName<'_> {
|
impl Display for QualifiedName<'_> {
|
||||||
|
|
|
@ -24,6 +24,7 @@ is-macro = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
|
smallvec = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ruff_python_parser = { workspace = true }
|
ruff_python_parser = { workspace = true }
|
||||||
|
|
|
@ -11,6 +11,7 @@ use ruff_python_stdlib::typing::{
|
||||||
is_typed_dict, is_typed_dict_member,
|
is_typed_dict, is_typed_dict_member,
|
||||||
};
|
};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
use crate::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
|
use crate::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
|
||||||
use crate::model::SemanticModel;
|
use crate::model::SemanticModel;
|
||||||
|
@ -983,23 +984,43 @@ fn find_parameter<'a>(
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// This function will return `["asyncio", "get_running_loop"]` for the `loop` binding.
|
/// This function will return `["asyncio", "get_running_loop"]` for the `loop` binding.
|
||||||
|
///
|
||||||
|
/// This function will also automatically expand attribute accesses, so given:
|
||||||
|
/// ```python
|
||||||
|
/// from module import AppContainer
|
||||||
|
///
|
||||||
|
/// container = AppContainer()
|
||||||
|
/// container.app.get(...)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This function will return `["module", "AppContainer", "app", "get"]` for the
|
||||||
|
/// attribute access `container.app.get`.
|
||||||
pub fn resolve_assignment<'a>(
|
pub fn resolve_assignment<'a>(
|
||||||
expr: &'a Expr,
|
expr: &'a Expr,
|
||||||
semantic: &'a SemanticModel<'a>,
|
semantic: &'a SemanticModel<'a>,
|
||||||
) -> Option<QualifiedName<'a>> {
|
) -> Option<QualifiedName<'a>> {
|
||||||
let name = expr.as_name_expr()?;
|
// Resolve any attribute chain.
|
||||||
|
let mut head_expr = expr;
|
||||||
|
let mut reversed_tail: SmallVec<[_; 4]> = smallvec![];
|
||||||
|
while let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = head_expr {
|
||||||
|
head_expr = value;
|
||||||
|
reversed_tail.push(attr.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the left-most name, e.g. `foo` in `foo.bar.baz` to a qualified name,
|
||||||
|
// then append the attributes.
|
||||||
|
let name = head_expr.as_name_expr()?;
|
||||||
let binding_id = semantic.resolve_name(name)?;
|
let binding_id = semantic.resolve_name(name)?;
|
||||||
let statement = semantic.binding(binding_id).statement(semantic)?;
|
let statement = semantic.binding(binding_id).statement(semantic)?;
|
||||||
match statement {
|
match statement {
|
||||||
Stmt::Assign(ast::StmtAssign { value, .. }) => {
|
Stmt::Assign(ast::StmtAssign { value, .. })
|
||||||
let ast::ExprCall { func, .. } = value.as_call_expr()?;
|
| Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||||
semantic.resolve_qualified_name(func)
|
|
||||||
}
|
|
||||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
|
||||||
value: Some(value), ..
|
value: Some(value), ..
|
||||||
}) => {
|
}) => {
|
||||||
let ast::ExprCall { func, .. } = value.as_call_expr()?;
|
let ast::ExprCall { func, .. } = value.as_call_expr()?;
|
||||||
semantic.resolve_qualified_name(func)
|
|
||||||
|
let qualified_name = semantic.resolve_qualified_name(func)?;
|
||||||
|
Some(qualified_name.extend_members(reversed_tail.into_iter().rev()))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1819,6 +1819,21 @@ pub struct Flake8TypeCheckingOptions {
|
||||||
///
|
///
|
||||||
/// Common examples include Pydantic's `@pydantic.validate_call` decorator
|
/// Common examples include Pydantic's `@pydantic.validate_call` decorator
|
||||||
/// (for functions) and attrs' `@attrs.define` decorator (for classes).
|
/// (for functions) and attrs' `@attrs.define` decorator (for classes).
|
||||||
|
///
|
||||||
|
/// This also supports framework decorators like FastAPI's `fastapi.FastAPI.get`
|
||||||
|
/// which will work across assignments in the same module.
|
||||||
|
///
|
||||||
|
/// For example:
|
||||||
|
/// ```python
|
||||||
|
/// import fastapi
|
||||||
|
///
|
||||||
|
/// app = FastAPI("app")
|
||||||
|
///
|
||||||
|
/// @app.get("/home")
|
||||||
|
/// def home() -> str: ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Here `app.get` will correctly be identified as `fastapi.FastAPI.get`.
|
||||||
#[option(
|
#[option(
|
||||||
default = "[]",
|
default = "[]",
|
||||||
value_type = "list[str]",
|
value_type = "list[str]",
|
||||||
|
|
2
ruff.schema.json
generated
2
ruff.schema.json
generated
|
@ -1387,7 +1387,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtime-evaluated-decorators": {
|
"runtime-evaluated-decorators": {
|
||||||
"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).",
|
"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).\n\nThis also supports framework decorators like FastAPI's `fastapi.FastAPI.get` which will work across assignments in the same module.\n\nFor example: ```python import fastapi\n\napp = FastAPI(\"app\")\n\n@app.get(\"/home\") def home() -> str: ... ```\n\nHere `app.get` will correctly be identified as `fastapi.FastAPI.get`.",
|
||||||
"type": [
|
"type": [
|
||||||
"array",
|
"array",
|
||||||
"null"
|
"null"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue