Skip to content

Commit a4c32d1

Browse files
authored
Fix unsound variance (#13714)
Fixes #13713, fixes #736, fixes #8611
1 parent 176b052 commit a4c32d1

File tree

4 files changed

+39
-3
lines changed

4 files changed

+39
-3
lines changed

mypy/checker.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,24 @@ def visit_class_def(self, defn: ClassDef) -> None:
20822082
self.allow_abstract_call = old_allow_abstract_call
20832083
# TODO: Apply the sig to the actual TypeInfo so we can handle decorators
20842084
# that completely swap out the type. (e.g. Callable[[Type[A]], Type[B]])
2085+
if typ.defn.type_vars:
2086+
for base_inst in typ.bases:
2087+
for base_tvar, base_decl_tvar in zip(
2088+
base_inst.args, base_inst.type.defn.type_vars
2089+
):
2090+
if (
2091+
isinstance(base_tvar, TypeVarType)
2092+
and base_tvar.variance != INVARIANT
2093+
and isinstance(base_decl_tvar, TypeVarType)
2094+
and base_decl_tvar.variance != base_tvar.variance
2095+
):
2096+
self.fail(
2097+
f'Variance of TypeVar "{base_tvar.name}" incompatible '
2098+
"with variance in parent type",
2099+
context=defn,
2100+
code=codes.TYPE_VAR,
2101+
)
2102+
20852103
if typ.is_protocol and typ.defn.type_vars:
20862104
self.check_protocol_variance(defn)
20872105
if not defn.has_incompatible_baseclass and defn.info.is_enum:

mypy/typeshed/stdlib/_typeshed/__init__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class SupportsKeysAndGetItem(Protocol[_KT, _VT_co]):
118118
def __getitem__(self, __k: _KT) -> _VT_co: ...
119119

120120
# stable
121-
class SupportsGetItem(Container[_KT_contra], Protocol[_KT_contra, _VT_co]):
121+
class SupportsGetItem(Container[_KT_contra], Protocol[_KT_contra, _VT_co]): # type: ignore
122122
def __getitem__(self, __k: _KT_contra) -> _VT_co: ...
123123

124124
# stable

test-data/unit/check-generic-subtyping.test

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,3 +1033,21 @@ x2: X2[str, int]
10331033
reveal_type(iter(x2)) # N: Revealed type is "typing.Iterator[builtins.int]"
10341034
reveal_type([*x2]) # N: Revealed type is "builtins.list[builtins.int]"
10351035
[builtins fixtures/dict.pyi]
1036+
1037+
[case testIncompatibleVariance]
1038+
from typing import TypeVar, Generic
1039+
T = TypeVar('T')
1040+
T_co = TypeVar('T_co', covariant=True)
1041+
T_contra = TypeVar('T_contra', contravariant=True)
1042+
1043+
class A(Generic[T_co]): ...
1044+
class B(A[T_contra], Generic[T_contra]): ... # E: Variance of TypeVar "T_contra" incompatible with variance in parent type
1045+
1046+
class C(Generic[T_contra]): ...
1047+
class D(C[T_co], Generic[T_co]): ... # E: Variance of TypeVar "T_co" incompatible with variance in parent type
1048+
1049+
class E(Generic[T]): ...
1050+
class F(E[T_co], Generic[T_co]): ... # E: Variance of TypeVar "T_co" incompatible with variance in parent type
1051+
1052+
class G(Generic[T]): ...
1053+
class H(G[T_contra], Generic[T_contra]): ... # E: Variance of TypeVar "T_contra" incompatible with variance in parent type

test-data/unit/fixtures/typing-full.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ class SupportsAbs(Protocol[T_co]):
160160
def runtime_checkable(cls: T) -> T:
161161
return cls
162162

163-
class ContextManager(Generic[T]):
164-
def __enter__(self) -> T: pass
163+
class ContextManager(Generic[T_co]):
164+
def __enter__(self) -> T_co: pass
165165
# Use Any because not all the precise types are in the fixtures.
166166
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: pass
167167

0 commit comments

Comments
 (0)