diff --git a/mypy/subtypes.py b/mypy/subtypes.py index df040dcb1311..c76b3569fdd4 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2024,6 +2024,16 @@ def infer_variance(info: TypeInfo, i: int) -> bool: typ = find_member(member, self_type, self_type) if typ: + # It's okay for a method in a generic class with a contravariant type + # variable to return a generic instance of the class, if it doesn't involve + # variance (i.e. values of type variables are propagated). Our normal rules + # would disallow this. Replace such return types with 'Any' to allow this. + # + # This could probably be more lenient (e.g. allow self type be nested, don't + # require all type arguments to be identical to self_type), but this will + # hopefully cover the vast majority of such cases, including Self. + typ = erase_return_self_types(typ, self_type) + typ2 = expand_type(typ, {tvar.id: object_type}) if not is_subtype(typ, typ2): co = False @@ -2066,3 +2076,20 @@ def infer_class_variances(info: TypeInfo) -> bool: if not infer_variance(info, i): success = False return success + + +def erase_return_self_types(typ: Type, self_type: Instance) -> Type: + """If a typ is function-like and returns self_type, replace return type with Any.""" + proper_type = get_proper_type(typ) + if isinstance(proper_type, CallableType): + ret = get_proper_type(proper_type.ret_type) + if isinstance(ret, Instance) and ret == self_type: + return proper_type.copy_modified(ret_type=AnyType(TypeOfAny.implementation_artifact)) + elif isinstance(proper_type, Overloaded): + return Overloaded( + [ + cast(CallableType, erase_return_self_types(it, self_type)) + for it in proper_type.items + ] + ) + return typ diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 2f0b912c439e..085cc052705d 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -213,7 +213,7 @@ b: Invariant[int] if int(): a = b # E: Incompatible types in assignment (expression has type "Invariant[int]", variable has type "Invariant[object]") if int(): - b = a # E: Incompatible types in assignment (expression has type "Invariant[object]", variable has type "Invariant[int]") + b = a c: Covariant[object] d: Covariant[int] @@ -393,6 +393,62 @@ inv3_1: Invariant3[float] = Invariant3[int](1) # E: Incompatible types in assig 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 testPEP695InferVarianceWithInheritedSelf] +from typing import overload, Self, TypeVar, Generic + +T = TypeVar("T") +S = TypeVar("S") + +class C(Generic[T]): + def f(self, x: T) -> Self: ... + def g(self) -> T: ... + +class D[T1, T2](C[T1]): + def m(self, x: T2) -> None: ... + +a1: D[int, int] = D[int, object]() +a2: D[int, object] = D[int, int]() # E: Incompatible types in assignment (expression has type "D[int, int]", variable has type "D[int, object]") +a3: D[int, int] = D[object, object]() # E: Incompatible types in assignment (expression has type "D[object, object]", variable has type "D[int, int]") +a4: D[object, int] = D[int, object]() # E: Incompatible types in assignment (expression has type "D[int, object]", variable has type "D[object, int]") + +[case testPEP695InferVarianceWithReturnSelf] +from typing import Self, overload + +class Cov[T]: + def f(self) -> Self: ... + +a1: Cov[int] = Cov[float]() # E: Incompatible types in assignment (expression has type "Cov[float]", variable has type "Cov[int]") +a2: Cov[float] = Cov[int]() + +class Contra[T]: + def f(self) -> Self: ... + def g(self, x: T) -> None: ... + +b1: Contra[int] = Contra[float]() +b2: Contra[float] = Contra[int]() # E: Incompatible types in assignment (expression has type "Contra[int]", variable has type "Contra[float]") + +class Cov2[T]: + @overload + def f(self, x): ... + @overload + def f(self) -> Self: ... + def f(self, x=None): ... + +c1: Cov2[int] = Cov2[float]() # E: Incompatible types in assignment (expression has type "Cov2[float]", variable has type "Cov2[int]") +c2: Cov2[float] = Cov2[int]() + +class Contra2[T]: + @overload + def f(self, x): ... + @overload + def f(self) -> Self: ... + def f(self, x=None): ... + + def g(self, x: T) -> None: ... + +d1: Contra2[int] = Contra2[float]() +d2: Contra2[float] = Contra2[int]() # E: Incompatible types in assignment (expression has type "Contra2[int]", variable has type "Contra2[float]") + [case testPEP695InheritInvariant] class Invariant[T]: x: T diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 26fc419030bf..89f01bff963e 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -2196,3 +2196,23 @@ type K4 = None | B[int] type L1 = Never type L2 = list[Never] + +[case testPEP695VarianceInferenceSpecialCaseWithTypeshed] +# flags: --python-version=3.12 +class C1[T1, T2](list[T1]): + def m(self, a: T2) -> None: ... + +def func1(p: C1[int, object]): + x: C1[int, int] = p + +class C2[T1, T2, T3](dict[T2, T3]): + def m(self, a: T1) -> None: ... + +def func2(p: C2[object, int, int]): + x: C2[int, int, int] = p + +class C3[T1, T2](tuple[T1, ...]): + def m(self, a: T2) -> None: ... + +def func3(p: C3[int, object]): + x: C3[int, int] = p