Skip to content

Commit a3af87b

Browse files
Narrow tuple types using len() (#16237)
Fixes #1178 Supersedes #10367 This is includes implementation for fixed length tuples, homogeneous tuples, and variadic tuples (and combinations of those). Generally implementation is straightforward. Some notes: * Unfortunately, it is necessary to add a new attribute `min_len` to `TypeVarTupleType`, which is probably fine, as it doesn't have that many attributes so far. * Supporting more general use cases (like `>` comparisons for variadic tuples) can cause quick proliferation of unions. I added two mechanisms to counteract this: not applying the narrowing if the integer literal in comparison is itself large, and collapsing unions of tuples into a single tuple (if possible) after we are done with the narrowing. This looks a bit arbitrary, but I think it is important to have. * Main missing feature here is probably not inferring type information from indirect comparisons like `len(x) > foo() > 1`. Supporting this kind of things in full generality is cumbersome, and we may add cases that turn out to be important later. * Note I am quite careful with indexing "inside" a `TypeVarTuple`, it is not really needed now, but I wanted to make everything future proof, so that it will be easy to add support for upper bounds for `TypeVarTuple`s, like `Nums = TypeVarTuple("Nums", bound=tuple[float, ...])`. * I also fix couple existing inconsistencies with `Any` handling in type narrowing. It looks like they stem from the old incorrect logic that meet of `Any` and `X` should be `X`, while in fact it should be `Any`. These fixes are not strictly necessary, but otherwise there may be new false positives, because I introduce a bunch of additional type narrowing scenarios here. cc @hatal175, thanks for the test cases, and for your nice first attempt to implement this! Co-authored-by: Tal Hayon <[email protected]>
1 parent ff8cebb commit a3af87b

16 files changed

+1154
-38
lines changed

mypy/binder.py

+83
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@
1212
from mypy.subtypes import is_same_type, is_subtype
1313
from mypy.types import (
1414
AnyType,
15+
Instance,
1516
NoneType,
1617
PartialType,
18+
ProperType,
19+
TupleType,
1720
Type,
1821
TypeOfAny,
1922
TypeType,
2023
UnionType,
24+
UnpackType,
25+
find_unpack_in_list,
2126
get_proper_type,
2227
)
2328
from mypy.typevars import fill_typevars_with_any
@@ -213,6 +218,24 @@ def update_from_options(self, frames: list[Frame]) -> bool:
213218
for other in resulting_values[1:]:
214219
assert other is not None
215220
type = join_simple(self.declarations[key], type, other)
221+
# Try simplifying resulting type for unions involving variadic tuples.
222+
# Technically, everything is still valid without this step, but if we do
223+
# not do this, this may create long unions after exiting an if check like:
224+
# x: tuple[int, ...]
225+
# if len(x) < 10:
226+
# ...
227+
# We want the type of x to be tuple[int, ...] after this block (if it is
228+
# still equivalent to such type).
229+
if isinstance(type, UnionType):
230+
type = collapse_variadic_union(type)
231+
if isinstance(type, ProperType) and isinstance(type, UnionType):
232+
# Simplify away any extra Any's that were added to the declared
233+
# type when popping a frame.
234+
simplified = UnionType.make_union(
235+
[t for t in type.items if not isinstance(get_proper_type(t), AnyType)]
236+
)
237+
if simplified == self.declarations[key]:
238+
type = simplified
216239
if current_value is None or not is_same_type(type, current_value):
217240
self._put(key, type)
218241
changed = True
@@ -453,3 +476,63 @@ def get_declaration(expr: BindableExpression) -> Type | None:
453476
elif isinstance(expr.node, TypeInfo):
454477
return TypeType(fill_typevars_with_any(expr.node))
455478
return None
479+
480+
481+
def collapse_variadic_union(typ: UnionType) -> Type:
482+
"""Simplify a union involving variadic tuple if possible.
483+
484+
This will collapse a type like e.g.
485+
tuple[X, Z] | tuple[X, Y, Z] | tuple[X, Y, Y, *tuple[Y, ...], Z]
486+
back to
487+
tuple[X, *tuple[Y, ...], Z]
488+
which is equivalent, but much simpler form of the same type.
489+
"""
490+
tuple_items = []
491+
other_items = []
492+
for t in typ.items:
493+
p_t = get_proper_type(t)
494+
if isinstance(p_t, TupleType):
495+
tuple_items.append(p_t)
496+
else:
497+
other_items.append(t)
498+
if len(tuple_items) <= 1:
499+
# This type cannot be simplified further.
500+
return typ
501+
tuple_items = sorted(tuple_items, key=lambda t: len(t.items))
502+
first = tuple_items[0]
503+
last = tuple_items[-1]
504+
unpack_index = find_unpack_in_list(last.items)
505+
if unpack_index is None:
506+
return typ
507+
unpack = last.items[unpack_index]
508+
assert isinstance(unpack, UnpackType)
509+
unpacked = get_proper_type(unpack.type)
510+
if not isinstance(unpacked, Instance):
511+
return typ
512+
assert unpacked.type.fullname == "builtins.tuple"
513+
suffix = last.items[unpack_index + 1 :]
514+
515+
# Check that first item matches the expected pattern and infer prefix.
516+
if len(first.items) < len(suffix):
517+
return typ
518+
if suffix and first.items[-len(suffix) :] != suffix:
519+
return typ
520+
if suffix:
521+
prefix = first.items[: -len(suffix)]
522+
else:
523+
prefix = first.items
524+
525+
# Check that all middle types match the expected pattern as well.
526+
arg = unpacked.args[0]
527+
for i, it in enumerate(tuple_items[1:-1]):
528+
if it.items != prefix + [arg] * (i + 1) + suffix:
529+
return typ
530+
531+
# Check the last item (the one with unpack), and choose an appropriate simplified type.
532+
if last.items != prefix + [arg] * (len(typ.items) - 1) + [unpack] + suffix:
533+
return typ
534+
if len(first.items) == 0:
535+
simplified: Type = unpacked.copy_modified()
536+
else:
537+
simplified = TupleType(prefix + [unpack] + suffix, fallback=last.partial_fallback)
538+
return UnionType.make_union([simplified] + other_items)

0 commit comments

Comments
 (0)