From e05953a9917a0f5e5a4271d41252741d8ab7c5fd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Aug 2024 14:34:56 -0400 Subject: [PATCH] Avoid treating `dataclasses.KW_ONLY` as typing-only (#12863) ## Summary Closes https://github.com/astral-sh/ruff/issues/12859. --- .../fixtures/flake8_type_checking/kw_only.py | 18 +++++++++++++ .../src/rules/flake8_type_checking/helpers.rs | 8 +++--- .../src/rules/flake8_type_checking/mod.rs | 2 ++ ...ly-standard-library-import_kw_only.py.snap | 25 +++++++++++++++++++ ...ly-standard-library-import_kw_only.py.snap | 4 +++ 5 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/kw_only.py create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_kw_only.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_kw_only.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/kw_only.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/kw_only.py new file mode 100644 index 0000000000000..682f430ed89cd --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/kw_only.py @@ -0,0 +1,18 @@ +"""Test: avoid marking a `KW_ONLY` annotation as typing-only.""" + +from __future__ import annotations + +from dataclasses import KW_ONLY, dataclass, Field + + +@dataclass +class Test1: + a: int + _: KW_ONLY + b: str + + +@dataclass +class Test2: + a: int + b: Field 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 fb85d48c226de..09973b7ff3637 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs @@ -125,12 +125,14 @@ pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &Semanti matches!(qualified_name.segments(), ["dataclasses", "dataclass"]) }) }) { - // Determine whether the annotation is `typing.ClassVar` or `dataclasses.InitVar`. + // Determine whether the annotation is `typing.ClassVar`, `dataclasses.InitVar`, or `dataclasses.KW_ONLY`. return semantic .resolve_qualified_name(map_subscript(annotation)) .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["dataclasses", "InitVar"]) - || semantic.match_typing_qualified_name(&qualified_name, "ClassVar") + matches!( + qualified_name.segments(), + ["dataclasses", "InitVar" | "KW_ONLY"] + ) || semantic.match_typing_qualified_name(&qualified_name, "ClassVar") }); } } 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 ed513d3236c6c..30fc9c5aa7d8b 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -40,6 +40,7 @@ mod tests { #[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))] #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))] #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))] + #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("kw_only.py"))] #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))] #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))] #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote.py"))] @@ -77,6 +78,7 @@ mod tests { #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))] #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))] + #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("kw_only.py"))] fn strict(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("strict_{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_kw_only.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_kw_only.py.snap new file mode 100644 index 0000000000000..478af5eba46b2 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_kw_only.py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +kw_only.py:5:45: TCH003 [*] Move standard library import `dataclasses.Field` into a type-checking block + | +3 | from __future__ import annotations +4 | +5 | from dataclasses import KW_ONLY, dataclass, Field + | ^^^^^ TCH003 + | + = help: Move into type-checking block + +ℹ Unsafe fix +2 2 | +3 3 | from __future__ import annotations +4 4 | +5 |-from dataclasses import KW_ONLY, dataclass, Field + 5 |+from dataclasses import KW_ONLY, dataclass + 6 |+from typing import TYPE_CHECKING + 7 |+ + 8 |+if TYPE_CHECKING: + 9 |+ from dataclasses import Field +6 10 | +7 11 | +8 12 | @dataclass diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_kw_only.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_kw_only.py.snap new file mode 100644 index 0000000000000..6c5ead27428ce --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_kw_only.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +