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

Properly handle unpacks in overlap checks #17356

Merged
merged 1 commit into from
Jun 10, 2024
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
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
Loading