diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f5def57a9b1f..d48605a85fa7 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1886,6 +1886,11 @@ def infer_literal_expr_type(self, value: LiteralValue, fallback_name: str) -> Ty column=typ.column, )) + def concat_tuples(self, left: TupleType, right: TupleType) -> TupleType: + """Concatenate two fixed length tuples.""" + return TupleType(items=left.items + right.items, + fallback=self.named_type('builtins.tuple')) + def visit_int_expr(self, e: IntExpr) -> Type: """Type check an integer literal (trivial).""" return self.infer_literal_expr_type(e.value, 'builtins.int') @@ -1945,6 +1950,16 @@ def visit_op_expr(self, e: OpExpr) -> Type: return self.strfrm_checker.check_str_interpolation(e.left, e.right) left_type = self.accept(e.left) + proper_left_type = get_proper_type(left_type) + if isinstance(proper_left_type, TupleType) and e.op == '+': + left_add_method = proper_left_type.partial_fallback.type.get('__add__') + if left_add_method and left_add_method.fullname == 'builtins.tuple.__add__': + proper_right_type = get_proper_type(self.accept(e.right)) + if isinstance(proper_right_type, TupleType): + right_radd_method = proper_right_type.partial_fallback.type.get('__radd__') + if right_radd_method is None: + return self.concat_tuples(proper_left_type, proper_right_type) + if e.op in nodes.op_methods: method = self.get_operator_method(e.op) result, method_type = self.check_op(method, left_type, e.right, e, diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index e7f240e91926..99a3157337cb 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -738,7 +738,7 @@ class C: pass a = None # type: A -(a, a) + a # E: Unsupported left operand type for + ("Tuple[A, A]") +(a, a) + a # E: Unsupported operand types for + ("Tuple[A, A]" and "A") a + (a, a) # E: Unsupported operand types for + ("A" and "Tuple[A, A]") f((a, a)) # E: Argument 1 to "f" has incompatible type "Tuple[A, A]"; expected "A" (a, a).foo # E: "Tuple[A, A]" has no attribute "foo" @@ -1233,3 +1233,11 @@ reveal_type(tup[2]) # N: Revealed type is 'Union[Any, builtins.int*]' \ reveal_type(tup[:]) # N: Revealed type is 'Union[Tuple[builtins.int, builtins.str], builtins.list[builtins.int*]]' [builtins fixtures/tuple.pyi] + +[case testFixedLengthTupleConcatenation] +a = (1, "foo", 3) +b = ("bar", 7) + +reveal_type(a + b) # N: Revealed type is 'Tuple[builtins.int, builtins.str, builtins.int, builtins.str, builtins.int]' + +[builtins fixtures/tuple.pyi] \ No newline at end of file diff --git a/test-data/unit/fixtures/tuple.pyi b/test-data/unit/fixtures/tuple.pyi index 4e6c421a1615..6e000a7699fd 100644 --- a/test-data/unit/fixtures/tuple.pyi +++ b/test-data/unit/fixtures/tuple.pyi @@ -1,6 +1,6 @@ # Builtins stub used in tuple-related test cases. -from typing import Iterable, Iterator, TypeVar, Generic, Sequence, Any, overload +from typing import Iterable, Iterator, TypeVar, Generic, Sequence, Any, overload, Tuple Tco = TypeVar('Tco', covariant=True) @@ -15,6 +15,7 @@ class tuple(Sequence[Tco], Generic[Tco]): def __contains__(self, item: object) -> bool: pass def __getitem__(self, x: int) -> Tco: pass def __rmul__(self, n: int) -> tuple: pass + def __add__(self, x: Tuple[Tco, ...]) -> Tuple[Tco, ...]: pass def count(self, obj: Any) -> int: pass class function: pass class ellipsis: pass