Skip to content

Commit

Permalink
[flake8-type-checking] Add exemption for runtime evaluated decorato…
Browse files Browse the repository at this point in the history
…r classes
  • Loading branch information
viccie30 committed Dec 19, 2024
1 parent 2802cbd commit 4413259
Show file tree
Hide file tree
Showing 14 changed files with 351 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ linter.flake8_type_checking.exempt_modules = [
]
linter.flake8_type_checking.runtime_required_base_classes = []
linter.flake8_type_checking.runtime_required_decorators = []
linter.flake8_type_checking.runtime_required_decorator_classes = []
linter.flake8_type_checking.quote_annotations = false
linter.flake8_unused_arguments.ignore_variadic_names = false
linter.isort.required_imports = []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import fastapi
from fastapi import FastAPI as Api

from example import DecoratingClass

if TYPE_CHECKING:
import datetime # TC004
from array import array # TC004

import pathlib # TC004

import pyproj

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

decorating_instance = DecoratingClass()

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

@app2.get("/array")
def get_array() -> array:
pass

@decorating_instance.decorator
def foo(path: pathlib.Path) -> None:
pass

@decorating_instance
def bar(arg: pyproj.Transformer) -> None:
pass

@DecoratingClass
def baz(arg: pyproj.Transformer) -> None:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

import pandas
import pyproj

import fastapi
from fastapi import FastAPI as Api
from example import DecoratingClass

import numpy # TC002

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

decorating_instance = DecoratingClass()


@app1.get("/transformer")
def get_transformer() -> pyproj.Transformer:
pass

@app2.put("/dataframe")
def set_dataframe(df: pandas.DataFrame):
pass

@decorating_instance
def foo(x: pandas.DataFrame):
pass

@DecoratingClass
def bar(x: numpy.ndarray):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

import pathlib

import fastapi
from fastapi import FastAPI as Api
from example import DecoratingClass

from uuid import UUID # TC003

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

decorating_instance = DecoratingClass()


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

@app2.put("/pure_path")
def set_pure_path(df: pathlib.PurePath):
pass

@decorating_instance
def foo(x: pathlib.PosixPath):
pass

@DecoratingClass
def bar(x: UUID):
pass
6 changes: 6 additions & 0 deletions crates/ruff_linter/src/checkers/ast/annotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ impl AnnotationContext {
if flake8_type_checking::helpers::runtime_required_function(
function_def,
&settings.flake8_type_checking.runtime_required_decorators,
&settings
.flake8_type_checking
.runtime_required_decorator_classes,
semantic,
) =>
{
Expand Down Expand Up @@ -85,6 +88,9 @@ impl AnnotationContext {
if flake8_type_checking::helpers::runtime_required_function(
function_def,
&settings.flake8_type_checking.runtime_required_decorators,
&settings
.flake8_type_checking
.runtime_required_decorator_classes,
semantic,
) {
Self::RuntimeRequired
Expand Down
35 changes: 35 additions & 0 deletions crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use ruff_python_ast::visitor::transformer::{walk_expr, Transformer};
use ruff_python_ast::{self as ast, Decorator, Expr};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_parser::typing::parse_type_annotation;
use ruff_python_semantic::analyze::typing::resolve_assignment;
use ruff_python_semantic::{
analyze, Binding, BindingKind, Modules, NodeId, ResolvedReference, ScopeKind, SemanticModel,
};
Expand Down Expand Up @@ -51,11 +52,16 @@ pub(crate) fn is_valid_runtime_import(
pub(crate) fn runtime_required_function(
function_def: &ast::StmtFunctionDef,
decorators: &[String],
decorator_classes: &[String],
semantic: &SemanticModel,
) -> bool {
if runtime_required_decorators(&function_def.decorator_list, decorators, semantic) {
return true;
}
if runtime_required_decorator_classes(&function_def.decorator_list, decorator_classes, semantic)
{
return true;
}
false
}

Expand Down Expand Up @@ -108,6 +114,35 @@ fn runtime_required_decorators(
})
}

fn runtime_required_decorator_classes(
decorator_list: &[Decorator],
decorator_classes: &[String],
semantic: &SemanticModel,
) -> bool {
if decorator_classes.is_empty() {
return false;
}

decorator_list.iter().any(|decorator| {
let expression = if let ast::Expr::Call(ast::ExprCall { func, .. }) = &decorator.expression
{
func
} else {
&decorator.expression
};

let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = expression else {
return false;
};

resolve_assignment(value, semantic).is_some_and(|qualified_name| {
decorator_classes.iter().any(|decorator_class| {
QualifiedName::from_dotted_name(decorator_class) == qualified_name
})
})
})
}

/// Returns `true` if an annotation will be inspected at runtime by the `dataclasses` module.
///
/// Specifically, detects whether an annotation is to either `dataclasses.InitVar` or
Expand Down
31 changes: 31 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 @@ -237,6 +237,37 @@ mod tests {
Ok(())
}

#[test_case(
Rule::RuntimeImportInTypeCheckingBlock,
Path::new("runtime_evaluated_decorator_classes_1.py")
)]
#[test_case(
Rule::TypingOnlyThirdPartyImport,
Path::new("runtime_evaluated_decorator_classes_2.py")
)]
#[test_case(
Rule::TypingOnlyStandardLibraryImport,
Path::new("runtime_evaluated_decorator_classes_3.py")
)]
fn runtime_evaluated_decorator_classes(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_decorator_classes: vec![
"fastapi.FastAPI".to_string(),
"example.DecoratingClass".to_string(),
],
..Default::default()
},
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}

