diff --git a/mypy/constraints.py b/mypy/constraints.py index 26986fa1975a..73e674e906c7 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -22,6 +22,7 @@ TypeInfo, ) from mypy.types import ( + MAX_PROTOCOL_DEPTH, TUPLE_LIKE_INSTANCE_NAMES, AnyType, CallableType, @@ -750,7 +751,9 @@ def visit_instance(self, template: Instance) -> list[Constraint]: if isinstance(actual, (CallableType, Overloaded)) and template.type.is_protocol: if "__call__" in template.type.protocol_members: # Special case: a generic callback protocol - if not any(template == t for t in template.type.inferring): + if len(template.type.inferring) < MAX_PROTOCOL_DEPTH and not any( + template == t for t in template.type.inferring + ): template.type.inferring.append(template) call = mypy.subtypes.find_member( "__call__", template, actual, is_operator=True @@ -944,6 +947,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]: if ( template.type.is_protocol and self.direction == SUPERTYPE_OF + and len(template.type.inferring) < MAX_PROTOCOL_DEPTH and # We avoid infinite recursion for structural subtypes by checking # whether this type already appeared in the inference chain. @@ -967,6 +971,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]: elif ( instance.type.is_protocol and self.direction == SUBTYPE_OF + and len(instance.type.inferring) < MAX_PROTOCOL_DEPTH and # We avoid infinite recursion for structural subtypes also here. not any(instance == i for i in reversed(instance.type.inferring)) @@ -1013,6 +1018,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]: if ( template.type.is_protocol and self.direction == SUPERTYPE_OF + and len(template.type.inferring) < MAX_PROTOCOL_DEPTH and not any(template == t for t in reversed(template.type.inferring)) and mypy.subtypes.is_protocol_implementation(instance, erased, skip=["__call__"]) ): diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a2f2929cbecd..68e074616824 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -37,6 +37,7 @@ from mypy.options import Options from mypy.state import state from mypy.types import ( + MAX_PROTOCOL_DEPTH, MYPYC_NATIVE_INT_NAMES, TUPLE_LIKE_INSTANCE_NAMES, TYPED_NAMEDTUPLE_NAMES, @@ -1309,6 +1310,8 @@ def f(self) -> A: ... if not members_right.issubset(members_left): return False assuming = right.type.assuming_proper if proper_subtype else right.type.assuming + if len(assuming) > MAX_PROTOCOL_DEPTH: + return True for l, r in reversed(assuming): if l == left and r == right: return True diff --git a/mypy/types.py b/mypy/types.py index d48582ad1560..def2f9f207fd 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -222,6 +222,13 @@ class SentinelValue(NamedTuple): # A placeholder for int parameters _dummy_int: Final = -999999 +# Maximum protocol subtyping assumptions depth. We need this to avoid infinite +# recursion for protocols that are genuinely undecidable, see testDivergingProtocol. +# Note we set very large cut-off for now, since some numerical libraries currently +# use ~25 assumptions, and we want to avoid false negatives with them. We may be able +# to reduce this in the future if we will optimize our callable unification logic. +MAX_PROTOCOL_DEPTH: Final = 40 + class TypeOfAny: """ diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index f9adfa51688d..ea0255d91612 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4789,3 +4789,17 @@ bad_rep(t) # E: Argument 1 to "bad_rep" has incompatible type "C"; expected "P[ # N: Got: \ # N: def rep(self) -> C [builtins fixtures/tuple.pyi] + +[case testDivergingProtocol] +from typing import Protocol, TypeVar, List + +T = TypeVar("T") +class P(Protocol[T]): + def meth(self) -> P[List[T]]: ... + +class C: + def meth(self) -> C: ... + +# This should not cause infinite recursion. +x: P[int] = C() +[builtins fixtures/tuple.pyi]