Skip to content

Commit 6427da6

Browse files
authored
Properly handle unpacks in overlap checks (#17356)
Fixes #17319 This is still not 100% robust, but at least it should not crash, and should cover correctly vast majority of cases.
1 parent 83d54ff commit 6427da6

File tree

2 files changed

+91
-0
lines changed

2 files changed

+91
-0
lines changed

mypy/meet.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,19 @@ def are_tuples_overlapping(
611611
right = adjust_tuple(right, left) or right
612612
assert isinstance(left, TupleType), f"Type {left} is not a tuple"
613613
assert isinstance(right, TupleType), f"Type {right} is not a tuple"
614+
615+
# This algorithm works well if only one tuple is variadic, if both are
616+
# variadic we may get rare false negatives for overlapping prefix/suffix.
617+
# Also, this ignores empty unpack case, but it is probably consistent with
618+
# how we handle e.g. empty lists in overload overlaps.
619+
# TODO: write a more robust algorithm for cases where both types are variadic.
620+
left_unpack = find_unpack_in_list(left.items)
621+
right_unpack = find_unpack_in_list(right.items)
622+
if left_unpack is not None:
623+
left = expand_tuple_if_possible(left, len(right.items))
624+
if right_unpack is not None:
625+
right = expand_tuple_if_possible(right, len(left.items))
626+
614627
if len(left.items) != len(right.items):
615628
return False
616629
return all(
@@ -624,6 +637,27 @@ def are_tuples_overlapping(
624637
)
625638

626639

640+
def expand_tuple_if_possible(tup: TupleType, target: int) -> TupleType:
641+
if len(tup.items) > target + 1:
642+
return tup
643+
extra = target + 1 - len(tup.items)
644+
new_items = []
645+
for it in tup.items:
646+
if not isinstance(it, UnpackType):
647+
new_items.append(it)
648+
continue
649+
unpacked = get_proper_type(it.type)
650+
if isinstance(unpacked, TypeVarTupleType):
651+
instance = unpacked.tuple_fallback
652+
else:
653+
# Nested non-variadic tuples should be normalized at this point.
654+
assert isinstance(unpacked, Instance)
655+
instance = unpacked
656+
assert instance.type.fullname == "builtins.tuple"
657+
new_items.extend([instance.args[0]] * extra)
658+
return tup.copy_modified(items=new_items)
659+
660+
627661
def adjust_tuple(left: ProperType, r: ProperType) -> TupleType | None:
628662
"""Find out if `left` is a Tuple[A, ...], and adjust its length to `right`"""
629663
if isinstance(left, Instance) and left.type.fullname == "builtins.tuple":

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,6 +1808,63 @@ def test(a: Tuple[int, str], b: Tuple[bool], c: Tuple[bool, ...]):
18081808
reveal_type(add(b, c)) # N: Revealed type is "builtins.tuple[builtins.bool, ...]"
18091809
[builtins fixtures/tuple.pyi]
18101810

1811+
[case testTypeVarTupleOverloadOverlap]
1812+
from typing import Union, overload, Tuple
1813+
from typing_extensions import Unpack
1814+
1815+
class Int(int): ...
1816+
1817+
A = Tuple[int, Unpack[Tuple[int, ...]]]
1818+
B = Tuple[int, Unpack[Tuple[str, ...]]]
1819+
1820+
@overload
1821+
def f(arg: A) -> int: ...
1822+
@overload
1823+
def f(arg: B) -> str: ...
1824+
def f(arg: Union[A, B]) -> Union[int, str]:
1825+
...
1826+
1827+
A1 = Tuple[int, Unpack[Tuple[Int, ...]]]
1828+
B1 = Tuple[Unpack[Tuple[Int, ...]], int]
1829+
1830+
@overload
1831+
def f1(arg: A1) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
1832+
@overload
1833+
def f1(arg: B1) -> str: ...
1834+
def f1(arg: Union[A1, B1]) -> Union[int, str]:
1835+
...
1836+
1837+
A2 = Tuple[int, int, int]
1838+
B2 = Tuple[int, Unpack[Tuple[int, ...]]]
1839+
1840+
@overload
1841+
def f2(arg: A2) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
1842+
@overload
1843+
def f2(arg: B2) -> str: ...
1844+
def f2(arg: Union[A2, B2]) -> Union[int, str]:
1845+
...
1846+
1847+
A3 = Tuple[int, int, int]
1848+
B3 = Tuple[int, Unpack[Tuple[str, ...]]]
1849+
1850+
@overload
1851+
def f3(arg: A3) -> int: ...
1852+
@overload
1853+
def f3(arg: B3) -> str: ...
1854+
def f3(arg: Union[A3, B3]) -> Union[int, str]:
1855+
...
1856+
1857+
A4 = Tuple[int, int, Unpack[Tuple[int, ...]]]
1858+
B4 = Tuple[int]
1859+
1860+
@overload
1861+
def f4(arg: A4) -> int: ...
1862+
@overload
1863+
def f4(arg: B4) -> str: ...
1864+
def f4(arg: Union[A4, B4]) -> Union[int, str]:
1865+
...
1866+
[builtins fixtures/tuple.pyi]
1867+
18111868
[case testTypeVarTupleIndexOldStyleNonNormalizedAndNonLiteral]
18121869
from typing import Any, Tuple
18131870
from typing_extensions import Unpack

0 commit comments

Comments
 (0)