Skip to content

Commit

Permalink
Properly handle unpacks in overlap checks (#17356)
Browse files Browse the repository at this point in the history
Fixes #17319

This is still not 100% robust, but at least it should not crash, and
should cover correctly vast majority of cases.
  • Loading branch information
ilevkivskyi committed Jun 10, 2024
1 parent 83d54ff commit 6427da6
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 0 deletions.
34 changes: 34 additions & 0 deletions mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,19 @@ def are_tuples_overlapping(
right = adjust_tuple(right, left) or right
assert isinstance(left, TupleType), f"Type {left} is not a tuple"
assert isinstance(right, TupleType), f"Type {right} is not a tuple"

# This algorithm works well if only one tuple is variadic, if both are
# variadic we may get rare false negatives for overlapping prefix/suffix.
# Also, this ignores empty unpack case, but it is probably consistent with
# how we handle e.g. empty lists in overload overlaps.
# TODO: write a more robust algorithm for cases where both types are variadic.
left_unpack = find_unpack_in_list(left.items)
right_unpack = find_unpack_in_list(right.items)
if left_unpack is not None:
left = expand_tuple_if_possible(left, len(right.items))
if right_unpack is not None:
right = expand_tuple_if_possible(right, len(left.items))

if len(left.items) != len(right.items):
return False
return all(
Expand All @@ -624,6 +637,27 @@ def are_tuples_overlapping(
)


def expand_tuple_if_possible(tup: TupleType, target: int) -> TupleType:
if len(tup.items) > target + 1:
return tup
extra = target + 1 - len(tup.items)
new_items = []
for it in tup.items:
if not isinstance(it, UnpackType):
new_items.append(it)
continue
unpacked = get_proper_type(it.type)
if isinstance(unpacked, TypeVarTupleType):
instance = unpacked.tuple_fallback
else:
# Nested non-variadic tuples should be normalized at this point.
assert isinstance(unpacked, Instance)
instance = unpacked
assert instance.type.fullname == "builtins.tuple"
new_items.extend([instance.args[0]] * extra)
return tup.copy_modified(items=new_items)


def adjust_tuple(left: ProperType, r: ProperType) -> TupleType | None:
"""Find out if `left` is a Tuple[A, ...], and adjust its length to `right`"""
if isinstance(left, Instance) and left.type.fullname == "builtins.tuple":
Expand Down
57 changes: 57 additions & 0 deletions test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -1808,6 +1808,63 @@ def test(a: Tuple[int, str], b: Tuple[bool], c: Tuple[bool, ...]):
reveal_type(add(b, c)) # N: Revealed type is "builtins.tuple[builtins.bool, ...]"
[builtins fixtures/tuple.pyi]

[case testTypeVarTupleOverloadOverlap]
from typing import Union, overload, Tuple
from typing_extensions import Unpack

class Int(int): ...

A = Tuple[int, Unpack[Tuple[int, ...]]]
B = Tuple[int, Unpack[Tuple[str, ...]]]

@overload
def f(arg: A) -> int: ...
@overload
def f(arg: B) -> str: ...
def f(arg: Union[A, B]) -> Union[int, str]:
...

A1 = Tuple[int, Unpack[Tuple[Int, ...]]]
B1 = Tuple[Unpack[Tuple[Int, ...]], int]

@overload
def f1(arg: A1) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def f1(arg: B1) -> str: ...
def f1(arg: Union[A1, B1]) -> Union[int, str]:
...

A2 = Tuple[int, int, int]
B2 = Tuple[int, Unpack[Tuple[int, ...]]]

@overload
def f2(arg: A2) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def f2(arg: B2) -> str: ...
def f2(arg: Union[A2, B2]) -> Union[int, str]:
...

A3 = Tuple[int, int, int]
B3 = Tuple[int, Unpack[Tuple[str, ...]]]

@overload
def f3(arg: A3) -> int: ...
@overload
def f3(arg: B3) -> str: ...
def f3(arg: Union[A3, B3]) -> Union[int, str]:
...

A4 = Tuple[int, int, Unpack[Tuple[int, ...]]]
B4 = Tuple[int]

@overload
def f4(arg: A4) -> int: ...
@overload
def f4(arg: B4) -> str: ...
def f4(arg: Union[A4, B4]) -> Union[int, str]:
...
[builtins fixtures/tuple.pyi]

[case testTypeVarTupleIndexOldStyleNonNormalizedAndNonLiteral]
from typing import Any, Tuple
from typing_extensions import Unpack
Expand Down

0 comments on commit 6427da6

Please sign in to comment.