From ff7d452649244c5b53213fcea44615a0515bdf28 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Tue, 20 May 2025 17:36:59 -0400 Subject: [PATCH 01/12] Added `cls.__dict__.get('__annotations__')` check for Python 3.10+ and Python < 3.10 with `typing_extensions` enabled. --- .../resources/test/fixtures/ruff/RUF061.py | 16 +++ .../src/checkers/ast/analyze/expression.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/ruff/mod.rs | 41 ++++++ .../ruff/rules/class_dict_annotations.rs | 131 ++++++++++++++++++ .../ruff_linter/src/rules/ruff/rules/mod.rs | 2 + ...__tests__class_dict_annotations_py310.snap | 30 ++++ ...annotations_py39_no_typing_extensions.snap | 4 + ...notations_py39_with_typing_extensions.snap | 30 ++++ 9 files changed, 258 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF061.py create mode 100644 crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_no_typing_extensions.snap create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061.py new file mode 100644 index 0000000000000..84b9694b4ccae --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061.py @@ -0,0 +1,16 @@ +# RUF061 +# Cases that should trigger the violation + +foo.__dict__.get("__annotations__") # RUF061 +foo.__dict__.get("__annotations__", None) # RUF061 +foo.__dict__.get("__annotations__", {}) # RUF061 + +# Cases that should NOT trigger the violation + +foo.__dict__.get("not__annotations__") # RUF061 +foo.__dict__.get("not__annotations__", None) # RUF061 +foo.__dict__.get("not__annotations__", {}) # RUF061 +foo.__annotations__ # RUF061 +foo.get("__annotations__") # RUF061 +foo.get("__annotations__", None) # RUF061 +foo.get("__annotations__", {}) # RUF061 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 377a50cc396a4..c120c706fd907 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1196,6 +1196,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.enabled(Rule::StarmapZip) { ruff::rules::starmap_zip(checker, call); } + if checker.enabled(Rule::ClassDictAnnotations) { + ruff::rules::class_dict_annotations(checker, call); + } if checker.enabled(Rule::LogExceptionOutsideExceptHandler) { flake8_logging::rules::log_exception_outside_except_handler(checker, call); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 80fcc0708f378..32164b018558e 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1028,6 +1028,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises), + (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::ClassDictAnnotations), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), (Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 5873a8d1ed9ca..aa3c9812fd407 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -170,6 +170,47 @@ mod tests { Ok(()) } + #[test] + fn class_dict_annotations_py39_no_typing_extensions() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF061.py"), + &LinterSettings { + typing_extensions: false, + unresolved_target_version: PythonVersion::PY39.into(), + ..LinterSettings::for_rule(Rule::ClassDictAnnotations) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + + #[test] + fn class_dict_annotations_py39_with_typing_extensions() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF061.py"), + &LinterSettings { + typing_extensions: true, + unresolved_target_version: PythonVersion::PY39.into(), + ..LinterSettings::for_rule(Rule::ClassDictAnnotations) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + + #[test] + fn class_dict_annotations_py310() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF061.py"), + &LinterSettings { + unresolved_target_version: PythonVersion::PY310.into(), + ..LinterSettings::for_rule(Rule::ClassDictAnnotations) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + #[test] fn confusables() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs new file mode 100644 index 0000000000000..95dcf6e649ecf --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -0,0 +1,131 @@ +use crate::checkers::ast::Checker; +use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{Expr, ExprCall, PythonVersion}; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks for uses of `.__dict__.get("__annotations__" [, ])` +/// on Python 3.10+ and Python < 3.10 with `typing_extensions` enabled. +/// +/// ## Why is this bad? +/// On Python 3.10 and newer, `.__dict__.get("__annotations__" [, ])` +/// can be unreliable, especially with the introduction of stringized annotations +/// and features like `from __future__ import annotations`. For Python 3.14+, this +/// approach is more strongly discouraged. +/// +/// See [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) +/// for alternatives. +/// +/// TLDR: +/// +/// If you use a class with a custom metaclass and access `__annotations__` +/// on the class, you may observe unexpected behavior; see +/// [PEP 749](https://peps.python.org/pep-0749/#pep749-metaclasses) for some +/// examples. +/// +/// You can avoid these quirks by using `annotationlib.get_annotations()` on +/// Python 3.14+, `inspect.get_annotations()` on Python 3.10+, or +/// `typing_extensions.get_annotations(cls)` on Python < 3.10 with +/// `typing_extensions` enabled. +/// +/// ## Example +/// +/// ```python +/// cls.__dict__.get("__annotations__", {}) +/// ``` +/// +/// On Python 3.14+, use instead: +/// ```python +/// import annotationlib +/// +/// annotationlib.get_annotations(cls) +/// ``` +/// +/// On Python 3.10+, use instead: +/// ```python +/// import inspect +/// +/// inspect.get_annotations(cls) +/// ``` +/// +/// On Python < 3.10 with `typing_extensions` enabled, use instead: +/// ```python +/// import typing_extensions +/// +/// typing_extensions.get_annotations(cls) +/// ``` +/// +/// ## Fix safety +/// +/// No autofix is currently provided for this rule. +/// +/// ## Fix availability +/// +/// No autofix is currently provided for this rule. +/// +/// ## References +/// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) +#[derive(ViolationMetadata)] +pub(crate) struct ClassDictAnnotations; + +impl Violation for ClassDictAnnotations { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::None; + + #[derive_message_formats] + fn message(&self) -> String { + "Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')`".to_string() + } +} + +/// RUF061 +pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { + // Only apply this rule for Python 3.10 and newer unless `typing_extensions` is enabled. + if checker.target_version() < PythonVersion::PY310 && !checker.settings.typing_extensions { + return; + } + + // Expected pattern: .__dict__.get("__annotations__" [, ]) + // Here, `call` is the `.get(...)` part. + + // 1. Check that the `call.func` is `get` + let get_attribute = match call.func.as_ref() { + Expr::Attribute(attr) if attr.attr.as_str() == "get" => attr, + _ => return, // Not a call to an attribute named "get" + }; + + // 2. Check that the `get_attribute.value` is `__dict__` + let dict_attribute = match get_attribute.value.as_ref() { + Expr::Attribute(attr) if attr.attr.as_str() == "__dict__" => attr, + _ => return, // The object of ".get" is not an attribute named "__dict__" + }; + + // 3. Check that the `dict_attribute.value` is an Expr::Name (e.g., `cls`, `obj`) + let Expr::Name(_object_name_expr) = dict_attribute.value.as_ref() else { + return; // Not .__dict__.get + }; + // `_object_name_expr` is now an &ruff_python_ast::ExprName + + // At this point, we have `.__dict__.get`. Now check arguments. + + // 4. Check arguments to `.get()`: + // - No keyword arguments. + // - One or two positional arguments. + // - First positional argument must be the string literal "__annotations__". + // - The value of the second positional argument (if present) does not affect the match. + if call.arguments.keywords.is_empty() + && (call.arguments.args.len() == 1 || call.arguments.args.len() == 2) + { + let first_arg = &call.arguments.args[0]; + let is_first_arg_correct = match first_arg.as_string_literal_expr() { + Some(str_literal) => str_literal.value.to_str() == "__annotations__", + None => false, + }; + + if is_first_arg_correct { + // Pattern successfully matched! Report a diagnostic. + let diagnostic = Diagnostic::new(ClassDictAnnotations, call.range()); + checker.report_diagnostic(diagnostic); + } + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index a8344ad9874d5..303f16bdfd9fd 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -2,6 +2,7 @@ pub(crate) use ambiguous_unicode_character::*; pub(crate) use assert_with_print_message::*; pub(crate) use assignment_in_assert::*; pub(crate) use asyncio_dangling_task::*; +pub(crate) use class_dict_annotations::*; pub(crate) use class_with_mixed_type_vars::*; pub(crate) use collection_literal_concatenation::*; pub(crate) use dataclass_enum::*; @@ -62,6 +63,7 @@ mod ambiguous_unicode_character; mod assert_with_print_message; mod assignment_in_assert; mod asyncio_dangling_task; +mod class_dict_annotations; mod class_with_mixed_type_vars; mod collection_literal_concatenation; mod confusables; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap new file mode 100644 index 0000000000000..0f137181f1d41 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF061.py:4:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` + | +2 | # Cases that should trigger the violation +3 | +4 | foo.__dict__.get("__annotations__") # RUF061 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +5 | foo.__dict__.get("__annotations__", None) # RUF061 +6 | foo.__dict__.get("__annotations__", {}) # RUF061 + | + +RUF061.py:5:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` + | +4 | foo.__dict__.get("__annotations__") # RUF061 +5 | foo.__dict__.get("__annotations__", None) # RUF061 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +6 | foo.__dict__.get("__annotations__", {}) # RUF061 + | + +RUF061.py:6:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` + | +4 | foo.__dict__.get("__annotations__") # RUF061 +5 | foo.__dict__.get("__annotations__", None) # RUF061 +6 | foo.__dict__.get("__annotations__", {}) # RUF061 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +7 | +8 | # Cases that should NOT trigger the violation + | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_no_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_no_typing_extensions.snap new file mode 100644 index 0000000000000..7f58cfd7246a3 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_no_typing_extensions.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap new file mode 100644 index 0000000000000..0f137181f1d41 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF061.py:4:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` + | +2 | # Cases that should trigger the violation +3 | +4 | foo.__dict__.get("__annotations__") # RUF061 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +5 | foo.__dict__.get("__annotations__", None) # RUF061 +6 | foo.__dict__.get("__annotations__", {}) # RUF061 + | + +RUF061.py:5:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` + | +4 | foo.__dict__.get("__annotations__") # RUF061 +5 | foo.__dict__.get("__annotations__", None) # RUF061 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +6 | foo.__dict__.get("__annotations__", {}) # RUF061 + | + +RUF061.py:6:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` + | +4 | foo.__dict__.get("__annotations__") # RUF061 +5 | foo.__dict__.get("__annotations__", None) # RUF061 +6 | foo.__dict__.get("__annotations__", {}) # RUF061 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +7 | +8 | # Cases that should NOT trigger the violation + | From c7a521566e2fddc0bc6fce263e5988af98a800d3 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Thu, 22 May 2025 12:56:31 -0400 Subject: [PATCH 02/12] implemented PR review suggestions --- .../ruff/rules/class_dict_annotations.rs | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index 95dcf6e649ecf..229a6fecc11c5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -9,26 +9,27 @@ use ruff_text_size::Ranged; /// on Python 3.10+ and Python < 3.10 with `typing_extensions` enabled. /// /// ## Why is this bad? -/// On Python 3.10 and newer, `.__dict__.get("__annotations__" [, ])` -/// can be unreliable, especially with the introduction of stringized annotations -/// and features like `from __future__ import annotations`. For Python 3.14+, this -/// approach is more strongly discouraged. +/// Starting with Python 3.14, directly accessing `__annotations__` via +/// `.__dict__.get("__annotations__")` will only return annotations +/// if the class is defined under `from __future__ import annotations`. +/// +/// Therefore, it is better to use dedicated library functions like +/// `inspect.get_annotations` (Python 3.10+), +/// `typing_extensions.get_annotations` (for older Python versions if +/// `typing_extensions` is available), or `annotationlib.get_annotations` +/// (Python 3.14+). +/// +/// The benefits of using these functions include: +/// 1. **Avoiding Undocumented Internals:** They provide a stable, public API, +/// unlike direct `__dict__` access which relies on implementation details. +/// 2. **Forward-Compatibility:** They are designed to handle changes in +/// Python's annotation system across versions, ensuring your code remains +/// robust (e.g., correctly handling the Python 3.14 behavior mentioned +/// above). /// /// See [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) /// for alternatives. /// -/// TLDR: -/// -/// If you use a class with a custom metaclass and access `__annotations__` -/// on the class, you may observe unexpected behavior; see -/// [PEP 749](https://peps.python.org/pep-0749/#pep749-metaclasses) for some -/// examples. -/// -/// You can avoid these quirks by using `annotationlib.get_annotations()` on -/// Python 3.14+, `inspect.get_annotations()` on Python 3.10+, or -/// `typing_extensions.get_annotations(cls)` on Python < 3.10 with -/// `typing_extensions` enabled. -/// /// ## Example /// /// ```python @@ -74,7 +75,11 @@ impl Violation for ClassDictAnnotations { #[derive_message_formats] fn message(&self) -> String { - "Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')`".to_string() + "Use `typing_extensions.get_annotations` (Python < 3.10 with \ + `typing_extensions` enabled), `inspect.get_annotations` \ + (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) \ + instead of `__dict__.get('__annotations__')`" + .to_string() } } @@ -95,20 +100,14 @@ pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { }; // 2. Check that the `get_attribute.value` is `__dict__` - let dict_attribute = match get_attribute.value.as_ref() { - Expr::Attribute(attr) if attr.attr.as_str() == "__dict__" => attr, + match get_attribute.value.as_ref() { + Expr::Attribute(attr) if attr.attr.as_str() == "__dict__" => {} _ => return, // The object of ".get" is not an attribute named "__dict__" - }; - - // 3. Check that the `dict_attribute.value` is an Expr::Name (e.g., `cls`, `obj`) - let Expr::Name(_object_name_expr) = dict_attribute.value.as_ref() else { - return; // Not .__dict__.get - }; - // `_object_name_expr` is now an &ruff_python_ast::ExprName + } - // At this point, we have `.__dict__.get`. Now check arguments. + // At this point, we have `.__dict__.get`. - // 4. Check arguments to `.get()`: + // 3. Check arguments to `.get()`: // - No keyword arguments. // - One or two positional arguments. // - First positional argument must be the string literal "__annotations__". From cdf11394dfa96d0abda8b99cc5894f7a3cf9fd08 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Fri, 23 May 2025 00:29:44 -0400 Subject: [PATCH 03/12] Trimmed message (slightly) --- .../src/rules/ruff/rules/class_dict_annotations.rs | 6 +++--- ...r__rules__ruff__tests__class_dict_annotations_py310.snap | 6 +++--- ..._class_dict_annotations_py39_with_typing_extensions.snap | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index 229a6fecc11c5..3dd20f5774cee 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -75,9 +75,9 @@ impl Violation for ClassDictAnnotations { #[derive_message_formats] fn message(&self) -> String { - "Use `typing_extensions.get_annotations` (Python < 3.10 with \ - `typing_extensions` enabled), `inspect.get_annotations` \ - (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) \ + "Use `annotationlib.get_annotations` (Py3.14+), \ + `inspect.get_annotations` (Py3.10+), or \ + `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) \ instead of `__dict__.get('__annotations__')`" .to_string() } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap index 0f137181f1d41..3ed644c217232 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF061.py:4:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` +RUF061.py:4:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | 2 | # Cases that should trigger the violation 3 | @@ -11,7 +11,7 @@ RUF061.py:4:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 wit 6 | foo.__dict__.get("__annotations__", {}) # RUF061 | -RUF061.py:5:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` +RUF061.py:5:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | 4 | foo.__dict__.get("__annotations__") # RUF061 5 | foo.__dict__.get("__annotations__", None) # RUF061 @@ -19,7 +19,7 @@ RUF061.py:5:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 wit 6 | foo.__dict__.get("__annotations__", {}) # RUF061 | -RUF061.py:6:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` +RUF061.py:6:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | 4 | foo.__dict__.get("__annotations__") # RUF061 5 | foo.__dict__.get("__annotations__", None) # RUF061 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap index 0f137181f1d41..3ed644c217232 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF061.py:4:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` +RUF061.py:4:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | 2 | # Cases that should trigger the violation 3 | @@ -11,7 +11,7 @@ RUF061.py:4:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 wit 6 | foo.__dict__.get("__annotations__", {}) # RUF061 | -RUF061.py:5:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` +RUF061.py:5:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | 4 | foo.__dict__.get("__annotations__") # RUF061 5 | foo.__dict__.get("__annotations__", None) # RUF061 @@ -19,7 +19,7 @@ RUF061.py:5:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 wit 6 | foo.__dict__.get("__annotations__", {}) # RUF061 | -RUF061.py:6:1: RUF061 Use `typing_extensions.get_annotations` (Python < 3.10 with `typing_extensions` enabled), `inspect.get_annotations` (Python 3.10+), or `annotationlib.get_annotations` (Python 3.14+) instead of `__dict__.get('__annotations__')` +RUF061.py:6:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | 4 | foo.__dict__.get("__annotations__") # RUF061 5 | foo.__dict__.get("__annotations__", None) # RUF061 From 6364a5bb32552c6c2a72f284c93e657691531c89 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Fri, 23 May 2025 00:35:29 -0400 Subject: [PATCH 04/12] Fixed usage of `typing-extensions`: module is installed & setting is enabled --- .../rules/ruff/rules/class_dict_annotations.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index 3dd20f5774cee..03fd37470b405 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -6,7 +6,9 @@ use ruff_text_size::Ranged; /// ## What it does /// Checks for uses of `.__dict__.get("__annotations__" [, ])` -/// on Python 3.10+ and Python < 3.10 with `typing_extensions` enabled. +/// on Python 3.10+ and Python < 3.10 when +/// [typing-extensions](https://docs.astral.sh/ruff/settings/#lint_typing-extensions) +/// is enabled. /// /// ## Why is this bad? /// Starting with Python 3.14, directly accessing `__annotations__` via @@ -14,10 +16,10 @@ use ruff_text_size::Ranged; /// if the class is defined under `from __future__ import annotations`. /// /// Therefore, it is better to use dedicated library functions like -/// `inspect.get_annotations` (Python 3.10+), -/// `typing_extensions.get_annotations` (for older Python versions if -/// `typing_extensions` is available), or `annotationlib.get_annotations` -/// (Python 3.14+). +/// `annotationlib.get_annotations` (Python 3.14+), `inspect.get_annotations` +/// (Python 3.10+), or `typing_extensions.get_annotations` (for Python < 3.10 if +/// [typing-extensions](https://pypi.org/project/typing-extensions/) is +/// available). /// /// The benefits of using these functions include: /// 1. **Avoiding Undocumented Internals:** They provide a stable, public API, @@ -50,7 +52,8 @@ use ruff_text_size::Ranged; /// inspect.get_annotations(cls) /// ``` /// -/// On Python < 3.10 with `typing_extensions` enabled, use instead: +/// On Python < 3.10 with [typing-extensions](https://pypi.org/project/typing-extensions/) +/// installed, use instead: /// ```python /// import typing_extensions /// @@ -85,7 +88,7 @@ impl Violation for ClassDictAnnotations { /// RUF061 pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { - // Only apply this rule for Python 3.10 and newer unless `typing_extensions` is enabled. + // Only apply this rule for Python 3.10 and newer unless `typing-extensions` is enabled. if checker.target_version() < PythonVersion::PY310 && !checker.settings.typing_extensions { return; } From 598eb97cd69b70cf8f158f26dd5df8f185fde345 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Sun, 8 Jun 2025 15:05:07 -0400 Subject: [PATCH 05/12] implemented PR review suggestions Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../ruff/rules/class_dict_annotations.rs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index 03fd37470b405..df23118254004 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -5,14 +5,14 @@ use ruff_python_ast::{Expr, ExprCall, PythonVersion}; use ruff_text_size::Ranged; /// ## What it does -/// Checks for uses of `.__dict__.get("__annotations__" [, ])` +/// Checks for uses of `foo.__dict__.get("__annotations__")` /// on Python 3.10+ and Python < 3.10 when /// [typing-extensions](https://docs.astral.sh/ruff/settings/#lint_typing-extensions) /// is enabled. /// /// ## Why is this bad? /// Starting with Python 3.14, directly accessing `__annotations__` via -/// `.__dict__.get("__annotations__")` will only return annotations +/// `foo.__dict__.get("__annotations__")` will only return annotations /// if the class is defined under `from __future__ import annotations`. /// /// Therefore, it is better to use dedicated library functions like @@ -35,21 +35,21 @@ use ruff_text_size::Ranged; /// ## Example /// /// ```python -/// cls.__dict__.get("__annotations__", {}) +/// foo.__dict__.get("__annotations__", {}) /// ``` /// /// On Python 3.14+, use instead: /// ```python /// import annotationlib /// -/// annotationlib.get_annotations(cls) +/// annotationlib.get_annotations(foo) /// ``` /// /// On Python 3.10+, use instead: /// ```python /// import inspect /// -/// inspect.get_annotations(cls) +/// inspect.get_annotations(foo) /// ``` /// /// On Python < 3.10 with [typing-extensions](https://pypi.org/project/typing-extensions/) @@ -57,7 +57,7 @@ use ruff_text_size::Ranged; /// ```python /// import typing_extensions /// -/// typing_extensions.get_annotations(cls) +/// typing_extensions.get_annotations(foo) /// ``` /// /// ## Fix safety @@ -93,39 +93,41 @@ pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { return; } - // Expected pattern: .__dict__.get("__annotations__" [, ]) + // Expected pattern: foo.__dict__.get("__annotations__" [, ]) // Here, `call` is the `.get(...)` part. // 1. Check that the `call.func` is `get` let get_attribute = match call.func.as_ref() { Expr::Attribute(attr) if attr.attr.as_str() == "get" => attr, - _ => return, // Not a call to an attribute named "get" + _ => return, }; // 2. Check that the `get_attribute.value` is `__dict__` match get_attribute.value.as_ref() { Expr::Attribute(attr) if attr.attr.as_str() == "__dict__" => {} - _ => return, // The object of ".get" is not an attribute named "__dict__" + _ => return, } - // At this point, we have `.__dict__.get`. + // At this point, we have `foo.__dict__.get`. // 3. Check arguments to `.get()`: // - No keyword arguments. // - One or two positional arguments. // - First positional argument must be the string literal "__annotations__". // - The value of the second positional argument (if present) does not affect the match. - if call.arguments.keywords.is_empty() - && (call.arguments.args.len() == 1 || call.arguments.args.len() == 2) + if !call.arguments.keywords.is_empty() || call.arguments.len() > 2 { + return; + } { - let first_arg = &call.arguments.args[0]; - let is_first_arg_correct = match first_arg.as_string_literal_expr() { - Some(str_literal) => str_literal.value.to_str() == "__annotations__", - None => false, + let Some(first_arg) = &call.arguments.find_positional(0) else { + return; }; + let is_first_arg_correct = first_arg + .as_string_literal_expr() + .is_some_and(|s| s.value.to_str() == "__annotations__"); + if is_first_arg_correct { - // Pattern successfully matched! Report a diagnostic. let diagnostic = Diagnostic::new(ClassDictAnnotations, call.range()); checker.report_diagnostic(diagnostic); } From 04840b33a6509cdf2fcb6dc9934de05724c12737 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Sun, 8 Jun 2025 15:12:50 -0400 Subject: [PATCH 06/12] renumbered to RUF063 --- .../resources/test/fixtures/ruff/RUF061.py | 16 ---------- .../resources/test/fixtures/ruff/RUF063.py | 16 ++++++++++ crates/ruff_linter/src/codes.rs | 2 +- crates/ruff_linter/src/rules/ruff/mod.rs | 6 ++-- .../ruff/rules/class_dict_annotations.rs | 2 +- ...__tests__class_dict_annotations_py310.snap | 30 +++++++++---------- ...notations_py39_with_typing_extensions.snap | 30 +++++++++---------- ruff.schema.json | 2 +- 8 files changed, 52 insertions(+), 52 deletions(-) delete mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF061.py create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061.py deleted file mode 100644 index 84b9694b4ccae..0000000000000 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061.py +++ /dev/null @@ -1,16 +0,0 @@ -# RUF061 -# Cases that should trigger the violation - -foo.__dict__.get("__annotations__") # RUF061 -foo.__dict__.get("__annotations__", None) # RUF061 -foo.__dict__.get("__annotations__", {}) # RUF061 - -# Cases that should NOT trigger the violation - -foo.__dict__.get("not__annotations__") # RUF061 -foo.__dict__.get("not__annotations__", None) # RUF061 -foo.__dict__.get("not__annotations__", {}) # RUF061 -foo.__annotations__ # RUF061 -foo.get("__annotations__") # RUF061 -foo.get("__annotations__", None) # RUF061 -foo.get("__annotations__", {}) # RUF061 diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py new file mode 100644 index 0000000000000..5743b02278408 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py @@ -0,0 +1,16 @@ +# RUF063 +# Cases that should trigger the violation + +foo.__dict__.get("__annotations__") # RUF063 +foo.__dict__.get("__annotations__", None) # RUF063 +foo.__dict__.get("__annotations__", {}) # RUF063 + +# Cases that should NOT trigger the violation + +foo.__dict__.get("not__annotations__") +foo.__dict__.get("not__annotations__", None) +foo.__dict__.get("not__annotations__", {}) +foo.__annotations__ +foo.get("__annotations__") +foo.get("__annotations__", None) +foo.get("__annotations__", {}) diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 32164b018558e..e77e38700f63c 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1028,7 +1028,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises), - (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::ClassDictAnnotations), + (Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::ClassDictAnnotations), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), (Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index aa3c9812fd407..ef125808ac211 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -173,7 +173,7 @@ mod tests { #[test] fn class_dict_annotations_py39_no_typing_extensions() -> Result<()> { let diagnostics = test_path( - Path::new("ruff/RUF061.py"), + Path::new("ruff/RUF063.py"), &LinterSettings { typing_extensions: false, unresolved_target_version: PythonVersion::PY39.into(), @@ -187,7 +187,7 @@ mod tests { #[test] fn class_dict_annotations_py39_with_typing_extensions() -> Result<()> { let diagnostics = test_path( - Path::new("ruff/RUF061.py"), + Path::new("ruff/RUF063.py"), &LinterSettings { typing_extensions: true, unresolved_target_version: PythonVersion::PY39.into(), @@ -201,7 +201,7 @@ mod tests { #[test] fn class_dict_annotations_py310() -> Result<()> { let diagnostics = test_path( - Path::new("ruff/RUF061.py"), + Path::new("ruff/RUF063.py"), &LinterSettings { unresolved_target_version: PythonVersion::PY310.into(), ..LinterSettings::for_rule(Rule::ClassDictAnnotations) diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index df23118254004..2e4a5219a9fe3 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -86,7 +86,7 @@ impl Violation for ClassDictAnnotations { } } -/// RUF061 +/// RUF063 pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { // Only apply this rule for Python 3.10 and newer unless `typing-extensions` is enabled. if checker.target_version() < PythonVersion::PY310 && !checker.settings.typing_extensions { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap index 3ed644c217232..67943c610ef32 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap @@ -1,30 +1,30 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF061.py:4:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | 2 | # Cases that should trigger the violation 3 | -4 | foo.__dict__.get("__annotations__") # RUF061 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 -5 | foo.__dict__.get("__annotations__", None) # RUF061 -6 | foo.__dict__.get("__annotations__", {}) # RUF061 +4 | foo.__dict__.get("__annotations__") # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF061.py:5:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | -4 | foo.__dict__.get("__annotations__") # RUF061 -5 | foo.__dict__.get("__annotations__", None) # RUF061 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 -6 | foo.__dict__.get("__annotations__", {}) # RUF061 +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF061.py:6:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:6:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | -4 | foo.__dict__.get("__annotations__") # RUF061 -5 | foo.__dict__.get("__annotations__", None) # RUF061 -6 | foo.__dict__.get("__annotations__", {}) # RUF061 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 7 | 8 | # Cases that should NOT trigger the violation | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap index 3ed644c217232..67943c610ef32 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap @@ -1,30 +1,30 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF061.py:4:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | 2 | # Cases that should trigger the violation 3 | -4 | foo.__dict__.get("__annotations__") # RUF061 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 -5 | foo.__dict__.get("__annotations__", None) # RUF061 -6 | foo.__dict__.get("__annotations__", {}) # RUF061 +4 | foo.__dict__.get("__annotations__") # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF061.py:5:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | -4 | foo.__dict__.get("__annotations__") # RUF061 -5 | foo.__dict__.get("__annotations__", None) # RUF061 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 -6 | foo.__dict__.get("__annotations__", {}) # RUF061 +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF061.py:6:1: RUF061 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:6:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` | -4 | foo.__dict__.get("__annotations__") # RUF061 -5 | foo.__dict__.get("__annotations__", None) # RUF061 -6 | foo.__dict__.get("__annotations__", {}) # RUF061 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 7 | 8 | # Cases that should NOT trigger the violation | diff --git a/ruff.schema.json b/ruff.schema.json index e8c6c30e853bd..571d2e913878c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4039,7 +4039,7 @@ "RUF059", "RUF06", "RUF060", - "RUF061", + "RUF063", "RUF1", "RUF10", "RUF100", From 397223185b993ed1336d6605c33cb1d49ebfe84c Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Sun, 8 Jun 2025 15:32:45 -0400 Subject: [PATCH 07/12] updated to 'checker.report_diagnostic(ClassDictAnnotations, call.range());' --- .../src/rules/ruff/rules/class_dict_annotations.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index 2e4a5219a9fe3..5545b16f9af91 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -1,5 +1,5 @@ use crate::checkers::ast::Checker; -use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; +use crate::{FixAvailability, Violation}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprCall, PythonVersion}; use ruff_text_size::Ranged; @@ -128,8 +128,7 @@ pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { .is_some_and(|s| s.value.to_str() == "__annotations__"); if is_first_arg_correct { - let diagnostic = Diagnostic::new(ClassDictAnnotations, call.range()); - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(ClassDictAnnotations, call.range()); } } } From 0b5c64fa0af51cf5ec1575cb7ef1bc2cd5465a14 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Sun, 8 Jun 2025 16:12:59 -0400 Subject: [PATCH 08/12] updated suggestion message based on python version and typing-extensions setting --- crates/ruff_linter/src/rules/ruff/mod.rs | 13 ++++++++ .../ruff/rules/class_dict_annotations.rs | 24 ++++++++++----- ...__tests__class_dict_annotations_py310.snap | 6 ++-- ...__tests__class_dict_annotations_py314.snap | 30 +++++++++++++++++++ ...notations_py39_with_typing_extensions.snap | 6 ++-- 5 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py314.snap diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index ef125808ac211..dc615210e0cdf 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -211,6 +211,19 @@ mod tests { Ok(()) } + #[test] + fn class_dict_annotations_py314() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF063.py"), + &LinterSettings { + unresolved_target_version: PythonVersion::PY314.into(), + ..LinterSettings::for_rule(Rule::ClassDictAnnotations) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + #[test] fn confusables() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index 5545b16f9af91..71ab1aa2c38bf 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -71,25 +71,33 @@ use ruff_text_size::Ranged; /// ## References /// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) #[derive(ViolationMetadata)] -pub(crate) struct ClassDictAnnotations; +pub(crate) struct ClassDictAnnotations { + python_version: PythonVersion, +} impl Violation for ClassDictAnnotations { const FIX_AVAILABILITY: FixAvailability = FixAvailability::None; #[derive_message_formats] fn message(&self) -> String { - "Use `annotationlib.get_annotations` (Py3.14+), \ - `inspect.get_annotations` (Py3.10+), or \ - `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) \ - instead of `__dict__.get('__annotations__')`" - .to_string() + let suggestion = if self.python_version >= PythonVersion::PY314 { + "Use `annotationlib.get_annotations`" + } else if self.python_version >= PythonVersion::PY310 { + "Use `inspect.get_annotations`" + } else { + "Use `typing_extensions.get_annotations`" + }; + format!("{suggestion} instead of `__dict__.get('__annotations__')`") } } /// RUF063 pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { + let python_version = checker.target_version(); + let typing_extensions = checker.settings.typing_extensions; + // Only apply this rule for Python 3.10 and newer unless `typing-extensions` is enabled. - if checker.target_version() < PythonVersion::PY310 && !checker.settings.typing_extensions { + if python_version < PythonVersion::PY310 && !typing_extensions { return; } @@ -128,7 +136,7 @@ pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { .is_some_and(|s| s.value.to_str() == "__annotations__"); if is_first_arg_correct { - checker.report_diagnostic(ClassDictAnnotations, call.range()); + checker.report_diagnostic(ClassDictAnnotations { python_version }, call.range()); } } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap index 67943c610ef32..c719e76b18cac 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:4:1: RUF063 Use `inspect.get_annotations` instead of `__dict__.get('__annotations__')` | 2 | # Cases that should trigger the violation 3 | @@ -11,7 +11,7 @@ RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.ge 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:5:1: RUF063 Use `inspect.get_annotations` instead of `__dict__.get('__annotations__')` | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 @@ -19,7 +19,7 @@ RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.ge 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF063.py:6:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:6:1: RUF063 Use `inspect.get_annotations` instead of `__dict__.get('__annotations__')` | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py314.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py314.snap new file mode 100644 index 0000000000000..538e644a92548 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py314.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__.get('__annotations__')` + | +2 | # Cases that should trigger the violation +3 | +4 | foo.__dict__.get("__annotations__") # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | + +RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__.get('__annotations__')` + | +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | + +RUF063.py:6:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__.get('__annotations__')` + | +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +7 | +8 | # Cases that should NOT trigger the violation + | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap index 67943c610ef32..93ce037aee440 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:4:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__.get('__annotations__')` | 2 | # Cases that should trigger the violation 3 | @@ -11,7 +11,7 @@ RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.ge 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:5:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__.get('__annotations__')` | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 @@ -19,7 +19,7 @@ RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.ge 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF063.py:6:1: RUF063 Use `annotationlib.get_annotations` (Py3.14+), `inspect.get_annotations` (Py3.10+), or `typing_extensions.get_annotations` (Py<3.10 w/ `typing-extensions`) instead of `__dict__.get('__annotations__')` +RUF063.py:6:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__.get('__annotations__')` | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 From bb5894e92febeebfb40085655abec68d90cb98c2 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Wed, 18 Jun 2025 18:25:04 -0400 Subject: [PATCH 09/12] remove unnecessary block --- .../ruff/rules/class_dict_annotations.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index 71ab1aa2c38bf..ad8713bd82811 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -126,17 +126,16 @@ pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { if !call.arguments.keywords.is_empty() || call.arguments.len() > 2 { return; } - { - let Some(first_arg) = &call.arguments.find_positional(0) else { - return; - }; - let is_first_arg_correct = first_arg - .as_string_literal_expr() - .is_some_and(|s| s.value.to_str() == "__annotations__"); + let Some(first_arg) = &call.arguments.find_positional(0) else { + return; + }; + + let is_first_arg_correct = first_arg + .as_string_literal_expr() + .is_some_and(|s| s.value.to_str() == "__annotations__"); - if is_first_arg_correct { - checker.report_diagnostic(ClassDictAnnotations { python_version }, call.range()); - } + if is_first_arg_correct { + checker.report_diagnostic(ClassDictAnnotations { python_version }, call.range()); } } From b11802e40723fcd16e236142ce902f69ad650d5f Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Wed, 18 Jun 2025 18:26:30 -0400 Subject: [PATCH 10/12] improve message formatting --- .../src/rules/ruff/rules/class_dict_annotations.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs index ad8713bd82811..00885f26646e5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs @@ -81,13 +81,13 @@ impl Violation for ClassDictAnnotations { #[derive_message_formats] fn message(&self) -> String { let suggestion = if self.python_version >= PythonVersion::PY314 { - "Use `annotationlib.get_annotations`" + "annotationlib.get_annotations" } else if self.python_version >= PythonVersion::PY310 { - "Use `inspect.get_annotations`" + "inspect.get_annotations" } else { - "Use `typing_extensions.get_annotations`" + "typing_extensions.get_annotations" }; - format!("{suggestion} instead of `__dict__.get('__annotations__')`") + format!("Use `{suggestion}` instead of `__dict__.get('__annotations__')`") } } From 00cfbd8d4c3e1ec9686234fb40f0be814f245e49 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Wed, 18 Jun 2025 18:51:06 -0400 Subject: [PATCH 11/12] rename to 'AnnotationsFromClassDict' --- .../src/checkers/ast/analyze/expression.rs | 4 ++-- crates/ruff_linter/src/codes.rs | 2 +- crates/ruff_linter/src/rules/ruff/mod.rs | 16 ++++++++-------- ...tations.rs => annotations_from_class_dict.rs} | 8 ++++---- crates/ruff_linter/src/rules/ruff/rules/mod.rs | 4 ++-- ...ests__annotations_from_class_dict_py310.snap} | 0 ...ests__annotations_from_class_dict_py314.snap} | 0 ...om_class_dict_py39_no_typing_extensions.snap} | 0 ..._class_dict_py39_with_typing_extensions.snap} | 0 9 files changed, 17 insertions(+), 17 deletions(-) rename crates/ruff_linter/src/rules/ruff/rules/{class_dict_annotations.rs => annotations_from_class_dict.rs} (94%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap => ruff_linter__rules__ruff__tests__annotations_from_class_dict_py310.snap} (100%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__class_dict_annotations_py314.snap => ruff_linter__rules__ruff__tests__annotations_from_class_dict_py314.snap} (100%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__class_dict_annotations_py39_no_typing_extensions.snap => ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_no_typing_extensions.snap} (100%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap => ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_with_typing_extensions.snap} (100%) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index c120c706fd907..9a141b08c1f13 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1196,8 +1196,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.enabled(Rule::StarmapZip) { ruff::rules::starmap_zip(checker, call); } - if checker.enabled(Rule::ClassDictAnnotations) { - ruff::rules::class_dict_annotations(checker, call); + if checker.enabled(Rule::AnnotationsFromClassDict) { + ruff::rules::annotations_from_class_dict(checker, call); } if checker.enabled(Rule::LogExceptionOutsideExceptHandler) { flake8_logging::rules::log_exception_outside_except_handler(checker, call); diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index e77e38700f63c..baf14adb8e5b8 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1028,7 +1028,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises), - (Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::ClassDictAnnotations), + (Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AnnotationsFromClassDict), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), (Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index dc615210e0cdf..16340452a0e1d 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -171,13 +171,13 @@ mod tests { } #[test] - fn class_dict_annotations_py39_no_typing_extensions() -> Result<()> { + fn annotations_from_class_dict_py39_no_typing_extensions() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), &LinterSettings { typing_extensions: false, unresolved_target_version: PythonVersion::PY39.into(), - ..LinterSettings::for_rule(Rule::ClassDictAnnotations) + ..LinterSettings::for_rule(Rule::AnnotationsFromClassDict) }, )?; assert_messages!(diagnostics); @@ -185,13 +185,13 @@ mod tests { } #[test] - fn class_dict_annotations_py39_with_typing_extensions() -> Result<()> { + fn annotations_from_class_dict_py39_with_typing_extensions() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), &LinterSettings { typing_extensions: true, unresolved_target_version: PythonVersion::PY39.into(), - ..LinterSettings::for_rule(Rule::ClassDictAnnotations) + ..LinterSettings::for_rule(Rule::AnnotationsFromClassDict) }, )?; assert_messages!(diagnostics); @@ -199,12 +199,12 @@ mod tests { } #[test] - fn class_dict_annotations_py310() -> Result<()> { + fn annotations_from_class_dict_py310() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), &LinterSettings { unresolved_target_version: PythonVersion::PY310.into(), - ..LinterSettings::for_rule(Rule::ClassDictAnnotations) + ..LinterSettings::for_rule(Rule::AnnotationsFromClassDict) }, )?; assert_messages!(diagnostics); @@ -212,12 +212,12 @@ mod tests { } #[test] - fn class_dict_annotations_py314() -> Result<()> { + fn annotations_from_class_dict_py314() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), &LinterSettings { unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(Rule::ClassDictAnnotations) + ..LinterSettings::for_rule(Rule::AnnotationsFromClassDict) }, )?; assert_messages!(diagnostics); diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs b/crates/ruff_linter/src/rules/ruff/rules/annotations_from_class_dict.rs similarity index 94% rename from crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs rename to crates/ruff_linter/src/rules/ruff/rules/annotations_from_class_dict.rs index 00885f26646e5..9a09dad7e3797 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_dict_annotations.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/annotations_from_class_dict.rs @@ -71,11 +71,11 @@ use ruff_text_size::Ranged; /// ## References /// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) #[derive(ViolationMetadata)] -pub(crate) struct ClassDictAnnotations { +pub(crate) struct AnnotationsFromClassDict { python_version: PythonVersion, } -impl Violation for ClassDictAnnotations { +impl Violation for AnnotationsFromClassDict { const FIX_AVAILABILITY: FixAvailability = FixAvailability::None; #[derive_message_formats] @@ -92,7 +92,7 @@ impl Violation for ClassDictAnnotations { } /// RUF063 -pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { +pub(crate) fn annotations_from_class_dict(checker: &Checker, call: &ExprCall) { let python_version = checker.target_version(); let typing_extensions = checker.settings.typing_extensions; @@ -136,6 +136,6 @@ pub(crate) fn class_dict_annotations(checker: &Checker, call: &ExprCall) { .is_some_and(|s| s.value.to_str() == "__annotations__"); if is_first_arg_correct { - checker.report_diagnostic(ClassDictAnnotations { python_version }, call.range()); + checker.report_diagnostic(AnnotationsFromClassDict { python_version }, call.range()); } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 303f16bdfd9fd..d54230f6a1041 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -1,8 +1,8 @@ pub(crate) use ambiguous_unicode_character::*; +pub(crate) use annotations_from_class_dict::*; pub(crate) use assert_with_print_message::*; pub(crate) use assignment_in_assert::*; pub(crate) use asyncio_dangling_task::*; -pub(crate) use class_dict_annotations::*; pub(crate) use class_with_mixed_type_vars::*; pub(crate) use collection_literal_concatenation::*; pub(crate) use dataclass_enum::*; @@ -60,10 +60,10 @@ pub(crate) use useless_if_else::*; pub(crate) use zip_instead_of_pairwise::*; mod ambiguous_unicode_character; +mod annotations_from_class_dict; mod assert_with_print_message; mod assignment_in_assert; mod asyncio_dangling_task; -mod class_dict_annotations; mod class_with_mixed_type_vars; mod collection_literal_concatenation; mod confusables; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py310.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py310.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py310.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py314.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py314.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py314.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py314.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_no_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_no_typing_extensions.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_no_typing_extensions.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_no_typing_extensions.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_with_typing_extensions.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class_dict_annotations_py39_with_typing_extensions.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_with_typing_extensions.snap From c31b5d68b38d9ff7b55e2c5bc28c5c3ed23a65a1 Mon Sep 17 00:00:00 2001 From: Deric Crago Date: Thu, 19 Jun 2025 02:36:36 -0400 Subject: [PATCH 12/12] Rename `AnnotationsFromClassDict` to `AccessAnnotationsFromClassDict` This commit renames the rule from `AnnotationsFromClassDict` to `AccessAnnotationsFromClassDict` to better reflect its expanded scope. The rule has been extended to detect direct subscript access (e.g., `foo.__dict__["__annotations__"]`), in addition to the existing check for `foo.__dict__.get("__annotations__")` method calls. This change provides more comprehensive detection of this discouraged access pattern. --- .../resources/test/fixtures/ruff/RUF063.py | 2 + .../src/checkers/ast/analyze/expression.rs | 7 +- crates/ruff_linter/src/codes.rs | 2 +- crates/ruff_linter/src/rules/ruff/mod.rs | 16 ++--- ... => access_annotations_from_class_dict.rs} | 64 ++++++++++++++++--- .../ruff_linter/src/rules/ruff/rules/mod.rs | 4 +- ...ss_annotations_from_class_dict_py310.snap} | 20 ++++-- ...ss_annotations_from_class_dict_py314.snap} | 20 ++++-- ...class_dict_py39_no_typing_extensions.snap} | 0 ...ass_dict_py39_with_typing_extensions.snap} | 20 ++++-- ruff.schema.json | 1 + 11 files changed, 118 insertions(+), 38 deletions(-) rename crates/ruff_linter/src/rules/ruff/rules/{annotations_from_class_dict.rs => access_annotations_from_class_dict.rs} (68%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__annotations_from_class_dict_py310.snap => ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py310.snap} (66%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__annotations_from_class_dict_py314.snap => ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py314.snap} (65%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_no_typing_extensions.snap => ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_no_typing_extensions.snap} (100%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_with_typing_extensions.snap => ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_with_typing_extensions.snap} (64%) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py index 5743b02278408..7681b5eea127b 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py @@ -4,12 +4,14 @@ foo.__dict__.get("__annotations__") # RUF063 foo.__dict__.get("__annotations__", None) # RUF063 foo.__dict__.get("__annotations__", {}) # RUF063 +foo.__dict__["__annotations__"] # RUF063 # Cases that should NOT trigger the violation foo.__dict__.get("not__annotations__") foo.__dict__.get("not__annotations__", None) foo.__dict__.get("not__annotations__", {}) +foo.__dict__["not__annotations__"] foo.__annotations__ foo.get("__annotations__") foo.get("__annotations__", None) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 9a141b08c1f13..06e7108919cf5 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -179,6 +179,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.enabled(Rule::MissingMaxsplitArg) { pylint::rules::missing_maxsplit_arg(checker, value, slice, expr); } + if checker.enabled(Rule::AccessAnnotationsFromClassDict) { + ruff::rules::access_annotations_from_class_dict_by_key(checker, subscript); + } pandas_vet::rules::subscript(checker, value, expr); } Expr::Tuple(ast::ExprTuple { @@ -1196,8 +1199,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.enabled(Rule::StarmapZip) { ruff::rules::starmap_zip(checker, call); } - if checker.enabled(Rule::AnnotationsFromClassDict) { - ruff::rules::annotations_from_class_dict(checker, call); + if checker.enabled(Rule::AccessAnnotationsFromClassDict) { + ruff::rules::access_annotations_from_class_dict_with_get(checker, call); } if checker.enabled(Rule::LogExceptionOutsideExceptHandler) { flake8_logging::rules::log_exception_outside_except_handler(checker, call); diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index baf14adb8e5b8..af0ef8dfd805f 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1028,7 +1028,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises), - (Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AnnotationsFromClassDict), + (Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), (Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 16340452a0e1d..7904adc446d6b 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -171,13 +171,13 @@ mod tests { } #[test] - fn annotations_from_class_dict_py39_no_typing_extensions() -> Result<()> { + fn access_annotations_from_class_dict_py39_no_typing_extensions() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), &LinterSettings { typing_extensions: false, unresolved_target_version: PythonVersion::PY39.into(), - ..LinterSettings::for_rule(Rule::AnnotationsFromClassDict) + ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) }, )?; assert_messages!(diagnostics); @@ -185,13 +185,13 @@ mod tests { } #[test] - fn annotations_from_class_dict_py39_with_typing_extensions() -> Result<()> { + fn access_annotations_from_class_dict_py39_with_typing_extensions() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), &LinterSettings { typing_extensions: true, unresolved_target_version: PythonVersion::PY39.into(), - ..LinterSettings::for_rule(Rule::AnnotationsFromClassDict) + ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) }, )?; assert_messages!(diagnostics); @@ -199,12 +199,12 @@ mod tests { } #[test] - fn annotations_from_class_dict_py310() -> Result<()> { + fn access_annotations_from_class_dict_py310() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), &LinterSettings { unresolved_target_version: PythonVersion::PY310.into(), - ..LinterSettings::for_rule(Rule::AnnotationsFromClassDict) + ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) }, )?; assert_messages!(diagnostics); @@ -212,12 +212,12 @@ mod tests { } #[test] - fn annotations_from_class_dict_py314() -> Result<()> { + fn access_annotations_from_class_dict_py314() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), &LinterSettings { unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(Rule::AnnotationsFromClassDict) + ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) }, )?; assert_messages!(diagnostics); diff --git a/crates/ruff_linter/src/rules/ruff/rules/annotations_from_class_dict.rs b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs similarity index 68% rename from crates/ruff_linter/src/rules/ruff/rules/annotations_from_class_dict.rs rename to crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs index 9a09dad7e3797..f8961f82a4aa8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/annotations_from_class_dict.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs @@ -1,19 +1,20 @@ use crate::checkers::ast::Checker; use crate::{FixAvailability, Violation}; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{Expr, ExprCall, PythonVersion}; +use ruff_python_ast::{Expr, ExprCall, ExprSubscript, PythonVersion}; use ruff_text_size::Ranged; /// ## What it does -/// Checks for uses of `foo.__dict__.get("__annotations__")` -/// on Python 3.10+ and Python < 3.10 when +/// Checks for uses of `foo.__dict__.get("__annotations__")` or +/// `foo.__dict__["__annotations__"]` on Python 3.10+ and Python < 3.10 when /// [typing-extensions](https://docs.astral.sh/ruff/settings/#lint_typing-extensions) /// is enabled. /// /// ## Why is this bad? /// Starting with Python 3.14, directly accessing `__annotations__` via -/// `foo.__dict__.get("__annotations__")` will only return annotations -/// if the class is defined under `from __future__ import annotations`. +/// `foo.__dict__.get("__annotations__")` or `foo.__dict__["__annotations__"]` +/// will only return annotations if the class is defined under +/// `from __future__ import annotations`. /// /// Therefore, it is better to use dedicated library functions like /// `annotationlib.get_annotations` (Python 3.14+), `inspect.get_annotations` @@ -36,6 +37,8 @@ use ruff_text_size::Ranged; /// /// ```python /// foo.__dict__.get("__annotations__", {}) +/// # or +/// foo.__dict__["__annotations__"] /// ``` /// /// On Python 3.14+, use instead: @@ -71,11 +74,11 @@ use ruff_text_size::Ranged; /// ## References /// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) #[derive(ViolationMetadata)] -pub(crate) struct AnnotationsFromClassDict { +pub(crate) struct AccessAnnotationsFromClassDict { python_version: PythonVersion, } -impl Violation for AnnotationsFromClassDict { +impl Violation for AccessAnnotationsFromClassDict { const FIX_AVAILABILITY: FixAvailability = FixAvailability::None; #[derive_message_formats] @@ -87,12 +90,12 @@ impl Violation for AnnotationsFromClassDict { } else { "typing_extensions.get_annotations" }; - format!("Use `{suggestion}` instead of `__dict__.get('__annotations__')`") + format!("Use `{suggestion}` instead of `__dict__` access") } } /// RUF063 -pub(crate) fn annotations_from_class_dict(checker: &Checker, call: &ExprCall) { +pub(crate) fn access_annotations_from_class_dict_with_get(checker: &Checker, call: &ExprCall) { let python_version = checker.target_version(); let typing_extensions = checker.settings.typing_extensions; @@ -136,6 +139,47 @@ pub(crate) fn annotations_from_class_dict(checker: &Checker, call: &ExprCall) { .is_some_and(|s| s.value.to_str() == "__annotations__"); if is_first_arg_correct { - checker.report_diagnostic(AnnotationsFromClassDict { python_version }, call.range()); + checker.report_diagnostic( + AccessAnnotationsFromClassDict { python_version }, + call.range(), + ); + } +} + +/// RUF063 +pub(crate) fn access_annotations_from_class_dict_by_key( + checker: &Checker, + subscript: &ExprSubscript, +) { + let python_version = checker.target_version(); + let typing_extensions = checker.settings.typing_extensions; + + // Only apply this rule for Python 3.10 and newer unless `typing-extensions` is enabled. + if python_version < PythonVersion::PY310 && !typing_extensions { + return; + } + + // Expected pattern: foo.__dict__["__annotations__"] + + // 1. Check that the slice is a string literal "__annotations__" + if subscript + .slice + .as_string_literal_expr() + .is_none_or(|s| s.value.to_str() != "__annotations__") + { + return; + } + + // 2. Check that the `subscript.value` is `__dict__` + let is_value_correct = subscript + .value + .as_attribute_expr() + .is_some_and(|attr| attr.attr.as_str() == "__dict__"); + + if is_value_correct { + checker.report_diagnostic( + AccessAnnotationsFromClassDict { python_version }, + subscript.range(), + ); } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index d54230f6a1041..8c8ad4e56fd72 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -1,5 +1,5 @@ +pub(crate) use access_annotations_from_class_dict::*; pub(crate) use ambiguous_unicode_character::*; -pub(crate) use annotations_from_class_dict::*; pub(crate) use assert_with_print_message::*; pub(crate) use assignment_in_assert::*; pub(crate) use asyncio_dangling_task::*; @@ -59,8 +59,8 @@ pub(crate) use used_dummy_variable::*; pub(crate) use useless_if_else::*; pub(crate) use zip_instead_of_pairwise::*; +mod access_annotations_from_class_dict; mod ambiguous_unicode_character; -mod annotations_from_class_dict; mod assert_with_print_message; mod assignment_in_assert; mod asyncio_dangling_task; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py310.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py310.snap similarity index 66% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py310.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py310.snap index c719e76b18cac..06274f1af201b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py310.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py310.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF063.py:4:1: RUF063 Use `inspect.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:4:1: RUF063 Use `inspect.get_annotations` instead of `__dict__` access | 2 | # Cases that should trigger the violation 3 | @@ -11,20 +11,30 @@ RUF063.py:4:1: RUF063 Use `inspect.get_annotations` instead of `__dict__.get('__ 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF063.py:5:1: RUF063 Use `inspect.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:5:1: RUF063 Use `inspect.get_annotations` instead of `__dict__` access | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 | -RUF063.py:6:1: RUF063 Use `inspect.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:6:1: RUF063 Use `inspect.get_annotations` instead of `__dict__` access | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 -7 | -8 | # Cases that should NOT trigger the violation +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:7:1: RUF063 Use `inspect.get_annotations` instead of `__dict__` access + | +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +8 | +9 | # Cases that should NOT trigger the violation | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py314.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py314.snap similarity index 65% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py314.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py314.snap index 538e644a92548..0f07c9a8e954d 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py314.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py314.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__` access | 2 | # Cases that should trigger the violation 3 | @@ -11,20 +11,30 @@ RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__.g 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__` access | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 | -RUF063.py:6:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:6:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__` access | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 -7 | -8 | # Cases that should NOT trigger the violation +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:7:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__` access + | +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +8 | +9 | # Cases that should NOT trigger the violation | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_no_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_no_typing_extensions.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_no_typing_extensions.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_no_typing_extensions.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_with_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_with_typing_extensions.snap similarity index 64% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_with_typing_extensions.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_with_typing_extensions.snap index 93ce037aee440..c5ef0419bad28 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__annotations_from_class_dict_py39_with_typing_extensions.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_with_typing_extensions.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF063.py:4:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:4:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__` access | 2 | # Cases that should trigger the violation 3 | @@ -11,20 +11,30 @@ RUF063.py:4:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | -RUF063.py:5:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:5:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__` access | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 | -RUF063.py:6:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__.get('__annotations__')` +RUF063.py:6:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__` access | 4 | foo.__dict__.get("__annotations__") # RUF063 5 | foo.__dict__.get("__annotations__", None) # RUF063 6 | foo.__dict__.get("__annotations__", {}) # RUF063 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 -7 | -8 | # Cases that should NOT trigger the violation +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:7:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__` access + | +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +8 | +9 | # Cases that should NOT trigger the violation | diff --git a/ruff.schema.json b/ruff.schema.json index 571d2e913878c..ee40e83d1b176 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4039,6 +4039,7 @@ "RUF059", "RUF06", "RUF060", + "RUF061", "RUF063", "RUF1", "RUF10",