Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flake8-type-checking] Improve flexibility of runtime-evaluated-decorators #15204

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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

app1 = fastapi.FastAPI("First application")
app2 = Api("Second application")

@app1.put("/datetime")
def set_datetime(value: datetime.datetime):
pass

@app2.get("/array")
def get_array() -> array:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations

import pathlib # OK

from module.app import app1, app2

@app1.get("/path")
def get_path() -> pathlib.Path:
pass

@app2.put("/pure_path")
def set_pure_path(df: pathlib.PurePath):
pass
24 changes: 22 additions & 2 deletions crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,32 @@ fn runtime_required_decorators(
}

decorator_list.iter().any(|decorator| {
let expression = map_callable(&decorator.expression);
semantic
.resolve_qualified_name(map_callable(&decorator.expression))
.resolve_qualified_name(expression)
// if we can't resolve the name, then try resolving the assignment
.or_else(|| {
let mut source = expression;
let mut tail = vec![];
while let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = source {
MichaReiser marked this conversation as resolved.
Show resolved Hide resolved
source = value;
tail.push(attr.as_str());
}
analyze::typing::resolve_assignment(source, semantic).map(|head| {
let mut qualified_name = head;
for member in tail {
// extend the full name with the attributes we accessed
// i.e. for `app.get` when `app` resolves to `fastapi.FastApi`
// then we want to get back `fastapi.FastApi.get`.
qualified_name = qualified_name.append_member(member);
}
qualified_name
})
})
.is_some_and(|qualified_name| {
decorators
.iter()
.any(|base_class| QualifiedName::from_dotted_name(base_class) == qualified_name)
.any(|decorator| QualifiedName::from_dotted_name(decorator) == qualified_name)
})
})
}
Expand Down
25 changes: 25 additions & 0 deletions crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,31 @@ mod tests {
Ok(())
}

#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("module/app.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("module/routes.py"))]
fn decorator_same_file_and_glob(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.app1.get".to_string(),
"module.app.app1.put".to_string(),
"module.app.app2.get".to_string(),
"module.app.app2.put".to_string(),
],
..Default::default()
},
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}

#[test_case(
r"
from __future__ import annotations
Expand Down
Original file line number Diff line number Diff line change
@@ -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 | app1 = 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 | app1 = 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 | app1 = fastapi.FastAPI("First application")
13 13 | app2 = Api("Second application")
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---

Loading