Skip to content

Commit e5c1e86

Browse files
committed
Fix joining of fixed-length tuples with mismatching lengths
For example: Tuple[bool, int] + Tuple[bool] becomes Tuple[int, ...] Previously Mypy simply punted and returned `object`. The handling of fixed tuple + variadic tuple will be implemented separately.
1 parent ea3c65c commit e5c1e86

File tree

3 files changed

+71
-6
lines changed

3 files changed

+71
-6
lines changed

mypy/join.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -260,14 +260,23 @@ def visit_overloaded(self, t: Overloaded) -> ProperType:
260260
return join_types(t.fallback, s)
261261

262262
def visit_tuple_type(self, t: TupleType) -> ProperType:
263-
if isinstance(self.s, TupleType) and self.s.length() == t.length():
264-
items = [] # type: List[Type]
265-
for i in range(t.length()):
266-
items.append(self.join(t.items[i], self.s.items[i]))
263+
# When given two fixed-length tuples:
264+
# * If lengths match, join their subtypes item-wise:
265+
# Tuple[int, bool] + Tuple[bool, bool] becomes Tuple[int, bool]
266+
# * If lengths do not match, return a variadic tuple:
267+
# Tuple[bool, int] + Tuple[bool] becomes Tuple[int, ...]
268+
# * Fixed tuple + variadic tuple is currently not implemented.
269+
if isinstance(self.s, TupleType):
267270
fallback = join_instances(mypy.typeops.tuple_fallback(self.s),
268271
mypy.typeops.tuple_fallback(t))
269272
assert isinstance(fallback, Instance)
270-
return TupleType(items, fallback)
273+
if self.s.length() == t.length():
274+
items = [] # type: List[Type]
275+
for i in range(t.length()):
276+
items.append(self.join(t.items[i], self.s.items[i]))
277+
return TupleType(items, fallback)
278+
else:
279+
return fallback
271280
else:
272281
return self.default(self.s)
273282

mypy/test/testtypes.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,13 @@ def test_tuples(self) -> None:
504504
self.fx.o)
505505
self.assert_join(self.tuple(self.fx.a),
506506
self.tuple(self.fx.a, self.fx.a),
507-
self.fx.o)
507+
self.var_tuple(self.fx.a))
508+
self.assert_join(self.tuple(self.fx.b),
509+
self.tuple(self.fx.a, self.fx.c),
510+
self.var_tuple(self.fx.a))
511+
self.assert_join(self.tuple(),
512+
self.tuple(self.fx.a),
513+
self.var_tuple(self.fx.a))
508514

509515
def test_function_types(self) -> None:
510516
self.assert_join(self.callable(self.fx.a, self.fx.b),
@@ -760,6 +766,10 @@ def assert_simple_join(self, s: Type, t: Type, join: Type) -> None:
760766
def tuple(self, *a: Type) -> TupleType:
761767
return TupleType(list(a), self.fx.std_tuple)
762768

769+
def var_tuple(self, t: Type) -> Instance:
770+
"""Construct a variable-length tuple type"""
771+
return Instance(self.fx.std_tuplei, [t])
772+
763773
def callable(self, *a: Type) -> CallableType:
764774
"""callable(a1, ..., an, r) constructs a callable with argument types
765775
a1, ... an and return type r.

test-data/unit/check-tuples.test

+46
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,52 @@ x, y = g(z) # E: Argument 1 to "g" has incompatible type "int"; expected "Tuple[
10771077
[builtins fixtures/tuple.pyi]
10781078
[out]
10791079

1080+
[case testTupleJoinIrregular]
1081+
from typing import Tuple
1082+
1083+
tup1 = None # type: Tuple[bool, int]
1084+
tup2 = None # type: Tuple[bool]
1085+
1086+
reveal_type(tup1 if int() else tup2) # N: Revealed type is 'builtins.tuple[builtins.int]'
1087+
reveal_type(tup2 if int() else tup1) # N: Revealed type is 'builtins.tuple[builtins.int]'
1088+
1089+
reveal_type(tup1 if int() else ()) # N: Revealed type is 'builtins.tuple[builtins.int]'
1090+
reveal_type(() if int() else tup1) # N: Revealed type is 'builtins.tuple[builtins.int]'
1091+
1092+
reveal_type(tup2 if int() else ()) # N: Revealed type is 'builtins.tuple[builtins.bool]'
1093+
reveal_type(() if int() else tup2) # N: Revealed type is 'builtins.tuple[builtins.bool]'
1094+
1095+
[builtins fixtures/tuple.pyi]
1096+
[out]
1097+
1098+
[case testTupleSubclassJoinIrregular]
1099+
from typing import Tuple, NamedTuple
1100+
1101+
class NTup1(NamedTuple):
1102+
a: bool
1103+
1104+
class NTup2(NTup1):
1105+
b: bool
1106+
1107+
class SubTuple(Tuple[bool, int, int]): ...
1108+
1109+
tup1 = None # type: NTup1
1110+
tup2 = None # type: NTup2
1111+
subtup = None # type: SubTuple
1112+
1113+
# FIXME NamedTuple inheritance has some unexpected behavior
1114+
reveal_type(tup1 if int() else tup2) # N: Revealed type is 'Tuple[builtins.bool, fallback=__main__.NTup1]'
1115+
reveal_type(tup2 if int() else tup1) # N: Revealed type is 'Tuple[builtins.bool, fallback=__main__.NTup1]'
1116+
1117+
reveal_type(tup1 if int() else subtup) # N: Revealed type is 'builtins.tuple[builtins.int]'
1118+
reveal_type(subtup if int() else tup1) # N: Revealed type is 'builtins.tuple[builtins.int]'
1119+
1120+
reveal_type(tup2 if int() else subtup) # N: Revealed type is 'builtins.tuple[builtins.int]'
1121+
reveal_type(subtup if int() else tup2) # N: Revealed type is 'builtins.tuple[builtins.int]'
1122+
1123+
[builtins fixtures/tuple.pyi]
1124+
[out]
1125+
10801126
[case testTupleWithUndersizedContext]
10811127
a = ([1], 'x')
10821128
if int():

0 commit comments

Comments
 (0)