diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f8b2fc4e7971..89b6eb0ca155 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3311,7 +3311,7 @@ def combine_function_signatures(self, types: list[ProperType]) -> AnyType | Call return AnyType(TypeOfAny.special_form) callables = cast("list[CallableType]", types) - combined = union_function_signatures(callables) + combined = union_function_signatures(callables, simplify_unions=True) if combined is not None: return combined diff --git a/mypy/constraints.py b/mypy/constraints.py index 48cc23f74222..340803929b6a 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,8 +751,13 @@ 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): - template.type.inferring.append(template) + inferring = template.type.inferring + if len(inferring) >= MAX_PROTOCOL_DEPTH: + raise ValueError + if len(inferring) < MAX_PROTOCOL_DEPTH and not any( + template == t for t in inferring + ): + inferring.append(template) call = mypy.subtypes.find_member( "__call__", template, actual, is_operator=True ) @@ -763,7 +769,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]: and mypy.subtypes.is_subtype(erase_typevars(call), actual) ): res.extend(infer_constraints(call, actual, self.direction)) - template.type.inferring.pop() + inferring.pop() if isinstance(actual, CallableType) and actual.fallback is not None: if ( actual.is_type_obj() @@ -941,6 +947,9 @@ def visit_instance(self, template: Instance) -> list[Constraint]: res.extend(infer_constraints(template_arg, mapped_arg, SUBTYPE_OF)) res.extend(infer_constraints(template_arg, mapped_arg, SUPERTYPE_OF)) return res + inferring = template.type.inferring + if len(inferring) >= MAX_PROTOCOL_DEPTH: + raise ValueError if ( template.type.is_protocol and self.direction == SUPERTYPE_OF @@ -953,32 +962,34 @@ def visit_instance(self, template: Instance) -> list[Constraint]: # Note that we use is_protocol_implementation instead of is_subtype # because some type may be considered a subtype of a protocol # due to _promote, but still not implement the protocol. - not any(template == t for t in reversed(template.type.inferring)) + len(inferring) < MAX_PROTOCOL_DEPTH + and not any(template == t for t in reversed(inferring)) and mypy.subtypes.is_protocol_implementation(instance, erased, skip=["__call__"]) ): - template.type.inferring.append(template) + inferring.append(template) res.extend( self.infer_constraints_from_protocol_members( instance, template, original_actual, template ) ) - template.type.inferring.pop() + inferring.pop() return res elif ( instance.type.is_protocol and self.direction == SUBTYPE_OF and # We avoid infinite recursion for structural subtypes also here. - not any(instance == i for i in reversed(instance.type.inferring)) + len(inferring) < MAX_PROTOCOL_DEPTH + and not any(instance == i for i in reversed(inferring)) and mypy.subtypes.is_protocol_implementation(erased, instance, skip=["__call__"]) ): - instance.type.inferring.append(instance) + inferring.append(instance) res.extend( self.infer_constraints_from_protocol_members( instance, template, template, instance ) ) - instance.type.inferring.pop() + inferring.pop() return res if res: return res @@ -1010,17 +1021,21 @@ def visit_instance(self, template: Instance) -> list[Constraint]: assert isinstance(erased, ProperType) and isinstance(erased, Instance) # Special-case protocols before using fallback to get more precise constraints # for custom tuple types like NamedTuples. + inferring = template.type.inferring + if len(inferring) >= MAX_PROTOCOL_DEPTH: + raise ValueError if ( template.type.is_protocol and self.direction == SUPERTYPE_OF - and not any(template == t for t in reversed(template.type.inferring)) + and len(inferring) < MAX_PROTOCOL_DEPTH + and not any(template == t for t in reversed(inferring)) and mypy.subtypes.is_protocol_implementation(instance, erased, skip=["__call__"]) ): - template.type.inferring.append(template) + inferring.append(template) res = self.infer_constraints_from_protocol_members( instance, template, original_actual, template ) - template.type.inferring.pop() + inferring.pop() return res return infer_constraints(template, instance, self.direction) elif isinstance(actual, TypeVarType): diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 9c67ebbdf106..445121312ad9 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, @@ -60,6 +61,7 @@ TypeAliasType, TypedDictType, TypeOfAny, + TypeStrVisitor, TypeType, TypeVarLikeType, TypeVarTupleType, @@ -1307,6 +1309,12 @@ 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: + visitor = TypeStrVisitor(options=options or Options()) + pairs = [] + for l, r in assuming: + pairs.append((l.accept(visitor), r.accept(visitor))) + raise ValueError(pairs) for l, r in reversed(assuming): if l == left and r == right: return True @@ -2199,7 +2207,9 @@ def report(*args: Any) -> None: return cast(NormalizedCallableType, applied) -def union_function_signatures(callables: list[CallableType]) -> CallableType | None: +def union_function_signatures( + callables: list[CallableType], *, simplify_unions: bool = False +) -> CallableType | None: """Combine a list of functions by taking the union of all the arguments and return types.""" if len(callables) == 1: return callables[0] @@ -2217,15 +2227,23 @@ def union_function_signatures(callables: list[CallableType]) -> CallableType | N # confusing and ought to be re-written anyway.) callables, variables = merge_typevars_in_callables_by_name(callables) - new_args: list[list[Type]] = [[] for _ in callables[0].arg_types] - new_kinds = list(callables[0].arg_kinds) + new_callable = callables[0].with_unpacked_kwargs().with_normalized_var_args() + new_args: list[list[Type]] = [[] for _ in new_callable.arg_types] + new_kinds = list(new_callable.arg_kinds) new_returns: list[Type] = [] for target in callables: + target = target.with_unpacked_kwargs().with_normalized_var_args() # TODO: Enhance the merging logic to handle a wider variety of signatures. + # In particular, allow name-only arguments that appear in different order. if len(new_kinds) != len(target.arg_kinds): return None for i, (new_kind, target_kind) in enumerate(zip(new_kinds, target.arg_kinds)): + if target_kind.is_named() and target.arg_names[i] != new_callable.arg_names[i]: + return None + if isinstance(target.arg_types[i], (ParamSpecType, UnpackType)): + # It is risky to put these inside a union. + return None if new_kind == target_kind: continue if new_kind.is_positional() and target_kind.is_positional(): @@ -2237,10 +2255,17 @@ def union_function_signatures(callables: list[CallableType]) -> CallableType | N new_args[i].append(arg) new_returns.append(target.ret_type) - return callables[0].copy_modified( - arg_types=[mypy.typeops.make_simplified_union(args) for args in new_args], + if simplify_unions: + arg_types = [mypy.typeops.make_simplified_union(args) for args in new_args] + ret_type = mypy.typeops.make_simplified_union(new_returns) + else: + arg_types = [UnionType.make_union(args) for args in new_args] + ret_type = UnionType.make_union(new_returns) + + return new_callable.copy_modified( + arg_types=arg_types, arg_kinds=new_kinds, - ret_type=mypy.typeops.make_simplified_union(new_returns), + ret_type=ret_type, variables=variables, implicit=True, ) diff --git a/mypy/types.py b/mypy/types.py index e01a1f21e8fb..8de0727b9cd2 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -222,6 +222,8 @@ class SentinelValue(NamedTuple): # A placeholder for int parameters _dummy_int: Final = -999999 +MAX_PROTOCOL_DEPTH: Final = 30 + class TypeOfAny: """ diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index f9adfa51688d..94ac2dcdf14e 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4789,3 +4789,16 @@ 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-skip] +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: ... + +x: P = C() +[builtins fixtures/tuple.pyi]