Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
39 changes: 27 additions & 12 deletions mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
TypeInfo,
)
from mypy.types import (
MAX_PROTOCOL_DEPTH,
TUPLE_LIKE_INSTANCE_NAMES,
AnyType,
CallableType,
Expand Down Expand Up @@ -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
)
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
23 changes: 20 additions & 3 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -60,6 +61,7 @@
TypeAliasType,
TypedDictType,
TypeOfAny,
TypeStrVisitor,
TypeType,
TypeVarLikeType,
TypeVarTupleType,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -2237,10 +2247,17 @@ def union_function_signatures(callables: list[CallableType]) -> CallableType | N
new_args[i].append(arg)
new_returns.append(target.ret_type)

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 callables[0].copy_modified(
arg_types=[mypy.typeops.make_simplified_union(args) for args in new_args],
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,
)
Expand Down
2 changes: 2 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ class SentinelValue(NamedTuple):
# A placeholder for int parameters
_dummy_int: Final = -999999

MAX_PROTOCOL_DEPTH: Final = 40


class TypeOfAny:
"""
Expand Down
13 changes: 13 additions & 0 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading