diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 2d97dd534d9f..eb500ab3078b 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1258,6 +1258,11 @@ def f(self) -> A: ... for l, r in reversed(assuming): if l == left and r == right: return True + # Guard against infinite recursion when left type keeps growing but right + # protocol stays the same (GH#21739). If we're already checking this + # protocol against many different left types, bail out assuming compatibility. + if len(assuming) > 50: + return True with pop_on_exit(assuming, left, right): for member in right.type.protocol_members: if member in members_not_to_check: diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 6e86507eb48d..a4f675fa5fd5 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4809,3 +4809,38 @@ 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 testProtocolInfiniteRecursionGrowingLeft] +# Regression test for https://github.com/python/mypy/issues/21739 +# When the left type keeps growing but the right protocol stays the same, +# the assuming mechanism cannot detect the cycle. Guard against infinite recursion. +from dataclasses import dataclass +from typing import Any, Callable, Protocol + +class PExp(Protocol): + def EQ(self, other: Any) -> "PExp": ... + +class PRef(Protocol): + def EQ(self, other: Any) -> "PExp": ... + +@dataclass +class Exp: + _left: Any + _op: str + _right: Any + + def EQ(self, other: Any) -> "Exp": + return Exp(self, "EQ", other) + +@dataclass +class Ref: + get: Callable[[], Any] + + def EQ(self, other: Any) -> "Exp": + return Exp(self, "EQ", other) + +@dataclass +class Cache: + def spam(self, name: str) -> "PRef": + return Ref(lambda: "spam") +[builtins fixtures/dataclasses.pyi]