diff --git a/mypy/checker.py b/mypy/checker.py index d13b927b28f2..1316eaee6209 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3138,6 +3138,16 @@ class C(B, A[int]): ... # this is unsafe because... return first = base1.names[name] second = base2.names[name] + if name == "__replace__" and first.plugin_generated and second.plugin_generated: + # Plugin-synthesized __replace__ methods (e.g. those added by the + # @dataclass plugin on Python 3.13+ to support copy.replace()) + # return Self and are regenerated fresh for every concrete + # subclass, so they can safely differ across unrelated bases -- + # same reasoning as __init__ and friends above. We only skip this + # for methods a plugin generated, not ones the user wrote by + # hand, so real incompatible __replace__ overrides are still + # caught. + return # Specify current_class explicitly as this function is called after leaving the class. first_type, _ = self.node_type_from_base(name, base1, ctx, current_class=ctx) second_type, _ = self.node_type_from_base(name, base2, ctx, current_class=ctx) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 54b3afadc8b3..28d824bae20d 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2609,6 +2609,65 @@ class Y(X): [builtins fixtures/tuple.pyi] +[case testDunderReplaceDoesNotBlockAdHocIntersectionNarrowing] +# https://github.com/python/mypy/issues/21635 +# flags: --python-version 3.13 +from dataclasses import dataclass + +@dataclass +class A: ... +@dataclass +class M: ... +@dataclass +class B(A): ... +@dataclass +class C(M, A): ... + +alist: list[type[A]] = [B, C] +mlist: list[type[M]] = [cls for cls in alist if issubclass(cls, M)] +reveal_type(mlist) # N: Revealed type is "builtins.list[type[__main__.M]]" +[builtins fixtures/isinstancelist.pyi] + +[case testDunderReplaceDoesNotBlockPlainIssubclassNarrowing] +# https://github.com/python/mypy/issues/21635 +# Same underlying bug as testDunderReplaceDoesNotBlockAdHocIntersectionNarrowing, +# but without a list/comprehension: plain issubclass() narrowing in an +# ordinary if-statement hit the exact same false "impossible intersection" +# and silently marked the branch as unreachable instead of narrowing (no +# reveal_type note, and the type error below went unreported). +# flags: --python-version 3.13 +from dataclasses import dataclass + +@dataclass +class A: ... +@dataclass +class M: ... +@dataclass +class B(A): ... +@dataclass +class C(M, A): ... + +cls: type[A] = C +if issubclass(cls, M): + reveal_type(cls) # N: Revealed type is "type[__main__.]" + n: int = 'foo' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +[builtins fixtures/isinstancelist.pyi] + +[case testDunderReplaceHandwrittenStillCheckedForCompatibility] +# flags: --python-version 3.13 +class A: + def __replace__(self) -> "A": + return A() + +class M: + def __replace__(self) -> "M": + return M() + +class C(M, A): # E: Definition of "__replace__" in base class "M" is incompatible with definition in base class "A" + pass +[builtins fixtures/tuple.pyi] + + [case testFrozenWithFinal] from dataclasses import dataclass from typing import Final