From 2991c0793c13b5994b4bc080652542deb999dcc5 Mon Sep 17 00:00:00 2001 From: qdegraaf Date: Wed, 31 May 2023 15:51:12 +0200 Subject: [PATCH 1/6] Add PYI024 --- .../test/fixtures/flake8_pyi/PYI024.py | 1 + .../test/fixtures/flake8_pyi/PYI024.pyi | 1 + crates/ruff/src/checkers/ast/mod.rs | 5 ++ crates/ruff/src/codes.rs | 1 + crates/ruff/src/registry.rs | 1 + crates/ruff/src/rules/flake8_pyi/mod.rs | 2 + .../flake8_pyi/rules/incorrect_named_tuple.rs | 74 +++++++++++++++++++ crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 2 + ...__flake8_pyi__tests__PYI024_PYI024.py.snap | 4 + ..._flake8_pyi__tests__PYI024_PYI024.pyi.snap | 15 ++++ ruff.schema.json | 1 + 11 files changed, 107 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/incorrect_named_tuple.rs create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py new file mode 100644 index 0000000000000..fd9008fad4d4e --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py @@ -0,0 +1 @@ +from collections import namedtuple # Ok, not a stub file diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi new file mode 100644 index 0000000000000..cf28085413884 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi @@ -0,0 +1 @@ +from collections import namedtuple # PYI024 diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index e4e54c47bc087..cc0fdfe1255dc 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1064,6 +1064,11 @@ where pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names); } } + if self.is_stub { + if self.enabled(Rule::IncorrectNamedTuple) { + flake8_pyi::rules::incorrect_named_tuple(self, stmt); + } + } if self.enabled(Rule::BannedApi) { if let Some(module) = helpers::resolve_imported_module_path(level, module, self.module_path) diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 1e896a3653f3b..ff064a41534f8 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -591,6 +591,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "016") => (RuleGroup::Unspecified, Rule::DuplicateUnionMember), (Flake8Pyi, "020") => (RuleGroup::Unspecified, Rule::QuotedAnnotationInStub), (Flake8Pyi, "021") => (RuleGroup::Unspecified, Rule::DocstringInStub), + (Flake8Pyi, "024") => (RuleGroup::Unspecified, Rule::IncorrectNamedTuple), (Flake8Pyi, "032") => (RuleGroup::Unspecified, Rule::AnyEqNeAnnotation), (Flake8Pyi, "033") => (RuleGroup::Unspecified, Rule::TypeCommentInStub), (Flake8Pyi, "042") => (RuleGroup::Unspecified, Rule::SnakeCaseTypeAlias), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 0a642d2fc49ed..3c2c3dbaeec71 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -518,6 +518,7 @@ ruff_macros::register_rules!( rules::flake8_pyi::rules::IterMethodReturnIterable, rules::flake8_pyi::rules::DuplicateUnionMember, rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody, + rules::flake8_pyi::rules::IncorrectNamedTuple, rules::flake8_pyi::rules::NonEmptyStubBody, rules::flake8_pyi::rules::PassInClassBody, rules::flake8_pyi::rules::PassStatementStubBody, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 3ccf108df1023..7fcd1b6359aa6 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -26,6 +26,8 @@ mod tests { #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))] #[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.py"))] #[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.pyi"))] + #[test_case(Rule::IncorrectNamedTuple, Path::new("PYI024.py"))] + #[test_case(Rule::IncorrectNamedTuple, Path::new("PYI024.pyi"))] #[test_case(Rule::IterMethodReturnIterable, Path::new("PYI045.py"))] #[test_case(Rule::IterMethodReturnIterable, Path::new("PYI045.pyi"))] #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/incorrect_named_tuple.rs b/crates/ruff/src/rules/flake8_pyi/rules/incorrect_named_tuple.rs new file mode 100644 index 0000000000000..1951f254a0ac6 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/incorrect_named_tuple.rs @@ -0,0 +1,74 @@ +use rustpython_parser::ast; +use rustpython_parser::ast::Stmt; + +use crate::registry::AsRule; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::prelude::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for an import of `collections.namedtuple` +/// +/// ## Why is this bad? +/// The type generated by subclassing typing.NamedTuple is equivalent to a collections.namedtuple, +/// but with __annotations__, _field_types and _field_defaults attributes added. +/// +/// For a developer, using the typing module for your namedtuples allows a more natural +/// declarative interface and more precise type inference. +/// +/// ## Example +/// ```python +/// from collections import namedtuple +/// +/// person = namedtuple("Person", ["name", "age"] +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import NamedTuple +/// +/// class Person(NamedTuple): +/// name: str +/// age: int +/// ``` +/// ## References +/// - [StackOverflow](https://stackoverflow.com/a/50767206) +#[violation] +pub struct IncorrectNamedTuple; + +impl AlwaysAutofixableViolation for IncorrectNamedTuple { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use `typing.NamedTuple` instead of `collections.namedtuple`") + } + + fn autofix_title(&self) -> String { + format!("Replace `collections.namedtuple` with `typing.NamedTuple`") + } +} + +/// PYI024 +pub(crate) fn incorrect_named_tuple(checker: &mut Checker, stmt: &Stmt) { + if let Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) = stmt { + let Some(module) = module else { + return + }; + if module.as_str() != "collections" { + return; + } + for name in names { + if name.name.as_str() == "namedtuple" { + let mut diagnostic = Diagnostic::new(IncorrectNamedTuple, name.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::manual(Edit::range_replacement( + format!("from typing import NamedTuple"), + stmt.range(), + ))); + } + checker.diagnostics.push(diagnostic); + } + } + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 790f0b494bea4..89925cda9a254 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -10,6 +10,7 @@ pub(crate) use ellipsis_in_non_empty_class_body::{ pub(crate) use iter_method_return_iterable::{ iter_method_return_iterable, IterMethodReturnIterable, }; +pub(crate) use incorrect_named_tuple::{incorrect_named_tuple, IncorrectNamedTuple}; pub(crate) use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody}; pub(crate) use pass_in_class_body::{pass_in_class_body, PassInClassBody}; pub(crate) use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody}; @@ -37,6 +38,7 @@ mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; mod iter_method_return_iterable; +mod incorrect_named_tuple; mod non_empty_stub_body; mod pass_in_class_body; mod pass_statement_stub_body; diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.py.snap new file mode 100644 index 0000000000000..d1aa2e9116558 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap new file mode 100644 index 0000000000000..73a98b98fe765 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI024.pyi:1:25: PYI024 [*] Use `typing.NamedTuple` instead of `collections.namedtuple` + | +1 | from collections import namedtuple # PYI024 + | ^^^^^^^^^^ PYI024 + | + = help: Replace `collections.namedtuple` with `typing.NamedTuple` + +ℹ Possible fix +1 |-from collections import namedtuple # PYI024 + 1 |+from typing import NamedTuple # PYI024 + + diff --git a/ruff.schema.json b/ruff.schema.json index 31550f9dcb680..bd19906af2b99 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2209,6 +2209,7 @@ "PYI02", "PYI020", "PYI021", + "PYI024", "PYI03", "PYI032", "PYI033", From c7a09ca56c71ca7f105a4cc40908f7113ab9147f Mon Sep 17 00:00:00 2001 From: qdegraaf Date: Wed, 31 May 2023 16:12:59 +0200 Subject: [PATCH 2/6] cargo fmt --- crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 89925cda9a254..787cec142dd40 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -7,10 +7,10 @@ pub(crate) use duplicate_union_member::{duplicate_union_member, DuplicateUnionMe pub(crate) use ellipsis_in_non_empty_class_body::{ ellipsis_in_non_empty_class_body, EllipsisInNonEmptyClassBody, }; +pub(crate) use incorrect_named_tuple::{incorrect_named_tuple, IncorrectNamedTuple}; pub(crate) use iter_method_return_iterable::{ iter_method_return_iterable, IterMethodReturnIterable, }; -pub(crate) use incorrect_named_tuple::{incorrect_named_tuple, IncorrectNamedTuple}; pub(crate) use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody}; pub(crate) use pass_in_class_body::{pass_in_class_body, PassInClassBody}; pub(crate) use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody}; @@ -37,8 +37,8 @@ mod bad_version_info_comparison; mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; -mod iter_method_return_iterable; mod incorrect_named_tuple; +mod iter_method_return_iterable; mod non_empty_stub_body; mod pass_in_class_body; mod pass_statement_stub_body; From 29b6d2ec125386ae801fdeddfd7f6d193ee88678 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 31 May 2023 11:43:32 -0400 Subject: [PATCH 3/6] Run check on attribute --- .../test/fixtures/flake8_pyi/PYI024.py | 4 +- .../test/fixtures/flake8_pyi/PYI024.pyi | 4 +- crates/ruff/src/checkers/ast/mod.rs | 10 +-- crates/ruff/src/codes.rs | 2 +- crates/ruff/src/registry.rs | 2 +- crates/ruff/src/rules/flake8_pyi/mod.rs | 4 +- .../rules/collections_named_tuple.rs | 64 ++++++++++++++++ .../flake8_pyi/rules/incorrect_named_tuple.rs | 74 ------------------- crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 4 +- ..._flake8_pyi__tests__PYI024_PYI024.pyi.snap | 14 ++-- 10 files changed, 87 insertions(+), 95 deletions(-) create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs delete mode 100644 crates/ruff/src/rules/flake8_pyi/rules/incorrect_named_tuple.rs diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py index fd9008fad4d4e..c2cec3d658e17 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py @@ -1 +1,3 @@ -from collections import namedtuple # Ok, not a stub file +import collections + +j: collections.namedtuple # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi index cf28085413884..edf47ccff8f01 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi @@ -1 +1,3 @@ -from collections import namedtuple # PYI024 +import collections + +j: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index cc0fdfe1255dc..bc3d0a746c2f6 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1064,11 +1064,6 @@ where pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names); } } - if self.is_stub { - if self.enabled(Rule::IncorrectNamedTuple) { - flake8_pyi::rules::incorrect_named_tuple(self, stmt); - } - } if self.enabled(Rule::BannedApi) { if let Some(module) = helpers::resolve_imported_module_path(level, module, self.module_path) @@ -2429,6 +2424,11 @@ where if self.enabled(Rule::PrivateMemberAccess) { flake8_self::rules::private_member_access(self, expr); } + if self.is_stub { + if self.enabled(Rule::CollectionsNamedTuple) { + flake8_pyi::rules::collections_named_tuple(self, expr); + } + } pandas_vet::rules::attr(self, attr, value, expr); } Expr::Call(ast::ExprCall { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index ff064a41534f8..c5ee5769e6fc7 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -591,7 +591,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "016") => (RuleGroup::Unspecified, Rule::DuplicateUnionMember), (Flake8Pyi, "020") => (RuleGroup::Unspecified, Rule::QuotedAnnotationInStub), (Flake8Pyi, "021") => (RuleGroup::Unspecified, Rule::DocstringInStub), - (Flake8Pyi, "024") => (RuleGroup::Unspecified, Rule::IncorrectNamedTuple), + (Flake8Pyi, "024") => (RuleGroup::Unspecified, Rule::CollectionsNamedTuple), (Flake8Pyi, "032") => (RuleGroup::Unspecified, Rule::AnyEqNeAnnotation), (Flake8Pyi, "033") => (RuleGroup::Unspecified, Rule::TypeCommentInStub), (Flake8Pyi, "042") => (RuleGroup::Unspecified, Rule::SnakeCaseTypeAlias), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 3c2c3dbaeec71..197d0de0a12eb 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -518,7 +518,7 @@ ruff_macros::register_rules!( rules::flake8_pyi::rules::IterMethodReturnIterable, rules::flake8_pyi::rules::DuplicateUnionMember, rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody, - rules::flake8_pyi::rules::IncorrectNamedTuple, + rules::flake8_pyi::rules::CollectionsNamedTuple, rules::flake8_pyi::rules::NonEmptyStubBody, rules::flake8_pyi::rules::PassInClassBody, rules::flake8_pyi::rules::PassStatementStubBody, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 7fcd1b6359aa6..5d242bb333411 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -26,8 +26,8 @@ mod tests { #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))] #[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.py"))] #[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.pyi"))] - #[test_case(Rule::IncorrectNamedTuple, Path::new("PYI024.py"))] - #[test_case(Rule::IncorrectNamedTuple, Path::new("PYI024.pyi"))] + #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))] + #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))] #[test_case(Rule::IterMethodReturnIterable, Path::new("PYI045.py"))] #[test_case(Rule::IterMethodReturnIterable, Path::new("PYI045.pyi"))] #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs new file mode 100644 index 0000000000000..93f593e9a6c80 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -0,0 +1,64 @@ +use rustpython_parser::ast::Expr; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::prelude::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for uses of `collections.namedtuple` in stub files. +/// +/// ## Why is this bad? +/// `typing.NamedTuple` is the "typed version" of `collections.namedtuple`. +/// +/// The class generated by subclassing `typing.NamedTuple` is equivalent to +/// `collections.namedtuple`, with the exception that `typing.NamedTuple` +/// includes an `__annotations__` attribute, which allows type checkers to +/// infer the types of the fields. +/// +/// ## Example +/// ```python +/// from collections import namedtuple +/// +/// +/// person = namedtuple("Person", ["name", "age"] +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import NamedTuple +/// +/// +/// class Person(NamedTuple): +/// name: str +/// age: int +/// ``` +#[violation] +pub struct CollectionsNamedTuple; + +impl Violation for CollectionsNamedTuple { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use `typing.NamedTuple` instead of `collections.namedtuple`") + } + + fn autofix_title(&self) -> Option { + Some(format!("Replace with `typing.NamedTuple`")) + } +} + +/// PYI024 +pub(crate) fn collections_named_tuple(checker: &mut Checker, expr: &Expr) { + if checker + .semantic_model() + .resolve_call_path(expr) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["collections", "namedtuple"]) + }) + { + checker + .diagnostics + .push(Diagnostic::new(CollectionsNamedTuple, expr.range())); + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/incorrect_named_tuple.rs b/crates/ruff/src/rules/flake8_pyi/rules/incorrect_named_tuple.rs deleted file mode 100644 index 1951f254a0ac6..0000000000000 --- a/crates/ruff/src/rules/flake8_pyi/rules/incorrect_named_tuple.rs +++ /dev/null @@ -1,74 +0,0 @@ -use rustpython_parser::ast; -use rustpython_parser::ast::Stmt; - -use crate::registry::AsRule; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Ranged; - -use crate::checkers::ast::Checker; - -/// ## What it does -/// Checks for an import of `collections.namedtuple` -/// -/// ## Why is this bad? -/// The type generated by subclassing typing.NamedTuple is equivalent to a collections.namedtuple, -/// but with __annotations__, _field_types and _field_defaults attributes added. -/// -/// For a developer, using the typing module for your namedtuples allows a more natural -/// declarative interface and more precise type inference. -/// -/// ## Example -/// ```python -/// from collections import namedtuple -/// -/// person = namedtuple("Person", ["name", "age"] -/// ``` -/// -/// Use instead: -/// ```python -/// from typing import NamedTuple -/// -/// class Person(NamedTuple): -/// name: str -/// age: int -/// ``` -/// ## References -/// - [StackOverflow](https://stackoverflow.com/a/50767206) -#[violation] -pub struct IncorrectNamedTuple; - -impl AlwaysAutofixableViolation for IncorrectNamedTuple { - #[derive_message_formats] - fn message(&self) -> String { - format!("Use `typing.NamedTuple` instead of `collections.namedtuple`") - } - - fn autofix_title(&self) -> String { - format!("Replace `collections.namedtuple` with `typing.NamedTuple`") - } -} - -/// PYI024 -pub(crate) fn incorrect_named_tuple(checker: &mut Checker, stmt: &Stmt) { - if let Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) = stmt { - let Some(module) = module else { - return - }; - if module.as_str() != "collections" { - return; - } - for name in names { - if name.name.as_str() == "namedtuple" { - let mut diagnostic = Diagnostic::new(IncorrectNamedTuple, name.range()); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::manual(Edit::range_replacement( - format!("from typing import NamedTuple"), - stmt.range(), - ))); - } - checker.diagnostics.push(diagnostic); - } - } - } -} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 787cec142dd40..2fe699df83808 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -2,12 +2,12 @@ pub(crate) use any_eq_ne_annotation::{any_eq_ne_annotation, AnyEqNeAnnotation}; pub(crate) use bad_version_info_comparison::{ bad_version_info_comparison, BadVersionInfoComparison, }; +pub(crate) use collections_named_tuple::{collections_named_tuple, CollectionsNamedTuple}; pub(crate) use docstring_in_stubs::{docstring_in_stubs, DocstringInStub}; pub(crate) use duplicate_union_member::{duplicate_union_member, DuplicateUnionMember}; pub(crate) use ellipsis_in_non_empty_class_body::{ ellipsis_in_non_empty_class_body, EllipsisInNonEmptyClassBody, }; -pub(crate) use incorrect_named_tuple::{incorrect_named_tuple, IncorrectNamedTuple}; pub(crate) use iter_method_return_iterable::{ iter_method_return_iterable, IterMethodReturnIterable, }; @@ -34,10 +34,10 @@ pub(crate) use unrecognized_platform::{ mod any_eq_ne_annotation; mod bad_version_info_comparison; +mod collections_named_tuple; mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; -mod incorrect_named_tuple; mod iter_method_return_iterable; mod non_empty_stub_body; mod pass_in_class_body; diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap index 73a98b98fe765..15402befc2e15 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap @@ -1,15 +1,13 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI024.pyi:1:25: PYI024 [*] Use `typing.NamedTuple` instead of `collections.namedtuple` +PYI024.pyi:3:4: PYI024 Use `typing.NamedTuple` instead of `collections.namedtuple` | -1 | from collections import namedtuple # PYI024 - | ^^^^^^^^^^ PYI024 +3 | import collections +4 | +5 | j: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" + | ^^^^^^^^^^^^^^^^^^^^^^ PYI024 | - = help: Replace `collections.namedtuple` with `typing.NamedTuple` - -ℹ Possible fix -1 |-from collections import namedtuple # PYI024 - 1 |+from typing import NamedTuple # PYI024 + = help: Replace with `typing.NamedTuple` From 52fdeb6cc0a926bb184f4016ed6267338fb7d622 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 31 May 2023 11:46:04 -0400 Subject: [PATCH 4/6] Check name --- .../ruff/resources/test/fixtures/flake8_pyi/PYI024.py | 4 ++++ .../resources/test/fixtures/flake8_pyi/PYI024.pyi | 4 ++++ crates/ruff/src/checkers/ast/mod.rs | 5 +++++ ...__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap | 11 +++++++++++ 4 files changed, 24 insertions(+) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py index c2cec3d658e17..a9ae42a094ee7 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py @@ -1,3 +1,7 @@ import collections j: collections.namedtuple # OK + +from collections import namedtuple + +j: namedtuple # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi index edf47ccff8f01..e3ffcf2f9c374 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi @@ -1,3 +1,7 @@ import collections j: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" + +from collections import namedtuple + +j: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index bc3d0a746c2f6..328fcc8b2f72d 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2292,6 +2292,11 @@ where if self.enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(self, expr); } + if self.is_stub { + if self.enabled(Rule::CollectionsNamedTuple) { + flake8_pyi::rules::collections_named_tuple(self, expr); + } + } // Ex) List[...] if self.any_enabled(&[ diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap index 15402befc2e15..9d97e410425aa 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap @@ -7,6 +7,17 @@ PYI024.pyi:3:4: PYI024 Use `typing.NamedTuple` instead of `collections.namedtupl 4 | 5 | j: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" | ^^^^^^^^^^^^^^^^^^^^^^ PYI024 +6 | +7 | from collections import namedtuple + | + = help: Replace with `typing.NamedTuple` + +PYI024.pyi:7:4: PYI024 Use `typing.NamedTuple` instead of `collections.namedtuple` + | +7 | from collections import namedtuple +8 | +9 | j: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" + | ^^^^^^^^^^ PYI024 | = help: Replace with `typing.NamedTuple` From ea5628676e532eba3edf6019cacf3ac26c0d62fd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 31 May 2023 11:50:54 -0400 Subject: [PATCH 5/6] Fix doc --- .../ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs index 93f593e9a6c80..7f171b755873b 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -22,7 +22,7 @@ use crate::checkers::ast::Checker; /// from collections import namedtuple /// /// -/// person = namedtuple("Person", ["name", "age"] +/// person = namedtuple("Person", ["name", "age"]) /// ``` /// /// Use instead: From c89fb2cac2d1db13e4dcdbafcb34651fd0815acc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 31 May 2023 11:54:48 -0400 Subject: [PATCH 6/6] Add test for call --- .../test/fixtures/flake8_pyi/PYI024.py | 6 ++-- .../test/fixtures/flake8_pyi/PYI024.pyi | 8 +++-- ..._flake8_pyi__tests__PYI024_PYI024.pyi.snap | 35 +++++++++++++------ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py index a9ae42a094ee7..3090ae76c3192 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.py @@ -1,7 +1,9 @@ import collections -j: collections.namedtuple # OK +person: collections.namedtuple # OK from collections import namedtuple -j: namedtuple # OK +person: namedtuple # OK + +person = namedtuple("Person", ["name", "age"]) # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi index e3ffcf2f9c374..b3d3b67b9ed03 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI024.pyi @@ -1,7 +1,11 @@ import collections -j: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" +person: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" from collections import namedtuple -j: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" +person: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" + +person = namedtuple( + "Person", ["name", "age"] +) # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap index 9d97e410425aa..1b8153dda2511 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI024_PYI024.pyi.snap @@ -1,24 +1,37 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI024.pyi:3:4: PYI024 Use `typing.NamedTuple` instead of `collections.namedtuple` +PYI024.pyi:3:9: PYI024 Use `typing.NamedTuple` instead of `collections.namedtuple` | 3 | import collections 4 | -5 | j: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" - | ^^^^^^^^^^^^^^^^^^^^^^ PYI024 +5 | person: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" + | ^^^^^^^^^^^^^^^^^^^^^^ PYI024 6 | 7 | from collections import namedtuple | = help: Replace with `typing.NamedTuple` -PYI024.pyi:7:4: PYI024 Use `typing.NamedTuple` instead of `collections.namedtuple` - | -7 | from collections import namedtuple -8 | -9 | j: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" - | ^^^^^^^^^^ PYI024 - | - = help: Replace with `typing.NamedTuple` +PYI024.pyi:7:9: PYI024 Use `typing.NamedTuple` instead of `collections.namedtuple` + | + 7 | from collections import namedtuple + 8 | + 9 | person: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" + | ^^^^^^^^^^ PYI024 +10 | +11 | person = namedtuple( + | + = help: Replace with `typing.NamedTuple` + +PYI024.pyi:9:10: PYI024 Use `typing.NamedTuple` instead of `collections.namedtuple` + | + 9 | person: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" +10 | +11 | person = namedtuple( + | ^^^^^^^^^^ PYI024 +12 | "Person", ["name", "age"] +13 | ) # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple" + | + = help: Replace with `typing.NamedTuple`