Skip to content

Commit 93971bb

Browse files
authored
Lenient handling of *tuple[Any, ...] (part 3) (#22014)
Fixes #20771 (it already worked, but I am now adding a regression test) Fixes #20651 This handles various edge cases related to inference with variadic unpacks (including `Any`). Interestingly, after properly handling some missing edge cases, I realized that the variadic case can be "merged in the main branch" of the inference logic, so that the final diff is very simple.
1 parent 43ca27d commit 93971bb

5 files changed

Lines changed: 138 additions & 20 deletions

File tree

‎mypy/checkpattern.py‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType:
402402
return PatternType(new_type, rest_type, captures)
403403

404404
def contract_starred_pattern_types(
405-
self, types: list[Type], star_pos: int | None, num_patterns: int
405+
self, types: list[Type], star_pos: int | None, num_required_patterns: int
406406
) -> list[Type]:
407407
"""
408408
Contracts a list of types in a sequence pattern depending on the position of a starred
@@ -422,15 +422,13 @@ def contract_starred_pattern_types(
422422
# This should be guaranteed by the normalization in the caller.
423423
assert isinstance(unpacked, Instance) and unpacked.type.fullname == "builtins.tuple"
424424
if star_pos is None:
425-
missing = num_patterns - len(types) + 1
425+
missing = num_required_patterns - len(types) + 1
426426
new_types = types[:unpack_index]
427427
new_types += [unpacked.args[0]] * missing
428428
new_types += types[unpack_index + 1 :]
429429
return new_types
430430
prefix, middle, suffix = split_with_prefix_and_suffix(
431-
tuple([UnpackType(unpacked) if isinstance(t, UnpackType) else t for t in types]),
432-
star_pos,
433-
num_patterns - star_pos,
431+
tuple(types), star_pos, num_required_patterns - star_pos
434432
)
435433
new_middle = []
436434
for m in middle:
@@ -445,7 +443,7 @@ def contract_starred_pattern_types(
445443
if star_pos is None:
446444
return types
447445
new_types = types[:star_pos]
448-
star_length = len(types) - num_patterns
446+
star_length = len(types) - num_required_patterns
449447
new_types.append(make_simplified_union(types[star_pos : star_pos + star_length]))
450448
new_types += types[star_pos + star_length :]
451449
return new_types

‎mypy/constraints.py‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,7 @@ def visit_tuple_type(self, template: TupleType) -> list[Constraint]:
13181318
a_unpack = actual.items[a_unpack_index]
13191319
assert isinstance(a_unpack, UnpackType)
13201320
a_unpacked = get_proper_type(a_unpack.type)
1321-
if len(actual.items) + 1 <= len(template.items):
1321+
if len(actual.items) <= len(template.items) + 1:
13221322
a_prefix_len = a_unpack_index
13231323
a_suffix_len = len(actual.items) - a_unpack_index - 1
13241324
t_prefix, t_middle, t_suffix = split_with_prefix_and_suffix(
@@ -1591,6 +1591,17 @@ def build_constraints_for_simple_unpack(
15911591
# This is the only case where we can guarantee there will be no partial overlap
15921592
# (note however partial overlap is OK for variadic tuples, it is handled below).
15931593
t_unpack = template_args[template_unpack]
1594+
else:
1595+
# A special case for a variadic actual tuple unpack, we can infer T <: X from
1596+
# tuple[..., *tuple[T, ...], ...] <: tuple[..., *tuple[X, ...], ...] etc.
1597+
actual_unpack_type = actual_args[actual_unpack]
1598+
assert isinstance(actual_unpack_type, UnpackType)
1599+
a_unpacked = get_proper_type(actual_unpack_type.type)
1600+
if isinstance(a_unpacked, Instance) and a_unpacked.type.fullname == "builtins.tuple":
1601+
t_unpack = template_args[template_unpack]
1602+
# In this case we can "eat away" as much as we need.
1603+
common_prefix = template_prefix
1604+
common_suffix = template_suffix
15941605

15951606
# Handle constraints from prefixes/suffixes first.
15961607
start, middle, end = split_with_prefix_and_suffix(
@@ -1619,18 +1630,6 @@ def build_constraints_for_simple_unpack(
16191630
res.extend(infer_constraints(tp.args[0], a_tp.args[0], direction))
16201631
elif isinstance(tp, TypeVarTupleType):
16211632
res.append(Constraint(tp, direction, TupleType(list(middle), tp.tuple_fallback)))
1622-
elif actual_unpack is not None:
1623-
# A special case for a variadic tuple unpack, we simply infer T <: X from
1624-
# Tuple[..., *tuple[T, ...], ...] <: Tuple[..., *tuple[X, ...], ...].
1625-
actual_unpack_type = actual_args[actual_unpack]
1626-
assert isinstance(actual_unpack_type, UnpackType)
1627-
a_unpacked = get_proper_type(actual_unpack_type.type)
1628-
if isinstance(a_unpacked, Instance) and a_unpacked.type.fullname == "builtins.tuple":
1629-
t_unpack = template_args[template_unpack]
1630-
assert isinstance(t_unpack, UnpackType)
1631-
tp = get_proper_type(t_unpack.type)
1632-
if isinstance(tp, Instance) and tp.type.fullname == "builtins.tuple":
1633-
res.extend(infer_constraints(tp.args[0], a_unpacked.args[0], direction))
16341633
return res
16351634

16361635

‎mypy/subtypes.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,8 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
960960
if not self._is_subtype(left_item, right_item):
961961
return False
962962
max_overlap = max(0, right_prefix - left_prefix, right_suffix - left_suffix)
963+
# We need to also handle the case where overlap is positive on both sides.
964+
max_overlap = max(max_overlap, right_prefix - left_prefix + right_suffix - left_suffix)
963965
for overlap in range(max_overlap + 1):
964966
repr_items = left.items[:left_prefix] + [left_item] * overlap
965967
if left_suffix:

‎mypy/types.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4253,7 +4253,15 @@ def has_recursive_types(typ: Type) -> bool:
42534253
def split_with_prefix_and_suffix(
42544254
types: tuple[Type, ...], prefix: int, suffix: int
42554255
) -> tuple[tuple[Type, ...], tuple[Type, ...], tuple[Type, ...]]:
4256-
if len(types) <= prefix + suffix:
4256+
# The caller must validate that the split can be satisfied, i.e. there is
4257+
# enough capacity in either initial type list or there is a variadic unpack.
4258+
# Otherwise, this function may return nonsensical result.
4259+
# TODO: should we add an assert here?
4260+
needs_extend = False
4261+
index = find_unpack_in_list(types)
4262+
if index is not None:
4263+
needs_extend = index < prefix or len(types) - index - 1 < suffix
4264+
if needs_extend:
42574265
types = extend_args_for_prefix_and_suffix(types, prefix, suffix)
42584266
if suffix:
42594267
return types[:prefix], types[prefix:-suffix], types[-suffix:]

‎test-data/unit/check-typevar-tuple.test‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3104,3 +3104,114 @@ foo(t2)
31043104
foo(t3) # E: Argument 1 to "foo" has incompatible type "C[Unpack[tuple[Any, ...]], str]"; expected "C[int, Unpack[tuple[int, ...]], int]"
31053105
foo(t4)
31063106
[builtins fixtures/tuple.pyi]
3107+
3108+
[case testInferAgainstVariadicAnySplit]
3109+
from typing import Generic, TypeVar, Optional, Any
3110+
from typing_extensions import TypeVarTuple, Unpack
3111+
3112+
T = TypeVar("T", bound=Any)
3113+
Ts = TypeVarTuple("Ts")
3114+
3115+
class Result(Generic[Unpack[Ts]]):
3116+
def foo(self: Result[T, Unpack[tuple[Any, ...]]]) -> Optional[T]: ...
3117+
def bar(self: Result[Unpack[tuple[Any, ...]], T]) -> Optional[T]: ...
3118+
3119+
result: Result[Unpack[tuple[Any, ...]]]
3120+
reveal_type(result.foo) # N: Revealed type is "def () -> Any | None"
3121+
reveal_type(result.bar) # N: Revealed type is "def () -> Any | None"
3122+
[builtins fixtures/tuple.pyi]
3123+
3124+
[case testInferAgainstVariadicPrefixProtocol]
3125+
from typing import Any, Protocol, TypeVar
3126+
from typing_extensions import TypeVarTuple, Unpack
3127+
3128+
T = TypeVar("T")
3129+
Ts = TypeVarTuple("Ts")
3130+
3131+
class WithTuple(Protocol[Unpack[Ts]]):
3132+
tup: tuple[Unpack[Ts]]
3133+
3134+
def get_tuple(e: WithTuple[Unpack[Ts]]) -> tuple[Unpack[Ts]]:
3135+
return e.tup
3136+
3137+
def get_first(e: WithTuple[T, Unpack[tuple[Any, ...]]]) -> T:
3138+
return e.tup[0]
3139+
3140+
class X:
3141+
tup: tuple[int, str]
3142+
3143+
reveal_type(get_tuple(X())) # N: Revealed type is "tuple[builtins.int, builtins.str]"
3144+
reveal_type(get_first(X())) # N: Revealed type is "builtins.int"
3145+
[builtins fixtures/tuple.pyi]
3146+
3147+
[case testInferAgainstVariadicPrefixSuffix]
3148+
from typing import Any, Protocol, TypeVar
3149+
from typing_extensions import TypeVarTuple, Unpack
3150+
3151+
T = TypeVar("T")
3152+
T1 = TypeVar("T1")
3153+
T2 = TypeVar("T2")
3154+
T3 = TypeVar("T3")
3155+
T4 = TypeVar("T4")
3156+
Ts = TypeVarTuple("Ts")
3157+
3158+
def first_last(t: tuple[T1, Unpack[tuple[Any, ...]], T2]) -> tuple[T1, T2]: ...
3159+
def first_last_2(t: tuple[T1, T2, Unpack[Ts], T3, T4]) -> tuple[tuple[T1, T2, T3, T4], tuple[Unpack[Ts]]]: ...
3160+
def first_last_middle(t: tuple[T1, Unpack[Ts], T2]) -> tuple[tuple[T1, T2], tuple[Unpack[Ts]]]: ...
3161+
3162+
t1: tuple[Any, ...]
3163+
t2i: tuple[str, Unpack[tuple[int, ...]], bytes]
3164+
t2a: tuple[str, Unpack[tuple[Any, ...]], bytes]
3165+
t3: tuple[str, bytes, Unpack[tuple[int, ...]], float, bool]
3166+
3167+
reveal_type(first_last(t1)) # N: Revealed type is "tuple[Any, Any]"
3168+
reveal_type(first_last(t2i)) # N: Revealed type is "tuple[builtins.str, builtins.bytes]"
3169+
reveal_type(first_last(t3)) # N: Revealed type is "tuple[builtins.str, builtins.bool]"
3170+
3171+
reveal_type(first_last_2(t1)) # N: Revealed type is "tuple[tuple[Any, Any, Any, Any], builtins.tuple[Any, ...]]"
3172+
reveal_type(first_last_2(t2a)) # N: Revealed type is "tuple[tuple[builtins.str, Any, Any, builtins.bytes], builtins.tuple[Any, ...]]"
3173+
reveal_type(first_last_2(t3)) # N: Revealed type is "tuple[tuple[builtins.str, builtins.bytes, builtins.float, builtins.bool], builtins.tuple[builtins.int, ...]]"
3174+
3175+
reveal_type(first_last_middle(t3)) # N: Revealed type is "tuple[tuple[builtins.str, builtins.bool], tuple[builtins.bytes, Unpack[builtins.tuple[builtins.int, ...]], builtins.float]]"
3176+
[builtins fixtures/tuple.pyi]
3177+
3178+
[case testInferAgainstVariadicPrefixSuffix2]
3179+
from typing import Any, TypeVar, Generic
3180+
from typing_extensions import TypeVarTuple, Unpack
3181+
3182+
T = TypeVar("T")
3183+
T1 = TypeVar("T1")
3184+
T2 = TypeVar("T2")
3185+
Ts = TypeVarTuple("Ts")
3186+
3187+
def mix(x: tuple[T1, T2, Unpack[Ts]]) -> tuple[T1, T2, tuple[Unpack[Ts]]]: ...
3188+
a: tuple[str, Unpack[tuple[Any, ...]], bytes, float]
3189+
3190+
reveal_type(mix(a)) # N: Revealed type is "tuple[builtins.str, Any, tuple[Unpack[builtins.tuple[Any, ...]], builtins.bytes, builtins.float]]"
3191+
3192+
class C(Generic[T1, Unpack[Ts], T2]):
3193+
start: T1
3194+
end: T2
3195+
middle: tuple[Unpack[Ts]]
3196+
3197+
c: C[str, bool, Unpack[tuple[int, ...]]]
3198+
reveal_type(c) # N: Revealed type is "__main__.C[builtins.str, builtins.bool, Unpack[builtins.tuple[builtins.int, ...]]]"
3199+
reveal_type(c.start) # N: Revealed type is "builtins.str"
3200+
reveal_type(c.middle) # N: Revealed type is "tuple[builtins.bool, Unpack[builtins.tuple[builtins.int, ...]]]"
3201+
reveal_type(c.end) # N: Revealed type is "builtins.int"
3202+
3203+
cs: C[Unpack[tuple[int, ...]], str, bool]
3204+
reveal_type(cs) # N: Revealed type is "__main__.C[Unpack[builtins.tuple[builtins.int, ...]], builtins.str, builtins.bool]"
3205+
reveal_type(cs.start) # N: Revealed type is "builtins.int"
3206+
reveal_type(cs.middle) # N: Revealed type is "tuple[Unpack[builtins.tuple[builtins.int, ...]], builtins.str]"
3207+
reveal_type(cs.end) # N: Revealed type is "builtins.bool"
3208+
3209+
def mix2(x: tuple[T1, T, T2]) -> tuple[T, tuple[T1, T2]]: ...
3210+
b1: tuple[int, str, Unpack[tuple[Any, ...]]]
3211+
b2: tuple[int, Unpack[tuple[Any, ...]], str]
3212+
b3: tuple[Unpack[tuple[Any, ...]], int, str]
3213+
3214+
reveal_type(mix2(b1)) # N: Revealed type is "tuple[builtins.str, tuple[builtins.int, Any]]"
3215+
reveal_type(mix2(b2)) # N: Revealed type is "tuple[Any, tuple[builtins.int, builtins.str]]"
3216+
reveal_type(mix2(b3)) # N: Revealed type is "tuple[builtins.int, tuple[Any, builtins.str]]"
3217+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)