From ea4b40641aa166c98094db6455b316fd69e2a333 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 29 Apr 2026 10:16:00 -0400 Subject: [PATCH] Restrict PYI034 for in-place operations to enclosing class (#24511) ## Summary In `.py` and `.pyi` files, we now only flag cases in which the return type is the enclosing class, like: ```python class A: def __iadd__(self) -> A: return self ``` As opposed to: ```python class A: def __iadd__(self) -> int: return self ``` Closes https://github.com/astral-sh/ruff/issues/24462. --- .../resources/test/fixtures/flake8_pyi/PYI034.py | 5 +++++ .../resources/test/fixtures/flake8_pyi/PYI034.pyi | 4 ++++ .../rules/flake8_pyi/rules/non_self_return_type.rs | 12 +++--------- ...__rules__flake8_pyi__tests__PYI034_PYI034.py.snap | 6 ++++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py index 37ad3d3706..9e8dbe09ba 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py @@ -392,3 +392,8 @@ class UsesStringizedForwardReferences: def __enter__(self) -> "UsesStringizedForwardReferences": ... # PYI034 async def __aenter__(self) -> "UsesStringizedForwardReferences": ... # PYI034 def __iadd__(self, other) -> "UsesStringizedForwardReferences": ... # PYI034 + + +class InPlaceOperationReturningOtherTypeAtRuntime: + def __iadd__(self, other: int) -> int: + return 1 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi index b22f76798b..b7e31cbf86 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi @@ -277,3 +277,7 @@ class MetaclassInWhichSelfCannotBeUsed7(django.db.models.base.ModelBase): class MetaclassInWhichSelfCannotBeUsed8(django.db.models.base.ModelBase): def __new__(cls, name: builtins.str, bases: tuple, attributes: dict, /, **kw) -> MetaclassInWhichSelfCannotBeUsed8: ... + + +class StubInPlaceOperationReturningOtherType: + def __iadd__(self, other: int) -> int: ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index f9778f4e2f..77d9d049a8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -42,7 +42,8 @@ use ruff_text_size::Ranged; /// Specifically, this check enforces that the return type of the following /// methods is `Self`: /// -/// 1. In-place binary-operation dunder methods, like `__iadd__`, `__imul__`, etc. +/// 1. In-place binary-operation dunder methods, like `__iadd__`, `__imul__`, etc., +/// if those methods return the class name. /// 1. `__new__`, `__enter__`, and `__aenter__`, if those methods return the /// class name. /// 1. `__iter__` methods that return `Iterator`, despite the class inheriting @@ -192,7 +193,7 @@ pub(crate) fn non_self_return_type( // In-place methods that are expected to return `Self`. if is_inplace_bin_op(name) { - if !is_self(returns, checker) { + if is_name_or_stringized_name(returns, &class_def.name, checker) { add_diagnostic(checker, stmt, returns, class_def, name); } return; @@ -330,13 +331,6 @@ fn is_name_or_stringized_name(expr: &ast::Expr, name: &str, checker: &Checker) - checker.match_maybe_stringized_annotation(expr, |expr| is_name(expr, name)) } -/// Return `true` if the given expression resolves to `typing.Self`. -fn is_self(expr: &ast::Expr, checker: &Checker) -> bool { - checker.match_maybe_stringized_annotation(expr, |expr| { - checker.semantic().match_typing_expr(expr, "Self") - }) -} - /// Return `true` if the given class extends `collections.abc.Iterator`. fn subclasses_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { analyze::class::any_qualified_base_class(class_def, semantic, |qualified_name| { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap index 3730675d45..28fe67a39d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap @@ -511,6 +511,7 @@ help: Use `Self` as return type 392 + def __enter__(self) -> typing.Self: ... # PYI034 393 | async def __aenter__(self) -> "UsesStringizedForwardReferences": ... # PYI034 394 | def __iadd__(self, other) -> "UsesStringizedForwardReferences": ... # PYI034 +395 | note: This is an unsafe fix and may change runtime behavior PYI034 [*] `__aenter__` methods in classes like `UsesStringizedForwardReferences` usually return `self` at runtime @@ -529,6 +530,8 @@ help: Use `Self` as return type - async def __aenter__(self) -> "UsesStringizedForwardReferences": ... # PYI034 393 + async def __aenter__(self) -> typing.Self: ... # PYI034 394 | def __iadd__(self, other) -> "UsesStringizedForwardReferences": ... # PYI034 +395 | +396 | note: This is an unsafe fix and may change runtime behavior PYI034 [*] `__iadd__` methods in classes like `UsesStringizedForwardReferences` usually return `self` at runtime @@ -545,4 +548,7 @@ help: Use `Self` as return type 393 | async def __aenter__(self) -> "UsesStringizedForwardReferences": ... # PYI034 - def __iadd__(self, other) -> "UsesStringizedForwardReferences": ... # PYI034 394 + def __iadd__(self, other) -> typing.Self: ... # PYI034 +395 | +396 | +397 | class InPlaceOperationReturningOtherTypeAtRuntime: note: This is an unsafe fix and may change runtime behavior