Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Narrow tuple types using len() #16237

Merged
merged 20 commits into from
Oct 21, 2023
Merged
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
83 changes: 83 additions & 0 deletions mypy/binder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
from mypy.subtypes import is_same_type, is_subtype
from mypy.types import (
AnyType,
Instance,
NoneType,
PartialType,
ProperType,
TupleType,
Type,
TypeOfAny,
TypeType,
UnionType,
UnpackType,
find_unpack_in_list,
get_proper_type,
)
from mypy.typevars import fill_typevars_with_any
Expand Down Expand Up @@ -213,6 +218,24 @@ def update_from_options(self, frames: list[Frame]) -> bool:
for other in resulting_values[1:]:
assert other is not None
type = join_simple(self.declarations[key], type, other)
# Try simplifying resulting type for unions involving variadic tuples.
# Technically, everything is still valid without this step, but if we do
# not do this, this may create long unions after exiting an if check like:
# x: tuple[int, ...]
# if len(x) < 10:
# ...
# We want the type of x to be tuple[int, ...] after this block (if it is
# still equivalent to such type).
if isinstance(type, UnionType):
type = collapse_variadic_union(type)
if isinstance(type, ProperType) and isinstance(type, UnionType):
# Simplify away any extra Any's that were added to the declared
# type when popping a frame.
simplified = UnionType.make_union(
[t for t in type.items if not isinstance(get_proper_type(t), AnyType)]
)
if simplified == self.declarations[key]:
type = simplified
if current_value is None or not is_same_type(type, current_value):
self._put(key, type)
changed = True
Expand Down Expand Up @@ -453,3 +476,63 @@ def get_declaration(expr: BindableExpression) -> Type | None:
elif isinstance(expr.node, TypeInfo):
return TypeType(fill_typevars_with_any(expr.node))
return None


def collapse_variadic_union(typ: UnionType) -> Type:
"""Simplify a union involving variadic tuple if possible.

This will collapse a type like e.g.
tuple[X, Z] | tuple[X, Y, Z] | tuple[X, Y, Y, *tuple[Y, ...], Z]
back to
tuple[X, *tuple[Y, ...], Z]
which is equivalent, but much simpler form of the same type.
"""
tuple_items = []
other_items = []
for t in typ.items:
p_t = get_proper_type(t)
if isinstance(p_t, TupleType):
tuple_items.append(p_t)
else:
other_items.append(t)
if len(tuple_items) <= 1:
# This type cannot be simplified further.
return typ
tuple_items = sorted(tuple_items, key=lambda t: len(t.items))
first = tuple_items[0]
last = tuple_items[-1]
unpack_index = find_unpack_in_list(last.items)
if unpack_index is None:
return typ
unpack = last.items[unpack_index]
assert isinstance(unpack, UnpackType)
unpacked = get_proper_type(unpack.type)
if not isinstance(unpacked, Instance):
return typ
assert unpacked.type.fullname == "builtins.tuple"
suffix = last.items[unpack_index + 1 :]

# Check that first item matches the expected pattern and infer prefix.
if len(first.items) < len(suffix):
return typ
if suffix and first.items[-len(suffix) :] != suffix:
return typ
if suffix:
prefix = first.items[: -len(suffix)]
else:
prefix = first.items

# Check that all middle types match the expected pattern as well.
arg = unpacked.args[0]
for i, it in enumerate(tuple_items[1:-1]):
if it.items != prefix + [arg] * (i + 1) + suffix:
return typ

# Check the last item (the one with unpack), and choose an appropriate simplified type.
if last.items != prefix + [arg] * (len(typ.items) - 1) + [unpack] + suffix:
return typ
if len(first.items) == 0:
simplified: Type = unpacked.copy_modified()
else:
simplified = TupleType(prefix + [unpack] + suffix, fallback=last.partial_fallback)
return UnionType.make_union([simplified] + other_items)
Loading