diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 649cbae4c831..6e2366c4e0df 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2006,16 +2006,22 @@ def infer_variance(info: TypeInfo, i: int) -> bool: for member in all_non_object_members(info): if member in ("__init__", "__new__"): continue - node = info[member].node - if isinstance(node, Var) and node.type is None: - tv.variance = VARIANCE_NOT_READY - return False + if isinstance(self_type, TupleType): self_type = mypy.typeops.tuple_fallback(self_type) - flags = get_member_flags(member, self_type) - typ = find_member(member, self_type, self_type) settable = IS_SETTABLE in flags + + node = info[member].node + if isinstance(node, Var): + if node.type is None: + tv.variance = VARIANCE_NOT_READY + return False + if has_underscore_prefix(member): + # Special case to avoid false positives (and to pass conformance tests) + settable = False + + typ = find_member(member, self_type, self_type) if typ: typ2 = expand_type(typ, {tvar.id: object_type}) if not is_subtype(typ, typ2): @@ -2036,6 +2042,10 @@ def infer_variance(info: TypeInfo, i: int) -> bool: return True +def has_underscore_prefix(name: str) -> bool: + return name.startswith("_") and not (name.startswith("__") and name.endswith("__")) + + def infer_class_variances(info: TypeInfo) -> bool: if not info.defn.type_args: return True diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 0b3055212d20..9dc52d2c07b0 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -342,6 +342,69 @@ class Invariant[T]: reveal_type(c(a1, a2)) # N: Revealed type is "Never" +[case testPEP695InferVarianceUnderscorePrefix] +# flags: --enable-incomplete-feature=NewGenericSyntax + +class Covariant1[T]: + def __init__(self, x: T) -> None: + self._x = x + + @property + def x(self) -> T: + return self._x + +co1_1: Covariant1[float] = Covariant1[int](1) +co1_2: Covariant1[int] = Covariant1[float](1) # E: Incompatible types in assignment (expression has type "Covariant1[float]", variable has type "Covariant1[int]") + +class Covariant2[T]: + def __init__(self, x: T) -> None: + self.__foo_bar = x + + @property + def x(self) -> T: + return self.__foo_bar + +co2_1: Covariant2[float] = Covariant2[int](1) +co2_2: Covariant2[int] = Covariant2[float](1) # E: Incompatible types in assignment (expression has type "Covariant2[float]", variable has type "Covariant2[int]") + +class Invariant1[T]: + def __init__(self, x: T) -> None: + self._x = x + + # Methods behave differently from attributes + def _f(self, x: T) -> None: ... + + @property + def x(self) -> T: + return self._x + +inv1_1: Invariant1[float] = Invariant1[int](1) # E: Incompatible types in assignment (expression has type "Invariant1[int]", variable has type "Invariant1[float]") +inv1_2: Invariant1[int] = Invariant1[float](1) # E: Incompatible types in assignment (expression has type "Invariant1[float]", variable has type "Invariant1[int]") + +class Invariant2[T]: + def __init__(self, x: T) -> None: + # Dunders are special + self.__x__ = x + + @property + def x(self) -> T: + return self.__x__ + +inv2_1: Invariant2[float] = Invariant2[int](1) # E: Incompatible types in assignment (expression has type "Invariant2[int]", variable has type "Invariant2[float]") +inv2_2: Invariant2[int] = Invariant2[float](1) # E: Incompatible types in assignment (expression has type "Invariant2[float]", variable has type "Invariant2[int]") + +class Invariant3[T]: + def __init__(self, x: T) -> None: + self._x = Invariant1(x) + + @property + def x(self) -> T: + return self._x._x + +inv3_1: Invariant3[float] = Invariant3[int](1) # E: Incompatible types in assignment (expression has type "Invariant3[int]", variable has type "Invariant3[float]") +inv3_2: Invariant3[int] = Invariant3[float](1) # E: Incompatible types in assignment (expression has type "Invariant3[float]", variable has type "Invariant3[int]") +[builtins fixtures/property.pyi] + [case testPEP695InheritInvariant] # flags: --enable-incomplete-feature=NewGenericSyntax