Skip to content

Commit 6fcc3e4

Browse files
committed
always exempt __replace__
1 parent ef2cba9 commit 6fcc3e4

2 files changed

Lines changed: 1 addition & 49 deletions

File tree

mypy/checker.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3133,21 +3133,11 @@ class C(B, A[int]): ... # this is unsafe because...
31333133
x: A[int] = C()
31343134
x.foo # ...runtime type is (str) -> None, while static type is (int) -> None
31353135
"""
3136-
if name in ("__init__", "__new__", "__init_subclass__"):
3136+
if name in {"__init__", "__new__", "__init_subclass__", "__replace__"}:
31373137
# __init__ and friends can be incompatible -- it's a special case.
31383138
return
31393139
first = base1.names[name]
31403140
second = base2.names[name]
3141-
if name == "__replace__" and first.plugin_generated and second.plugin_generated:
3142-
# Plugin-synthesized __replace__ methods (e.g. those added by the
3143-
# @dataclass plugin on Python 3.13+ to support copy.replace())
3144-
# return Self and are regenerated fresh for every concrete
3145-
# subclass, so they can safely differ across unrelated bases --
3146-
# same reasoning as __init__ and friends above. We only skip this
3147-
# for methods a plugin generated, not ones the user wrote by
3148-
# hand, so real incompatible __replace__ overrides are still
3149-
# caught.
3150-
return
31513141
# Specify current_class explicitly as this function is called after leaving the class.
31523142
first_type, _ = self.node_type_from_base(name, base1, ctx, current_class=ctx)
31533143
second_type, _ = self.node_type_from_base(name, base2, ctx, current_class=ctx)

test-data/unit/check-dataclasses.test

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,32 +2609,8 @@ class Y(X):
26092609
[builtins fixtures/tuple.pyi]
26102610

26112611

2612-
[case testDunderReplaceDoesNotBlockAdHocIntersectionNarrowing]
2613-
# https://github.com/python/mypy/issues/21635
2614-
# flags: --python-version 3.13
2615-
from dataclasses import dataclass
2616-
2617-
@dataclass
2618-
class A: ...
2619-
@dataclass
2620-
class M: ...
2621-
@dataclass
2622-
class B(A): ...
2623-
@dataclass
2624-
class C(M, A): ...
2625-
2626-
alist: list[type[A]] = [B, C]
2627-
mlist: list[type[M]] = [cls for cls in alist if issubclass(cls, M)]
2628-
reveal_type(mlist) # N: Revealed type is "builtins.list[type[__main__.M]]"
2629-
[builtins fixtures/isinstancelist.pyi]
2630-
26312612
[case testDunderReplaceDoesNotBlockPlainIssubclassNarrowing]
26322613
# https://github.com/python/mypy/issues/21635
2633-
# Same underlying bug as testDunderReplaceDoesNotBlockAdHocIntersectionNarrowing,
2634-
# but without a list/comprehension: plain issubclass() narrowing in an
2635-
# ordinary if-statement hit the exact same false "impossible intersection"
2636-
# and silently marked the branch as unreachable instead of narrowing (no
2637-
# reveal_type note, and the type error below went unreported).
26382614
# flags: --python-version 3.13
26392615
from dataclasses import dataclass
26402616

@@ -2653,20 +2629,6 @@ if issubclass(cls, M):
26532629
n: int = 'foo' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
26542630
[builtins fixtures/isinstancelist.pyi]
26552631

2656-
[case testDunderReplaceHandwrittenStillCheckedForCompatibility]
2657-
# flags: --python-version 3.13
2658-
class A:
2659-
def __replace__(self) -> "A":
2660-
return A()
2661-
2662-
class M:
2663-
def __replace__(self) -> "M":
2664-
return M()
2665-
2666-
class C(M, A): # E: Definition of "__replace__" in base class "M" is incompatible with definition in base class "A"
2667-
pass
2668-
[builtins fixtures/tuple.pyi]
2669-
26702632

26712633
[case testFrozenWithFinal]
26722634
from dataclasses import dataclass

0 commit comments

Comments
 (0)