From 2b8617b25f74f66f378f5918624e95e383935c73 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 18 Sep 2021 12:43:55 +0300 Subject: [PATCH 1/3] Fixes mypy crash on protocol with contravariant var --- mypy/constraints.py | 5 +++-- test-data/unit/check-protocols.test | 23 ++++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index d8dad95a34306..82c0c248cbcfa 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -388,7 +388,7 @@ def visit_instance(self, template: Instance) -> List[Constraint]: if (template.type.is_protocol and self.direction == SUPERTYPE_OF and # We avoid infinite recursion for structural subtypes by checking # whether this type already appeared in the inference chain. - # This is a conservative way break the inference cycles. + # This is a conservative way to break the inference cycles. # It never produces any "false" constraints but gives up soon # on purely structural inference cycles, see #3829. # Note that we use is_protocol_implementation instead of is_subtype @@ -444,7 +444,8 @@ def infer_constraints_from_protocol_members(self, res: List[Constraint], for member in protocol.type.protocol_members: inst = mypy.subtypes.find_member(member, instance, subtype) temp = mypy.subtypes.find_member(member, template, subtype) - assert inst is not None and temp is not None + if inst is None or temp is None: + continue # See #11020 # The above is safe since at this point we know that 'instance' is a subtype # of (erased) 'template', therefore it defines all protocol members res.extend(infer_constraints(temp, inst, self.direction)) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index f13d2bc597da9..52c51cfd04562 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -486,6 +486,27 @@ class P2(Protocol[T_co]): # E: Covariant type variable "T_co" used in protocol w lst: List[T_co] [builtins fixtures/list.pyi] + +[case testProtocolConstraintsUnsolvableWithContravariant] +# https://github.com/python/mypy/issues/11020 +from typing import overload, Protocol, TypeVar + +I = TypeVar('I') +V_contra = TypeVar('V_contra', contravariant=True) + +class C(Protocol[I]): # E: Invariant type variable "I" used in protocol where covariant one is expected + def __abs__(self: 'C[V_contra]') -> 'C[V_contra]': + ... + + @overload + def f(self: 'C', q: int) -> int: + ... + @overload + def f(self: 'C[float]', q: float) -> 'C[float]': + ... +[builtins fixtures/bool.pyi] + + [case testProtocolVarianceWithUnusedVariable] from typing import Protocol, TypeVar T = TypeVar('T') @@ -2124,7 +2145,7 @@ class B(Protocol): def execute(self, stmt: Any, *args: Any, **kwargs: Any) -> None: ... def cool(self) -> None: ... -def func1(arg: A) -> None: ... +def func1(arg: A) -> None: ... def func2(arg: Optional[A]) -> None: ... x: B From 957922b73e961af9155b172fac8cfad8c1e0e0c8 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 20 Sep 2021 15:14:20 +0300 Subject: [PATCH 2/3] Fixes how `infer_constraints_from_protocol_members` works. Here's what was wrong: 1. When checking if some type is a subtype of a protocol, we iterate over all its members 2. When we are inside `is_protocol_implementation` we set `.assuming` or `.assuming_proper` attributes and continue to check the other members 3. While checking methods with annotated `self` type with a protocol itself, we recurse into `is_protocol_implementation` once again 4. It always returns `True` in this case, because `assuming` is set in the context above 5. Because `is_protocol_implementation` is set to `True`, we dive into `infer_constraints_from_protocol_members` with a type, which is not really a protocol implementation 6. Sanity check breaks! Solution: return empty constraints instead of raising `AssertionError` --- mypy/constraints.py | 17 ++++++++++------- test-data/unit/check-protocols.test | 24 +++++++++++++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 82c0c248cbcfa..7fc28eb35c8f4 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -398,8 +398,8 @@ def visit_instance(self, template: Instance) -> List[Constraint]: for t in template.type.inferring) and mypy.subtypes.is_protocol_implementation(instance, erased)): template.type.inferring.append(template) - self.infer_constraints_from_protocol_members(res, instance, template, - original_actual, template) + res.extend(self.infer_constraints_from_protocol_members( + instance, template, original_actual, template)) template.type.inferring.pop() return res elif (instance.type.is_protocol and self.direction == SUBTYPE_OF and @@ -408,8 +408,8 @@ def visit_instance(self, template: Instance) -> List[Constraint]: for i in instance.type.inferring) and mypy.subtypes.is_protocol_implementation(erased, instance)): instance.type.inferring.append(instance) - self.infer_constraints_from_protocol_members(res, instance, template, - template, instance) + res.extend(self.infer_constraints_from_protocol_members( + instance, template, template, instance)) instance.type.inferring.pop() return res if isinstance(actual, AnyType): @@ -432,20 +432,22 @@ def visit_instance(self, template: Instance) -> List[Constraint]: else: return [] - def infer_constraints_from_protocol_members(self, res: List[Constraint], + def infer_constraints_from_protocol_members(self, instance: Instance, template: Instance, - subtype: Type, protocol: Instance) -> None: + subtype: Type, protocol: Instance, + ) -> List[Constraint]: """Infer constraints for situations where either 'template' or 'instance' is a protocol. The 'protocol' is the one of two that is an instance of protocol type, 'subtype' is the type used to bind self during inference. Currently, we just infer constrains for every protocol member type (both ways for settable members). """ + res = [] for member in protocol.type.protocol_members: inst = mypy.subtypes.find_member(member, instance, subtype) temp = mypy.subtypes.find_member(member, template, subtype) if inst is None or temp is None: - continue # See #11020 + return [] # See #11020 # The above is safe since at this point we know that 'instance' is a subtype # of (erased) 'template', therefore it defines all protocol members res.extend(infer_constraints(temp, inst, self.direction)) @@ -453,6 +455,7 @@ def infer_constraints_from_protocol_members(self, res: List[Constraint], mypy.subtypes.get_member_flags(member, protocol.type)): # Settable members are invariant, add opposite constraints res.extend(infer_constraints(temp, inst, neg_op(self.direction))) + return res def visit_callable_type(self, template: CallableType) -> List[Constraint]: if isinstance(self.actual, CallableType): diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 52c51cfd04562..c279e3d1a751c 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -487,14 +487,14 @@ class P2(Protocol[T_co]): # E: Covariant type variable "T_co" used in protocol w [builtins fixtures/list.pyi] -[case testProtocolConstraintsUnsolvableWithContravariant] +[case testProtocolConstraintsUnsolvableWithSelfAnnotation1] # https://github.com/python/mypy/issues/11020 from typing import overload, Protocol, TypeVar -I = TypeVar('I') +I = TypeVar('I', covariant=True) V_contra = TypeVar('V_contra', contravariant=True) -class C(Protocol[I]): # E: Invariant type variable "I" used in protocol where covariant one is expected +class C(Protocol[I]): def __abs__(self: 'C[V_contra]') -> 'C[V_contra]': ... @@ -507,6 +507,24 @@ class C(Protocol[I]): # E: Invariant type variable "I" used in protocol where c [builtins fixtures/bool.pyi] +[case testProtocolConstraintsUnsolvableWithSelfAnnotation2] +# https://github.com/python/mypy/issues/11020 +from typing import Protocol, TypeVar + +I = TypeVar('I', covariant=True) +V = TypeVar('V') + +class C(Protocol[I]): + def g(self: 'C[V]') -> 'C[V]': + ... + +class D: + pass + +x: C = D() # E: Incompatible types in assignment (expression has type "D", variable has type "C[Any]") +[builtins fixtures/bool.pyi] + + [case testProtocolVarianceWithUnusedVariable] from typing import Protocol, TypeVar T = TypeVar('T') From e5f80ca00695d996ae6f54715670a0981311a807 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 20 Sep 2021 15:31:23 +0300 Subject: [PATCH 3/3] Adds positive case --- test-data/unit/check-protocols.test | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index c279e3d1a751c..8b505108f89f6 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -525,6 +525,25 @@ x: C = D() # E: Incompatible types in assignment (expression has type "D", vari [builtins fixtures/bool.pyi] +[case testProtocolConstraintsUnsolvableWithSelfAnnotation3] +# https://github.com/python/mypy/issues/11020 +from typing import Protocol, TypeVar + +I = TypeVar('I', covariant=True) +V = TypeVar('V') + +class C(Protocol[I]): + def g(self: 'C[V]') -> 'C[V]': + ... + +class D: + def g(self) -> D: + ... + +x: C = D() +[builtins fixtures/bool.pyi] + + [case testProtocolVarianceWithUnusedVariable] from typing import Protocol, TypeVar T = TypeVar('T')