Skip to content

Commit 5a06937

Browse files
authored
Lenient handling of *tuple[Any, ...] (part 2) (#22006)
Ref #19858 This (smaller) part handles various edge cases that should be handled leniently in presence of `*tuple[Any, ...]`: indexing and unpacking. Everything is straightforward, similar to first part #22001, the tests are focused on situations that previously gave errors.
1 parent 0b6f1b7 commit 5a06937

6 files changed

Lines changed: 128 additions & 21 deletions

File tree

‎mypy/checker.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,12 @@ def __init__(self) -> None:
291291
UninhabitedType,
292292
UnionType,
293293
UnpackType,
294+
extend_args_for_prefix_and_suffix,
294295
find_unpack_in_list,
295296
flatten_nested_unions,
296297
get_proper_type,
297298
get_proper_types,
299+
get_variadic_item,
298300
instance_cache,
299301
is_literal_type,
300302
is_named_instance,
@@ -4389,6 +4391,36 @@ def flatten_lvalues(self, lvalues: list[Expression]) -> list[Expression]:
43894391
res.append(lv)
43904392
return res
43914393

4394+
def adjust_rvalue_type_if_possible(
4395+
self, rvalue_type: TupleType, lvalues: list[Lvalue]
4396+
) -> TupleType:
4397+
"""Adjust type of rvalue to match the shape/structure of lvalues.
4398+
4399+
Currently, we only allow this if the rvalue type has contains *tuple[Any, ...].
4400+
"""
4401+
right_variadic = get_variadic_item(rvalue_type)
4402+
if right_variadic is None:
4403+
return rvalue_type
4404+
right_unpack_index, right_item = right_variadic
4405+
if not isinstance(get_proper_type(right_item), AnyType):
4406+
return rvalue_type
4407+
left_star_index = next(
4408+
(i for i, lv in enumerate(lvalues) if isinstance(lv, StarExpr)), None
4409+
)
4410+
if left_star_index is None:
4411+
extra = len(lvalues) - len(rvalue_type.items) + 1
4412+
if extra < 0:
4413+
return rvalue_type
4414+
return rvalue_type.copy_modified(
4415+
items=rvalue_type.items[:right_unpack_index]
4416+
+ [right_item] * extra
4417+
+ rvalue_type.items[right_unpack_index + 1 :]
4418+
)
4419+
new_items = extend_args_for_prefix_and_suffix(
4420+
tuple(rvalue_type.items), left_star_index, len(lvalues) - left_star_index - 1
4421+
)
4422+
return rvalue_type.copy_modified(items=list(new_items))
4423+
43924424
def check_multi_assignment_from_tuple(
43934425
self,
43944426
lvalues: list[Lvalue],
@@ -4399,6 +4431,9 @@ def check_multi_assignment_from_tuple(
43994431
infer_lvalue_type: bool = True,
44004432
) -> None:
44014433
rvalue_unpack = find_unpack_in_list(rvalue_type.items)
4434+
if rvalue_unpack is not None:
4435+
rvalue_type = self.adjust_rvalue_type_if_possible(rvalue_type, lvalues)
4436+
rvalue_unpack = find_unpack_in_list(rvalue_type.items)
44024437
if self.check_rvalue_count_in_assignment(
44034438
lvalues, len(rvalue_type.items), context, rvalue_unpack=rvalue_unpack
44044439
):
@@ -4440,8 +4475,14 @@ def check_multi_assignment_from_tuple(
44404475
if isinstance(reinferred_rvalue_type, TupleType):
44414476
# This branch will usually be taken, but in some cases context can
44424477
# e.g. select a different overload
4478+
# TODO: reinferred tuple may be of a different (invalid) shape.
44434479
rvalue_type = reinferred_rvalue_type
44444480

4481+
# Reinferring the type can undo the shape adjustment, so do it again.
4482+
rvalue_unpack = find_unpack_in_list(rvalue_type.items)
4483+
if rvalue_unpack is not None:
4484+
rvalue_type = self.adjust_rvalue_type_if_possible(rvalue_type, lvalues)
4485+
44454486
left_rv_types, star_rv_types, right_rv_types = self.split_around_star(
44464487
rvalue_type.items, star_index, len(lvalues)
44474488
)

‎mypy/checkexpr.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@
200200
flatten_nested_unions,
201201
get_proper_type,
202202
get_proper_types,
203+
get_variadic_item,
203204
has_recursive_types,
204205
has_type_vars,
205206
is_named_instance,
@@ -4654,6 +4655,10 @@ def visit_tuple_index_helper(self, left: TupleType, n: int) -> Type | None:
46544655
if n >= self.min_tuple_length(left):
46554656
# For tuple[int, *tuple[str, ...], int] we allow either index 0 or 1,
46564657
# since variadic item may have zero items.
4658+
if isinstance(get_proper_type(middle), AnyType):
4659+
# The only exception is when the variadic item is Any,
4660+
# which is handled leniently.
4661+
return UnionType.make_union([middle] + left.items[unpack_index + 1 :])
46574662
return None
46584663
if n < unpack_index:
46594664
return left.items[n]
@@ -4666,6 +4671,8 @@ def visit_tuple_index_helper(self, left: TupleType, n: int) -> Type | None:
46664671
n += self.min_tuple_length(left)
46674672
if n < 0:
46684673
# Similar to above, we only allow -1, and -2 for tuple[int, *tuple[str, ...], int]
4674+
if isinstance(get_proper_type(middle), AnyType):
4675+
return UnionType.make_union(left.items[:unpack_index] + [middle])
46694676
return None
46704677
if n >= unpack_index + extra_items:
46714678
return left.items[n - extra_items + 1]
@@ -4698,8 +4705,18 @@ def visit_tuple_slice_helper(self, left_type: TupleType, slic: SliceExpr) -> Typ
46984705

46994706
items: list[Type] = []
47004707
for b, e, s in itertools.product(begin, end, stride):
4708+
if s == 0:
4709+
self.chk.fail("Slice step cannot be zero", slic)
4710+
items.append(self.named_type("builtins.tuple"))
4711+
continue
47014712
item = left_type.slice(b, e, s, fallback=self.named_type("builtins.tuple"))
47024713
if item is None:
4714+
left_variadic = get_variadic_item(left_type)
4715+
if left_variadic is not None:
4716+
_, left_item = left_variadic
4717+
if isinstance(get_proper_type(left_item), AnyType):
4718+
# If the tuple has *tuple[Any, ...] slice should never fail.
4719+
return self.nonliteral_tuple_index_helper(left_type, slic)
47034720
self.chk.fail(message_registry.AMBIGUOUS_SLICE_OF_VARIADIC_TUPLE, slic)
47044721
return AnyType(TypeOfAny.from_error)
47054722
items.append(item)

‎mypy/subtypes.py‎

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
find_unpack_in_list,
7474
flatten_nested_unions,
7575
get_proper_type,
76+
get_variadic_item,
7677
is_named_instance,
7778
split_with_prefix_and_suffix,
7879
)
@@ -852,7 +853,7 @@ def adjust_left_if_possible(self, left: TupleType, right: TupleType) -> TupleTyp
852853
Note: this only works if right is fixed size (including *Ts), the variadic
853854
right are handled by the caller, currently with variadic_tuple_subtype().
854855
"""
855-
left_variadic = self.get_variadic_item(left)
856+
left_variadic = get_variadic_item(left)
856857
if left_variadic is None:
857858
return left
858859
left_unpack_index, left_item = left_variadic
@@ -884,19 +885,6 @@ def adjust_left_if_possible(self, left: TupleType, right: TupleType) -> TupleTyp
884885
)
885886
return left.copy_modified(items=list(new_items))
886887

887-
def get_variadic_item(self, tup: TupleType) -> tuple[int, Type] | None:
888-
"""If this is tuple[X, *tuple[Y, ...], Z], return Y, otherwise None."""
889-
unpack_index = find_unpack_in_list(tup.items)
890-
if unpack_index is None:
891-
return None
892-
unpack = tup.items[unpack_index]
893-
assert isinstance(unpack, UnpackType)
894-
unpacked = get_proper_type(unpack.type)
895-
if not isinstance(unpacked, Instance):
896-
return None
897-
assert unpacked.type.fullname == "builtins.tuple"
898-
return unpack_index, unpacked.args[0]
899-
900888
def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
901889
"""Check subtyping between two potentially variadic tuples.
902890
@@ -906,7 +894,7 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
906894
Note: the cases where right is fixed or has *Ts unpack should be handled
907895
by the caller.
908896
"""
909-
right_variadic = self.get_variadic_item(right)
897+
right_variadic = get_variadic_item(right)
910898
if right_variadic is None:
911899
# This case should be handled by the caller.
912900
return False

‎mypy/types.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2946,9 +2946,6 @@ def slice(
29462946
if fallback is None:
29472947
fallback = self.partial_fallback
29482948

2949-
if stride == 0:
2950-
return None
2951-
29522949
if any(isinstance(t, UnpackType) for t in self.items):
29532950
total = len(self.items)
29542951
unpack_index = find_unpack_in_list(self.items)
@@ -2989,7 +2986,7 @@ def slice(
29892986
else:
29902987
return None
29912988
else:
2992-
# TODO: there some additional cases we can support for homogeneous variadic
2989+
# TODO: there are some additional cases we can support for homogeneous variadic
29932990
# items, we can "eat away" finite number of items.
29942991
return None
29952992
else:
@@ -4448,6 +4445,20 @@ def type_vars_as_args(type_vars: Sequence[TypeVarLikeType]) -> tuple[Type, ...]:
44484445
return tuple(args)
44494446

44504447

4448+
def get_variadic_item(tup: TupleType) -> tuple[int, Type] | None:
4449+
"""If this is tuple[X, *tuple[Y, ...], Z], return Y, otherwise None."""
4450+
unpack_index = find_unpack_in_list(tup.items)
4451+
if unpack_index is None:
4452+
return None
4453+
unpack = tup.items[unpack_index]
4454+
assert isinstance(unpack, UnpackType)
4455+
unpacked = get_proper_type(unpack.type)
4456+
if not isinstance(unpacked, Instance):
4457+
return None
4458+
assert unpacked.type.fullname == "builtins.tuple"
4459+
return unpack_index, unpacked.args[0]
4460+
4461+
44514462
# See docstring for mypy/cache.py for reserved tag ranges.
44524463
# Instance-related tags.
44534464
INSTANCE: Final[Tag] = 80

‎test-data/unit/check-tuples.test‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,8 +1485,7 @@ t[y:] # E: Invalid index type "slice[str, None, None]" for "tuple[int, str]"; e
14851485

14861486
[case testTupleSliceStepZeroNoCrash]
14871487
# This was crashing: https://github.com/python/mypy/issues/18062
1488-
# TODO: emit better error when 0 is used for step
1489-
()[::0] # E: Ambiguous slice of a variadic tuple
1488+
()[::0] # E: Slice step cannot be zero
14901489
[builtins fixtures/tuple.pyi]
14911490

14921491
[case testInferTupleTypeFallbackAgainstInstance]

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1563,6 +1563,57 @@ x = c1
15631563
x = c2
15641564
[builtins fixtures/tuple.pyi]
15651565

1566+
[case testAnyVariadicTupleLenientIndex]
1567+
from typing import Any, Unpack
1568+
1569+
t: tuple[int, Unpack[tuple[Any, ...]], str]
1570+
t1: tuple[int, Unpack[tuple[Any, ...]]]
1571+
t2: tuple[Unpack[tuple[Any, ...]], str]
1572+
1573+
reveal_type(t[42]) # N: Revealed type is "Any | builtins.str"
1574+
reveal_type(t[-42]) # N: Revealed type is "builtins.int | Any"
1575+
1576+
reveal_type(t1[42]) # N: Revealed type is "Any"
1577+
reveal_type(t1[-42]) # N: Revealed type is "builtins.int | Any"
1578+
1579+
reveal_type(t2[42]) # N: Revealed type is "Any | builtins.str"
1580+
reveal_type(t2[-42]) # N: Revealed type is "Any"
1581+
1582+
# Slicing support for variadic tuples is currently best effort, the key idea is to not give an error.
1583+
reveal_type(t[1:3:2]) # N: Revealed type is "builtins.tuple[builtins.int | Any | builtins.str, ...]"
1584+
reveal_type(t1[1:3:2]) # N: Revealed type is "builtins.tuple[builtins.int | Any, ...]"
1585+
reveal_type(t2[1:3:2]) # N: Revealed type is "builtins.tuple[Any | builtins.str, ...]"
1586+
bt = t[::0] # E: Slice step cannot be zero
1587+
reveal_type(bt) # N: Revealed type is "builtins.tuple[Any, ...]"
1588+
[builtins fixtures/tuple.pyi]
1589+
1590+
[case testAnyVariadicTupleLenientUnpacking]
1591+
from typing import Any, Unpack
1592+
1593+
t: tuple[int, Unpack[tuple[Any, ...]], str]
1594+
1595+
x0, y0 = t
1596+
reveal_type(x0) # N: Revealed type is "builtins.int"
1597+
reveal_type(y0) # N: Revealed type is "builtins.str"
1598+
1599+
x, y, z = t
1600+
reveal_type(x) # N: Revealed type is "builtins.int"
1601+
reveal_type(y) # N: Revealed type is "Any"
1602+
reveal_type(z) # N: Revealed type is "builtins.str"
1603+
1604+
x1, y1, *zz, x2, y2 = t
1605+
reveal_type(x1) # N: Revealed type is "builtins.int"
1606+
reveal_type(y1) # N: Revealed type is "Any"
1607+
reveal_type(zz) # N: Revealed type is "builtins.list[Any]"
1608+
reveal_type(x2) # N: Revealed type is "Any"
1609+
reveal_type(y2) # N: Revealed type is "builtins.str"
1610+
1611+
t2: tuple[int, int, Unpack[tuple[Any, ...]], str, str]
1612+
1613+
# Not the best error, but this definitely invalid, even with Any.
1614+
x3, y3, z3 = t2 # E: Variadic tuple unpacking requires a star target
1615+
[builtins fixtures/tuple.pyi]
1616+
15661617
[case testUnpackingVariadicTuplesTypeVar]
15671618
from typing import Tuple
15681619
from typing_extensions import TypeVarTuple, Unpack

0 commit comments

Comments
 (0)