From 3b54a5eed95bb818b838d9d105b219089184363a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 16 Oct 2026 14:43:58 +0100 Subject: [PATCH] Unify multiple-TypeVarTuple error handling --- mypy/exprtotype.py | 2 + mypy/semanal.py | 52 +++++++++++-------------- mypy/typeanal.py | 9 ++++- test-data/unit/check-python312.test | 29 +++++++++++++- test-data/unit/check-type-aliases.test | 2 +- test-data/unit/check-typevar-tuple.test | 42 ++++++++++++++++++-- test-data/unit/semanal-errors.test | 2 +- 7 files changed, 100 insertions(+), 38 deletions(-) diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 9ec54ce44b675..8c42d205d5687 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -253,6 +253,8 @@ def expr_to_unanalyzed_type( expr.expr, options, allow_new_syntax, lookup_qualified=lookup_qualified ), from_star_syntax=True, + line=expr.line, + column=expr.column, ) elif isinstance(expr, DictExpr): if not expr.items: diff --git a/mypy/semanal.py b/mypy/semanal.py index a816108f59f43..d03e88eaa8396 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2338,17 +2338,10 @@ class Foo(Bar, Generic[T]): ... removed: list[int] = [] declared_tvars: TypeVarLikeList = [] is_protocol = False - has_type_var_tuple = False if defn.type_args is not None: for p in defn.type_args: node = self.get_defined_type_param(p.name) - assert node is not None assert isinstance(node, TypeVarLikeExpr) - if isinstance(node, TypeVarTupleExpr): - if has_type_var_tuple: - self.fail("Can only use one type var tuple in a class def", context) - continue - has_type_var_tuple = True declared_tvars.append((p.name, node)) for i, base_expr in enumerate(base_type_exprs): @@ -2361,7 +2354,7 @@ class Foo(Bar, Generic[T]): ... except TypeTranslationError: # This error will be caught later. continue - result = self.analyze_class_typevar_declaration(base, has_type_var_tuple) + result = self.analyze_class_typevar_declaration(base) if result is not None: tvars = result[0] is_protocol |= result[1] @@ -2412,12 +2405,11 @@ class Foo(Bar, Generic[T]): ... # grained incremental mode. defn.removed_base_type_exprs.append(defn.base_type_exprs[i]) del base_type_exprs[i] + declared_tvars = self.pre_validate_declared_tvars(declared_tvars, context, "class") tvar_defs = self.tvar_defs_from_tvars(declared_tvars, context) return base_type_exprs, tvar_defs, is_protocol, declared_tvars - def analyze_class_typevar_declaration( - self, base: Type, has_type_var_tuple: bool - ) -> tuple[TypeVarLikeList, bool] | None: + def analyze_class_typevar_declaration(self, base: Type) -> tuple[TypeVarLikeList, bool] | None: """Analyze type variables declared using Generic[...] or Protocol[...]. Args: @@ -2443,11 +2435,6 @@ def analyze_class_typevar_declaration( tag = self.track_incomplete_refs() tvar = self.analyze_unbound_tvar(arg) if tvar: - if isinstance(tvar[1], TypeVarTupleExpr): - if has_type_var_tuple: - self.fail("Can only use one type var tuple in a class def", base) - continue - has_type_var_tuple = True tvars.append(tvar) elif not self.found_incomplete_ref(tag): self.fail("Free type variable expected in %s[...]" % sym.node.name, base) @@ -4083,6 +4070,24 @@ def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Typ return typ.copy_modified(last_known_value=LiteralType(value=value, fallback=typ)) return typ + def pre_validate_declared_tvars( + self, declared_tvars: TypeVarLikeList, ctx: Context, kind: str + ) -> TypeVarLikeList: + has_type_var_tuple = False + valid_declared_tvars = [] + for name, node in declared_tvars: + if isinstance(node, TypeVarTupleExpr): + if has_type_var_tuple: + self.fail( + f"Can only use one TypeVarTuple in a {kind} definition", + ctx, + code=codes.TYPE_VAR, + ) + continue + has_type_var_tuple = True + valid_declared_tvars.append((name, node)) + return valid_declared_tvars + def analyze_alias( self, name: str, @@ -4119,6 +4124,7 @@ def analyze_alias( found_type_vars = self.find_type_var_likes(typ) namespace = self.qualified_name(name) alias_type_vars = found_type_vars if declared_type_vars is None else declared_type_vars + alias_type_vars = self.pre_validate_declared_tvars(alias_type_vars, rvalue, "type alias") with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): tvar_defs = self.tvar_defs_from_tvars(alias_type_vars, typ) @@ -4426,7 +4432,6 @@ def analyze_type_alias_type_params( declared_tvars: TypeVarLikeList = [] all_declared_tvar_names: list[str] = [] # includes bound type variables - have_type_var_tuple = False for tp_expr in type_params: if isinstance(tp_expr, StarExpr): tp_expr.valid = False @@ -4440,18 +4445,7 @@ def analyze_type_alias_type_params( tag = self.track_incomplete_refs() tvar = self.analyze_unbound_tvar_impl(base, is_typealias_param=True) - if tvar: - if isinstance(tvar[1], TypeVarTupleExpr): - if have_type_var_tuple: - self.fail( - "Can only use one TypeVarTuple in type_params argument to TypeAliasType", - base, - code=codes.TYPE_VAR, - ) - have_type_var_tuple = True - continue - have_type_var_tuple = True - elif not self.found_incomplete_ref(tag): + if not tvar and not self.found_incomplete_ref(tag): sym = self.lookup_qualified(base.name, base) if sym and isinstance(sym.node, TypeVarLikeExpr): all_declared_tvar_names.append(sym.node.name) # Error will be reported later diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 6717f4f18c90b..bd6f97cc94cef 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1180,7 +1180,12 @@ def visit_unpack_type(self, t: UnpackType) -> Type: self.fail(message_registry.INVALID_UNPACK_POSITION, t.type, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) self.allow_type_var_tuple = self.nesting_level + 1 - result = UnpackType(self.anal_type(t.type), from_star_syntax=t.from_star_syntax) + result = UnpackType( + self.anal_type(t.type), + from_star_syntax=t.from_star_syntax, + line=t.line, + column=t.column, + ) self.allow_type_var_tuple = -1 return result @@ -2078,7 +2083,7 @@ def check_unpacks_in_list(self, items: list[Type]) -> list[Type]: if num_unpacks > 1: assert final_unpack is not None - self.fail("More than one variadic Unpack in a type is not allowed", final_unpack.type) + self.fail("More than one variadic Unpack in a type is not allowed", final_unpack) return new_items def tuple_type(self, items: list[Type], line: int, column: int) -> TupleType: diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 7252969ff2aaa..8db9e67625d12 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -2288,11 +2288,11 @@ from typing_extensions import TypeVarTuple, Unpack Us = TypeVarTuple("Us") -class C[*Ts, *Us]: # E: Can only use one type var tuple in a class def +class C[*Ts, *Us]: # E: Can only use one TypeVarTuple in a class definition pass class D[*Ts](Generic[Unpack[Us]]): # E: Generic[...] base class is redundant \ - # E: Can only use one type var tuple in a class def + # E: Can only use one TypeVarTuple in a class definition pass [builtins fixtures/tuple.pyi] @@ -2438,3 +2438,28 @@ def test[*Us](*xs: *Us) -> None: # TODO: we should erase last_known_values from Parameters. reveal_type(print_args(2, *xs, 4)) # N: Revealed type is "__main__.C[[Literal[2]?, *args: *tuple[*Us, Literal[4]?]], tuple[Literal[2]?, *Us, Literal[4]?]]" [builtins fixtures/paramspec.pyi] + +[case testDoubleTypeVarTupleNoCrashNewSyntax] +class C[*T, *U](T, U): # E: Can only use one TypeVarTuple in a class definition \ + # E: TypeVarTuple "T" is only valid with an unpack \ + # E: TypeVarTuple "U" is unbound + x: tuple[*T] + y: tuple[*T, *U] # E: More than one variadic Unpack in a type is not allowed \ + # E: TypeVarTuple "U" is unbound + z: tuple[*U] # E: TypeVarTuple "U" is unbound + +type A[*T, *U] = tuple[tuple[*T], tuple[*T, *U], tuple[*U]] # E: Can only use one TypeVarTuple in a type alias definition \ + # E: More than one variadic Unpack in a type is not allowed \ + # E: TypeVarTuple "U" is unbound + +c: C[int, str] +a: A[int, str] + +reveal_type(c.x) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(c.y) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(c.z) # N: Revealed type is "builtins.tuple[Any, ...]" + +reveal_type(a[0]) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(a[1]) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(a[2]) # N: Revealed type is "builtins.tuple[Any, ...]" +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 3f712eae7c54d..156fe78edab12 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -1186,7 +1186,7 @@ partially_generic1: Ta3[int] = {"a": 1} reveal_type(partially_generic1) # N: Revealed type is "builtins.dict[Any, builtins.int]" partially_generic2: Ta3[int] = {1: "a"} # E: Dict entry 0 has incompatible type "int": "str"; expected "Any": "int" -Ta4 = TypeAliasType("Ta4", Tuple[Unpack[Ts]], type_params=(Ts, Ts1)) # E: Can only use one TypeVarTuple in type_params argument to TypeAliasType +Ta4 = TypeAliasType("Ta4", Tuple[Unpack[Ts]], type_params=(Ts, Ts1)) # E: Can only use one TypeVarTuple in a type alias definition Ta5 = TypeAliasType("Ta5", Dict) # Unlike old style aliases, this is not generic non_generic_dict: Ta5[int, str] # E: Bad number of arguments for type alias, expected 0, given 2 diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 5449613aa3c50..c298e8e118d7e 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -725,15 +725,21 @@ Ts = TypeVarTuple("Ts") Us = TypeVarTuple("Us") class G(Generic[Unpack[Ts]]): ... -A = Tuple[Unpack[Ts], Unpack[Us]] # E: More than one variadic Unpack in a type is not allowed +A = Tuple[Unpack[Ts], Unpack[Us]] # E: Can only use one TypeVarTuple in a type alias definition \ + # E: More than one variadic Unpack in a type is not allowed \ + # E: TypeVarTuple "Us" is unbound x: A[int, str] reveal_type(x) # N: Revealed type is "tuple[builtins.int, builtins.str]" -B = Callable[[Unpack[Ts], Unpack[Us]], int] # E: More than one variadic Unpack in a type is not allowed +B = Callable[[Unpack[Ts], Unpack[Us]], int] # E: Can only use one TypeVarTuple in a type alias definition \ + # E: More than one variadic Unpack in a type is not allowed \ + # E: TypeVarTuple "Us" is unbound y: B[int, str] reveal_type(y) # N: Revealed type is "def (builtins.int, builtins.str) -> builtins.int" -C = G[Unpack[Ts], Unpack[Us]] # E: More than one variadic Unpack in a type is not allowed +C = G[Unpack[Ts], Unpack[Us]] # E: Can only use one TypeVarTuple in a type alias definition \ + # E: More than one variadic Unpack in a type is not allowed \ + # E: TypeVarTuple "Us" is unbound z: C[int, str] reveal_type(z) # N: Revealed type is "__main__.G[builtins.int, builtins.str]" [builtins fixtures/tuple.pyi] @@ -3273,3 +3279,33 @@ reveal_type(x) # N: Revealed type is "__main__.C[builtins.int, builtins.int]" y: Union[Any, C[Unpack[tuple[int, ...]]]] reveal_type(bar(y)) # N: Revealed type is "builtins.tuple[Any, ...]" [builtins fixtures/tuple.pyi] + +[case testDoubleTypeVarTupleNoCrashOldSyntax] +from typing import Generic, TypeVarTuple, Unpack + +T = TypeVarTuple("T") +U = TypeVarTuple("U") + +class C(T, U, Generic[Unpack[T], Unpack[U]]): # E: Can only use one TypeVarTuple in a class definition \ + # E: TypeVarTuple "T" is only valid with an unpack \ + # E: TypeVarTuple "U" is unbound + x: tuple[Unpack[T]] + y: tuple[Unpack[T], Unpack[U]] # E: More than one variadic Unpack in a type is not allowed \ + # E: TypeVarTuple "U" is unbound + z: tuple[Unpack[U]] # E: TypeVarTuple "U" is unbound + +A = tuple[tuple[Unpack[T]], tuple[Unpack[T], Unpack[U]], tuple[Unpack[U]]] # E: Can only use one TypeVarTuple in a type alias definition \ + # E: More than one variadic Unpack in a type is not allowed \ + # E: TypeVarTuple "U" is unbound + +c: C[int, str] +a: A[int, str] + +reveal_type(c.x) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(c.y) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(c.z) # N: Revealed type is "builtins.tuple[Any, ...]" + +reveal_type(a[0]) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(a[1]) # N: Revealed type is "tuple[builtins.int, builtins.str]" +reveal_type(a[2]) # N: Revealed type is "builtins.tuple[Any, ...]" +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index 62669ed3f1fc7..a2b562ac88c63 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -1502,7 +1502,7 @@ x: TVariadic # E: TypeVarTuple "TVariadic" is unbound y: Unpack[TVariadic] # E: Unpack is only valid in a variadic position -class Variadic(Generic[Unpack[TVariadic], Unpack[TVariadic2]]): # E: Can only use one type var tuple in a class def +class Variadic(Generic[Unpack[TVariadic], Unpack[TVariadic2]]): # E: Can only use one TypeVarTuple in a class definition pass def bad_args(*args: TVariadic): # E: TypeVarTuple "TVariadic" is only valid with an unpack