#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("module/direct.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("module/import.py"))]
#[test_case(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ use crate::rules::isort::{categorize, ImportSection, ImportType};
/// corresponding import to be moved into an `if TYPE_CHECKING:` block.
///
/// If a class _requires_ that type annotations be available at runtime (as is
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
/// the [`lint.flake8-type-checking.runtime-evaluated-base-classes`] and
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
/// as such.
/// the case for Pydantic, SQLAlchemy, FastAPI, and other libraries), consider
/// using the [`lint.flake8-type-checking.runtime-evaluated-base-classes`],
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`], and
/// [`lint.flake8-type-checking.runtime-evaluated-decorator-classes`] settings
/// to mark them as such.
///
/// ## Example
/// ```python
Expand Down Expand Up @@ -108,10 +109,11 @@ impl Violation for TypingOnlyFirstPartyImport {
/// corresponding import to be moved into an `if TYPE_CHECKING:` block.
///
/// If a class _requires_ that type annotations be available at runtime (as is
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
/// the [`lint.flake8-type-checking.runtime-evaluated-base-classes`] and
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
/// as such.
/// the case for Pydantic, SQLAlchemy, FastAPI, and other libraries), consider
/// using the [`lint.flake8-type-checking.runtime-evaluated-base-classes`],
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`], and
/// [`lint.flake8-type-checking.runtime-evaluated-decorator-classes`] settings
/// to mark them as such.
///
/// ## Example
/// ```python
Expand Down Expand Up @@ -146,7 +148,8 @@ impl Violation for TypingOnlyFirstPartyImport {
/// - `lint.typing-modules`
///
/// ## References
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
/// - [PEP 563: Runtime annotation resolution and
/// `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
#[derive(ViolationMetadata)]
pub(crate) struct TypingOnlyThirdPartyImport {
qualified_name: String,
Expand Down Expand Up @@ -183,10 +186,11 @@ impl Violation for TypingOnlyThirdPartyImport {
/// corresponding import to be moved into an `if TYPE_CHECKING:` block.
///
/// If a class _requires_ that type annotations be available at runtime (as is
/// the case for Pydantic, SQLAlchemy, and other libraries), consider using
/// the [`lint.flake8-type-checking.runtime-evaluated-base-classes`] and
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
/// as such.
/// the case for Pydantic, SQLAlchemy, FastAPI, and other libraries), consider
/// using the [`lint.flake8-type-checking.runtime-evaluated-base-classes`],
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`], and
/// [`lint.flake8-type-checking.runtime-evaluated-decorator-classes`] settings
/// to mark them as such.
///
/// ## Example
/// ```python
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/rules/flake8_type_checking/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct Settings {
pub exempt_modules: Vec<String>,
pub runtime_required_base_classes: Vec<String>,
pub runtime_required_decorators: Vec<String>,
pub runtime_required_decorator_classes: Vec<String>,
pub quote_annotations: bool,
}

Expand All @@ -20,6 +21,7 @@ impl Default for Settings {
exempt_modules: vec!["typing".to_string(), "typing_extensions".to_string()],
runtime_required_base_classes: vec![],
runtime_required_decorators: vec![],
runtime_required_decorator_classes: vec![],
quote_annotations: false,
}
}
Expand All @@ -35,6 +37,7 @@ impl Display for Settings {
self.exempt_modules | array,
self.runtime_required_base_classes | array,
self.runtime_required_decorators | array,
self.runtime_required_decorator_classes | array,
self.quote_annotations
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
snapshot_kind: text
---
runtime_evaluated_decorator_classes_1.py:11:12: TC004 [*] Move import `datetime` out of type-checking block. Import is used for more than type hinting.
|
10 | if TYPE_CHECKING:
11 | import datetime # TC004
| ^^^^^^^^ TC004
12 | from array import array # TC004
|
= help: Move out of type-checking block

Unsafe fix
6 6 | from fastapi import FastAPI as Api
7 7 |
8 8 | from example import DecoratingClass
9 |+import datetime
9 10 |
10 11 | if TYPE_CHECKING:
11 |- import datetime # TC004
12 12 | from array import array # TC004
13 13 |
14 14 | import pathlib # TC004

runtime_evaluated_decorator_classes_1.py:12:23: TC004 [*] Move import `array.array` out of type-checking block. Import is used for more than type hinting.
|
10 | if TYPE_CHECKING:
11 | import datetime # TC004
12 | from array import array # TC004
| ^^^^^ TC004
13 |
14 | import pathlib # TC004
|
= help: Move out of type-checking block

Unsafe fix
6 6 | from fastapi import FastAPI as Api
7 7 |
8 8 | from example import DecoratingClass
9 |+from array import array
9 10 |
10 11 | if TYPE_CHECKING:
11 12 | import datetime # TC004
12 |- from array import array # TC004
13 13 |
14 14 | import pathlib # TC004
15 15 |

runtime_evaluated_decorator_classes_1.py:14:12: TC004 [*] Move import `pathlib` out of type-checking block. Import is used for more than type hinting.
|
12 | from array import array # TC004
13 |
14 | import pathlib # TC004
| ^^^^^^^ TC004
15 |
16 | import pyproj
|
= help: Move out of type-checking block

Unsafe fix
6 6 | from fastapi import FastAPI as Api
7 7 |
8 8 | from example import DecoratingClass
9 |+import pathlib
9 10 |
10 11 | if TYPE_CHECKING:
11 12 | import datetime # TC004
12 13 | from array import array # TC004
13 14 |
14 |- import pathlib # TC004
15 15 |
16 16 | import pyproj
17 17 |
Loading

0 comments on commit 4413259

Please sign in to comment.