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.
This commit is contained in:
Charlie Marsh
2026-04-29 10:16:00 -04:00
committed by GitHub
parent 1b931ba658
commit ea4b40641a
4 changed files with 18 additions and 9 deletions
@@ -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
@@ -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: ...
@@ -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| {
@@ -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