Skip to content

Commit 4d6b6d1

Browse files
committed
Ignore plugin-generated members when inferring PEP 695 variance
infer_variance walked every member of a class, including methods and attributes synthesized by plugins. Those synthesized members reuse the class's own type in positions that don't reflect how the user uses the type variable, which corrupted the inferred variance: - attrs generates ordering methods (__lt__/__le__/__gt__/__ge__) whose ``other`` parameter is typed as the class's own Self[T], plus an __attrs_attrs__ tuple of the invariant Attribute[T]. This made @attrs.define/@attrs.frozen generic classes invariant (and empty ones contravariant) even when T was used only covariantly. - On Python 3.13+ the dataclass plugin generates __replace__, whose keyword parameters reuse the field types and made otherwise covariant frozen dataclasses invariant. User-written declarations are never flagged plugin_generated, so skipping plugin-generated members during variance inference leaves real fields and methods in control while ignoring synthesized ones. This generalizes the existing __mypy-replace special case. Tests: - testPEP695InferVarianceWithAttrsFrozen (check-python312.test) - testPEP695InferVarianceInFrozenDataclass (check-python313.test) Relates to #17623. Assistant-Model: Claude Code
1 parent 94838b0 commit 4d6b6d1

3 files changed

Lines changed: 64 additions & 0 deletions

File tree

mypy/subtypes.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,6 +2274,18 @@ def infer_variance(info: TypeInfo, i: int) -> bool:
22742274
if member in ("__init__", "__new__", "__mypy-replace"):
22752275
continue
22762276

2277+
# Members synthesized by plugins must not influence variance
2278+
# inference. attrs, for example, generates ordering methods whose
2279+
# "other" parameter is typed as the class's own Self[T], plus an
2280+
# __attrs_attrs__ tuple of (invariant) Attribute[T]; dataclasses
2281+
# generate __replace__. These mention the type variable only because
2282+
# they are derived from the user's own declarations -- and those
2283+
# declarations are not plugin-generated, so they still drive the
2284+
# inferred variance.
2285+
sym = info.get(member)
2286+
if sym is not None and sym.plugin_generated:
2287+
continue
2288+
22772289
if isinstance(self_type, TupleType):
22782290
self_type = mypy.typeops.tuple_fallback(self_type)
22792291
flags = get_member_flags(member, self_type)

test-data/unit/check-python312.test

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,42 @@ inv2: Invariant[int] = Invariant[float]([1]) # E: Incompatible types in assignm
248248
[builtins fixtures/tuple.pyi]
249249
[typing fixtures/typing-full.pyi]
250250

251+
[case testPEP695InferVarianceWithAttrsFrozen]
252+
import attrs
253+
254+
# attrs synthesizes ordering dunders (__lt__/__le__/__gt__/__ge__) whose
255+
# ``other`` parameter is typed as the class's own ``Self[T]``. Those methods
256+
# are plugin-generated and must not drag T into a contravariant position
257+
# during PEP 695 variance inference. A frozen class using T only covariantly
258+
# should be inferred covariant.
259+
@attrs.frozen
260+
class Covariant[T]:
261+
x: T
262+
def get(self) -> T:
263+
return self.x
264+
265+
cov1: Covariant[object] = Covariant[int](1)
266+
cov2: Covariant[int] = Covariant[object](1) # E: Incompatible types in assignment (expression has type "Covariant[object]", variable has type "Covariant[int]")
267+
268+
# A mutable attribute still makes the class invariant.
269+
@attrs.define
270+
class Invariant[T]:
271+
x: T
272+
273+
inv1: Invariant[object] = Invariant[int](1) # E: Incompatible types in assignment (expression has type "Invariant[int]", variable has type "Invariant[object]")
274+
inv2: Invariant[int] = Invariant[object](1) # E: Incompatible types in assignment (expression has type "Invariant[object]", variable has type "Invariant[int]")
275+
276+
# A user-written method with T in a parameter must still be honored: only
277+
# plugin-generated methods are skipped, so this class stays contravariant.
278+
@attrs.frozen
279+
class Contravariant[T]:
280+
def feed(self, x: T) -> None: ...
281+
282+
con1: Contravariant[int] = Contravariant[object]()
283+
con2: Contravariant[object] = Contravariant[int]() # E: Incompatible types in assignment (expression has type "Contravariant[int]", variable has type "Contravariant[object]")
284+
[builtins fixtures/plugin_attrs.pyi]
285+
[typing fixtures/typing-full.pyi]
286+
251287
[case testPEP695InferVarianceCalculateOnDemand]
252288
class Covariant[T]:
253289
def __init__(self) -> None:

test-data/unit/check-python313.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,19 @@ reveal_type(x) # N: Revealed type is "builtins.list[tuple[()]]"
480480
reveal_type(y) # N: Revealed type is "builtins.list[tuple[()]]"
481481
reveal_type(z) # N: Revealed type is "builtins.list[tuple[()]]"
482482
[builtins fixtures/tuple.pyi]
483+
484+
[case testPEP695InferVarianceInFrozenDataclass]
485+
# On Python 3.13+ the dataclass plugin synthesizes a __replace__ method whose
486+
# keyword parameters reuse the field types. Being plugin-generated, it must not
487+
# drag the type variable into a contravariant position and make an otherwise
488+
# covariant frozen dataclass invariant.
489+
from dataclasses import dataclass
490+
491+
@dataclass(frozen=True)
492+
class Covariant[T]:
493+
x: T
494+
495+
cov1: Covariant[float] = Covariant[int](1)
496+
cov2: Covariant[int] = Covariant[float](1) # E: Incompatible types in assignment (expression has type "Covariant[float]", variable has type "Covariant[int]")
497+
[builtins fixtures/tuple.pyi]
498+
[typing fixtures/typing-full.pyi]

0 commit comments

Comments
 (0)