From 6427da62add428d452f690aabbb6c272ff713710 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 10 Jun 2024 20:12:15 +0100 Subject: [PATCH] Properly handle unpacks in overlap checks (#17356) Fixes https://github.com/python/mypy/issues/17319 This is still not 100% robust, but at least it should not crash, and should cover correctly vast majority of cases. --- mypy/meet.py | 34 +++++++++++++++ test-data/unit/check-typevar-tuple.test | 57 +++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/mypy/meet.py b/mypy/meet.py index 2d44cafb23b3..48e5dfaa18ee 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -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( @@ -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": diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 21415abb9c28..2751e01aa21a 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -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