diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f9fbd53866da..13373474786b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -151,11 +151,13 @@ UninhabitedType, UnionType, UnpackType, + flatten_nested_tuples, flatten_nested_unions, get_proper_type, get_proper_types, has_recursive_types, is_named_instance, + split_with_prefix_and_suffix, ) from mypy.types_utils import is_generic_instance, is_optional, is_self_type_like, remove_optional from mypy.typestate import type_state @@ -4070,6 +4072,35 @@ class LongName(Generic[T]): ... # The _SpecialForm type can be used in some runtime contexts (e.g. it may have __or__). return self.named_type("typing._SpecialForm") + def split_for_callable( + self, t: CallableType, args: Sequence[Type], ctx: Context + ) -> list[Type]: + """Handle directly applying type arguments to a variadic Callable. + + This is needed in situations where e.g. variadic class object appears in + runtime context. For example: + class C(Generic[T, Unpack[Ts]]): ... + x = C[int, str]() + + We simply group the arguments that need to go into Ts variable into a TupleType, + similar to how it is done in other places using split_with_prefix_and_suffix(). + """ + vars = t.variables + if not vars or not any(isinstance(v, TypeVarTupleType) for v in vars): + return list(args) + + prefix = next(i for (i, v) in enumerate(vars) if isinstance(v, TypeVarTupleType)) + suffix = len(vars) - prefix - 1 + args = flatten_nested_tuples(args) + if len(args) < len(vars) - 1: + self.msg.incompatible_type_application(len(vars), len(args), ctx) + return [AnyType(TypeOfAny.from_error)] * len(vars) + + tvt = vars[prefix] + assert isinstance(tvt, TypeVarTupleType) + start, middle, end = split_with_prefix_and_suffix(tuple(args), prefix, suffix) + return list(start) + [TupleType(list(middle), tvt.tuple_fallback)] + list(end) + def apply_type_arguments_to_callable( self, tp: Type, args: Sequence[Type], ctx: Context ) -> Type: @@ -4083,19 +4114,28 @@ def apply_type_arguments_to_callable( tp = get_proper_type(tp) if isinstance(tp, CallableType): - if len(tp.variables) != len(args): + if len(tp.variables) != len(args) and not any( + isinstance(v, TypeVarTupleType) for v in tp.variables + ): if tp.is_type_obj() and tp.type_object().fullname == "builtins.tuple": # TODO: Specialize the callable for the type arguments return tp self.msg.incompatible_type_application(len(tp.variables), len(args), ctx) return AnyType(TypeOfAny.from_error) - return self.apply_generic_arguments(tp, args, ctx) + return self.apply_generic_arguments(tp, self.split_for_callable(tp, args, ctx), ctx) if isinstance(tp, Overloaded): for it in tp.items: - if len(it.variables) != len(args): + if len(it.variables) != len(args) and not any( + isinstance(v, TypeVarTupleType) for v in it.variables + ): self.msg.incompatible_type_application(len(it.variables), len(args), ctx) return AnyType(TypeOfAny.from_error) - return Overloaded([self.apply_generic_arguments(it, args, ctx) for it in tp.items]) + return Overloaded( + [ + self.apply_generic_arguments(it, self.split_for_callable(it, args, ctx), ctx) + for it in tp.items + ] + ) return AnyType(TypeOfAny.special_form) def visit_list_expr(self, e: ListExpr) -> Type: diff --git a/mypy/constraints.py b/mypy/constraints.py index 9a662f1004f7..33230871b505 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterable, List, Sequence +from typing import TYPE_CHECKING, Iterable, List, Sequence, cast from typing_extensions import Final import mypy.subtypes @@ -46,15 +46,11 @@ has_recursive_types, has_type_vars, is_named_instance, + split_with_prefix_and_suffix, ) from mypy.types_utils import is_union_with_any from mypy.typestate import type_state -from mypy.typevartuples import ( - extract_unpack, - find_unpack_in_list, - split_with_mapped_and_template, - split_with_prefix_and_suffix, -) +from mypy.typevartuples import extract_unpack, find_unpack_in_list, split_with_mapped_and_template if TYPE_CHECKING: from mypy.infer import ArgumentInferContext @@ -669,7 +665,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]: instance.type.type_var_tuple_prefix, instance.type.type_var_tuple_suffix, ) - tvars = list(tvars_prefix + tvars_suffix) + tvars = cast("list[TypeVarLikeType]", list(tvars_prefix + tvars_suffix)) else: mapped_args = mapped.args instance_args = instance.args @@ -738,7 +734,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]: template.type.type_var_tuple_prefix, template.type.type_var_tuple_suffix, ) - tvars = list(tvars_prefix + tvars_suffix) + tvars = cast("list[TypeVarLikeType]", list(tvars_prefix + tvars_suffix)) else: mapped_args = mapped.args template_args = template.args diff --git a/mypy/expandtype.py b/mypy/expandtype.py index d9e87082184d..7d7af80ccb2b 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -37,14 +37,12 @@ UninhabitedType, UnionType, UnpackType, + flatten_nested_tuples, flatten_nested_unions, get_proper_type, -) -from mypy.typevartuples import ( - find_unpack_in_list, - split_with_instance, split_with_prefix_and_suffix, ) +from mypy.typevartuples import find_unpack_in_list, split_with_instance # WARNING: these functions should never (directly or indirectly) depend on # is_subtype(), meet_types(), join_types() etc. @@ -115,6 +113,7 @@ def expand_type_by_instance(typ: Type, instance: Instance) -> Type: instance_args = instance.args for binder, arg in zip(tvars, instance_args): + assert isinstance(binder, TypeVarLikeType) variables[binder.id] = arg return expand_type(typ, variables) @@ -282,12 +281,14 @@ def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: raise NotImplementedError def visit_unpack_type(self, t: UnpackType) -> Type: - # It is impossible to reasonally implement visit_unpack_type, because + # It is impossible to reasonably implement visit_unpack_type, because # unpacking inherently expands to something more like a list of types. # # Relevant sections that can call unpack should call expand_unpack() # instead. - assert False, "Mypy bug: unpacking must happen at a higher level" + # However, if the item is a variadic tuple, we can simply carry it over. + # it is hard to assert this without getting proper type. + return UnpackType(t.type.accept(self)) def expand_unpack(self, t: UnpackType) -> list[Type] | Instance | AnyType | None: return expand_unpack_with_variables(t, self.variables) @@ -356,7 +357,15 @@ def interpolate_args_for_unpack( # Extract the typevartuple so we can get a tuple fallback from it. expanded_unpacked_tvt = expanded_unpack.type - assert isinstance(expanded_unpacked_tvt, TypeVarTupleType) + if isinstance(expanded_unpacked_tvt, TypeVarTupleType): + fallback = expanded_unpacked_tvt.tuple_fallback + else: + # This can happen when tuple[Any, ...] is used to "patch" a variadic + # generic type without type arguments provided. + assert isinstance(expanded_unpacked_tvt, ProperType) + assert isinstance(expanded_unpacked_tvt, Instance) + assert expanded_unpacked_tvt.type.fullname == "builtins.tuple" + fallback = expanded_unpacked_tvt prefix_len = expanded_unpack_index arg_names = t.arg_names[:star_index] + [None] * prefix_len + t.arg_names[star_index:] @@ -368,11 +377,7 @@ def interpolate_args_for_unpack( + expanded_items[:prefix_len] # Constructing the Unpack containing the tuple without the prefix. + [ - UnpackType( - TupleType( - expanded_items[prefix_len:], expanded_unpacked_tvt.tuple_fallback - ) - ) + UnpackType(TupleType(expanded_items[prefix_len:], fallback)) if len(expanded_items) - prefix_len > 1 else expanded_items[0] ] @@ -456,9 +461,12 @@ def expand_types_with_unpack( indicates use of Any or some error occurred earlier. In this case callers should simply propagate the resulting type. """ + # TODO: this will cause a crash on aliases like A = Tuple[int, Unpack[A]]. + # Although it is unlikely anyone will write this, we should fail gracefully. + typs = flatten_nested_tuples(typs) items: list[Type] = [] for item in typs: - if isinstance(item, UnpackType): + if isinstance(item, UnpackType) and isinstance(item.type, TypeVarTupleType): unpacked_items = self.expand_unpack(item) if unpacked_items is None: # TODO: better error, something like tuple of unknown? @@ -523,7 +531,11 @@ def visit_type_type(self, t: TypeType) -> Type: def visit_type_alias_type(self, t: TypeAliasType) -> Type: # Target of the type alias cannot contain type variables (not bound by the type # alias itself), so we just expand the arguments. - return t.copy_modified(args=self.expand_types(t.args)) + args = self.expand_types_with_unpack(t.args) + if isinstance(args, list): + return t.copy_modified(args=args) + else: + return args def expand_types(self, types: Iterable[Type]) -> list[Type]: a: list[Type] = [] diff --git a/mypy/nodes.py b/mypy/nodes.py index 414b5c190aa0..330e28b0fa2f 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3471,6 +3471,7 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here "normalized", "_is_recursive", "eager", + "tvar_tuple_index", ) __match_args__ = ("name", "target", "alias_tvars", "no_args") @@ -3498,6 +3499,10 @@ def __init__( # it is the cached value. self._is_recursive: bool | None = None self.eager = eager + self.tvar_tuple_index = None + for i, t in enumerate(alias_tvars): + if isinstance(t, mypy.types.TypeVarTupleType): + self.tvar_tuple_index = i super().__init__(line, column) @classmethod diff --git a/mypy/semanal.py b/mypy/semanal.py index 70bd876af46e..648852fdecc8 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -270,6 +270,7 @@ TypeOfAny, TypeType, TypeVarLikeType, + TypeVarTupleType, TypeVarType, UnboundType, UnpackType, @@ -3424,8 +3425,18 @@ def analyze_alias( allowed_alias_tvars=tvar_defs, ) + # There can be only one variadic variable at most, the error is reported elsewhere. + new_tvar_defs = [] + variadic = False + for td in tvar_defs: + if isinstance(td, TypeVarTupleType): + if variadic: + continue + variadic = True + new_tvar_defs.append(td) + qualified_tvars = [node.fullname for _name, node in found_type_vars] - return analyzed, tvar_defs, depends_on, qualified_tvars + return analyzed, new_tvar_defs, depends_on, qualified_tvars def is_pep_613(self, s: AssignmentStmt) -> bool: if s.unanalyzed_type is not None and isinstance(s.unanalyzed_type, UnboundType): diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index 5d66c03aa33e..e188955dabbb 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -18,6 +18,7 @@ from mypy.options import Options from mypy.scope import Scope from mypy.subtypes import is_same_type, is_subtype +from mypy.typeanal import set_any_tvars from mypy.types import ( AnyType, Instance, @@ -32,8 +33,10 @@ TypeVarType, UnboundType, UnpackType, + flatten_nested_tuples, get_proper_type, get_proper_types, + split_with_prefix_and_suffix, ) @@ -79,10 +82,34 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None: self.seen_aliases.add(t) # Some recursive aliases may produce spurious args. In principle this is not very # important, as we would simply ignore them when expanding, but it is better to keep - # correct aliases. - if t.alias and len(t.args) != len(t.alias.alias_tvars): - t.args = [AnyType(TypeOfAny.from_error) for _ in t.alias.alias_tvars] + # correct aliases. Also, variadic aliases are better to check when fully analyzed, + # so we do this here. assert t.alias is not None, f"Unfixed type alias {t.type_ref}" + args = flatten_nested_tuples(t.args) + if t.alias.tvar_tuple_index is not None: + correct = len(args) >= len(t.alias.alias_tvars) - 1 + if any( + isinstance(a, UnpackType) and isinstance(get_proper_type(a.type), Instance) + for a in args + ): + correct = True + else: + correct = len(args) == len(t.alias.alias_tvars) + if not correct: + if t.alias.tvar_tuple_index is not None: + exp_len = f"at least {len(t.alias.alias_tvars) - 1}" + else: + exp_len = f"{len(t.alias.alias_tvars)}" + self.fail( + f"Bad number of arguments for type alias, expected: {exp_len}, given: {len(args)}", + t, + code=codes.TYPE_ARG, + ) + t.args = set_any_tvars( + t.alias, t.line, t.column, self.options, from_error=True, fail=self.fail + ).args + else: + t.args = args is_error = self.validate_args(t.alias.name, t.args, t.alias.alias_tvars, t) if not is_error: # If there was already an error for the alias itself, there is no point in checking @@ -101,6 +128,17 @@ def visit_instance(self, t: Instance) -> None: def validate_args( self, name: str, args: Sequence[Type], type_vars: list[TypeVarLikeType], ctx: Context ) -> bool: + # TODO: we need to do flatten_nested_tuples and validate arg count for instances + # similar to how do we do this for type aliases above, but this may have perf penalty. + if any(isinstance(v, TypeVarTupleType) for v in type_vars): + prefix = next(i for (i, v) in enumerate(type_vars) if isinstance(v, TypeVarTupleType)) + tvt = type_vars[prefix] + assert isinstance(tvt, TypeVarTupleType) + start, middle, end = split_with_prefix_and_suffix( + tuple(args), prefix, len(type_vars) - prefix - 1 + ) + args = list(start) + [TupleType(list(middle), tvt.tuple_fallback)] + list(end) + is_error = False for (i, arg), tvar in zip(enumerate(args), type_vars): if isinstance(tvar, TypeVarType): @@ -167,7 +205,11 @@ def visit_unpack_type(self, typ: UnpackType) -> None: return if isinstance(proper_type, Instance) and proper_type.type.fullname == "builtins.tuple": return - if isinstance(proper_type, AnyType) and proper_type.type_of_any == TypeOfAny.from_error: + if ( + isinstance(proper_type, UnboundType) + or isinstance(proper_type, AnyType) + and proper_type.type_of_any == TypeOfAny.from_error + ): return # TODO: Infer something when it can't be unpacked to allow rest of diff --git a/mypy/subtypes.py b/mypy/subtypes.py index b26aee1a92af..a3b28a3e24de 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -661,6 +661,8 @@ def visit_type_var_tuple(self, left: TypeVarTupleType) -> bool: def visit_unpack_type(self, left: UnpackType) -> bool: if isinstance(self.right, UnpackType): return self._is_subtype(left.type, self.right.type) + if isinstance(self.right, Instance) and self.right.type.fullname == "builtins.object": + return True return False def visit_parameters(self, left: Parameters) -> bool: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 5b51d07dfde4..95acb71b45d2 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -366,6 +366,8 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) return AnyType(TypeOfAny.from_error) if isinstance(sym.node, TypeVarTupleExpr): if tvar_def is None: + if self.allow_unbound_tvars: + return t self.fail(f'TypeVarTuple "{t.name}" is unbound', t, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) assert isinstance(tvar_def, TypeVarTupleType) @@ -407,6 +409,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) self.options, unexpanded_type=t, disallow_any=disallow_any, + empty_tuple_index=t.empty_tuple_index, ) # The only case where instantiate_type_alias() can return an incorrect instance is # when it is top-level instance, so no need to recurse. @@ -414,6 +417,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) isinstance(res, Instance) # type: ignore[misc] and len(res.args) != len(res.type.type_vars) and not self.defining_alias + and not res.type.has_type_var_tuple_type ): fix_instance( res, @@ -941,8 +945,15 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: ] else: arg_types = self.anal_array(t.arg_types, nested=nested) + # If there were multiple (invalid) unpacks, the arg types list will become shorter, + # we need to trim the kinds/names as well to avoid crashes. + arg_kinds = t.arg_kinds[: len(arg_types)] + arg_names = t.arg_names[: len(arg_types)] + ret = t.copy_modified( arg_types=arg_types, + arg_kinds=arg_kinds, + arg_names=arg_names, ret_type=self.anal_type(t.ret_type, nested=nested), # If the fallback isn't filled in yet, # its type will be the falsey FakeInfo @@ -1272,7 +1283,6 @@ def analyze_callable_args( args: list[Type] = [] kinds: list[ArgKind] = [] names: list[str | None] = [] - found_unpack = False for arg in arglist.items: if isinstance(arg, CallableArgument): args.append(arg.typ) @@ -1299,9 +1309,6 @@ def analyze_callable_args( sym = self.lookup_qualified(arg.name, arg) if sym is not None: if sym.fullname in ("typing_extensions.Unpack", "typing.Unpack"): - if found_unpack: - self.fail("Callables can only have a single unpack", arg) - found_unpack = True kind = ARG_STAR args.append(arg) kinds.append(kind) @@ -1581,7 +1588,9 @@ def check_unpacks_in_list(self, items: list[Type]) -> list[Type]: num_unpacks = 0 final_unpack = None for item in items: - if isinstance(item, UnpackType): + if isinstance(item, UnpackType) and not isinstance( + get_proper_type(item.type), TupleType + ): if not num_unpacks: new_items.append(item) num_unpacks += 1 @@ -1724,6 +1733,7 @@ def instantiate_type_alias( unexpanded_type: Type | None = None, disallow_any: bool = False, use_standard_error: bool = False, + empty_tuple_index: bool = False, ) -> Type: """Create an instance of a (generic) type alias from alias node and type arguments. @@ -1739,7 +1749,11 @@ def instantiate_type_alias( """ exp_len = len(node.alias_tvars) act_len = len(args) - if exp_len > 0 and act_len == 0: + if ( + exp_len > 0 + and act_len == 0 + and not (empty_tuple_index and node.tvar_tuple_index is not None) + ): # Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...] return set_any_tvars( node, @@ -1767,7 +1781,7 @@ def instantiate_type_alias( tp.line = ctx.line tp.column = ctx.column return tp - if act_len != exp_len: + if act_len != exp_len and node.tvar_tuple_index is None: if use_standard_error: # This is used if type alias is an internal representation of another type, # for example a generic TypedDict or NamedTuple. @@ -1802,7 +1816,7 @@ def set_any_tvars( disallow_any: bool = False, fail: MsgCallback | None = None, unexpanded_type: Type | None = None, -) -> Type: +) -> TypeAliasType: if from_error or disallow_any: type_of_any = TypeOfAny.from_error else: @@ -1824,7 +1838,14 @@ def set_any_tvars( code=codes.TYPE_ARG, ) any_type = AnyType(type_of_any, line=newline, column=newcolumn) - return TypeAliasType(node, [any_type] * len(node.alias_tvars), newline, newcolumn) + + args: list[Type] = [] + for tv in node.alias_tvars: + if isinstance(tv, TypeVarTupleType): + args.append(UnpackType(Instance(tv.tuple_fallback.type, [any_type]))) + else: + args.append(any_type) + return TypeAliasType(node, args, newline, newcolumn) def remove_dups(tvars: list[T]) -> list[T]: @@ -1929,7 +1950,11 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type: assert t.alias is not None, f"Unfixed type alias {t.type_ref}" if t.alias in self.seen_nodes: for arg in t.args: - if not isinstance(arg, TypeVarLikeType) and has_type_vars(arg): + if not ( + isinstance(arg, TypeVarLikeType) + or isinstance(arg, UnpackType) + and isinstance(arg.type, TypeVarLikeType) + ) and has_type_vars(arg): self.diverging = True return t # All clear for this expansion chain. @@ -2073,7 +2098,7 @@ def __init__(self, fail: MsgCallback, note: MsgCallback, options: Options) -> No def visit_instance(self, typ: Instance) -> None: super().visit_instance(typ) - if len(typ.args) != len(typ.type.type_vars): + if len(typ.args) != len(typ.type.type_vars) and not typ.type.has_type_var_tuple_type: fix_instance( typ, self.fail, diff --git a/mypy/typeops.py b/mypy/typeops.py index 43740c75af40..ee544c6740bb 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -103,6 +103,11 @@ def tuple_fallback(typ: TupleType) -> Instance: # TODO: might make sense to do recursion here to support nested unpacks # of tuple constants items.extend(unpacked_type.items) + elif ( + isinstance(unpacked_type, Instance) + and unpacked_type.type.fullname == "builtins.tuple" + ): + items.append(unpacked_type.args[0]) else: raise NotImplementedError else: diff --git a/mypy/types.py b/mypy/types.py index f23800234600..0e1374466341 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -315,10 +315,22 @@ def _expand_once(self) -> Type: # as their target. assert isinstance(self.alias.target, Instance) # type: ignore[misc] return self.alias.target.copy_modified(args=self.args) - replacer = InstantiateAliasVisitor( - {v.id: s for (v, s) in zip(self.alias.alias_tvars, self.args)} - ) - new_tp = self.alias.target.accept(replacer) + + if self.alias.tvar_tuple_index is None: + mapping = {v.id: s for (v, s) in zip(self.alias.alias_tvars, self.args)} + else: + prefix = self.alias.tvar_tuple_index + suffix = len(self.alias.alias_tvars) - self.alias.tvar_tuple_index - 1 + start, middle, end = split_with_prefix_and_suffix(tuple(self.args), prefix, suffix) + tvar = self.alias.alias_tvars[prefix] + assert isinstance(tvar, TypeVarTupleType) + mapping = {tvar.id: TupleType(list(middle), tvar.tuple_fallback)} + for tvar, sub in zip( + self.alias.alias_tvars[:prefix] + self.alias.alias_tvars[prefix + 1 :], start + end + ): + mapping[tvar.id] = sub + + new_tp = self.alias.target.accept(InstantiateAliasVisitor(mapping)) new_tp.accept(LocationSetter(self.line, self.column)) new_tp.line = self.line new_tp.column = self.column @@ -1023,6 +1035,12 @@ def deserialize(cls, data: JsonDict) -> UnpackType: typ = data["type"] return UnpackType(deserialize_type(typ)) + def __hash__(self) -> int: + return hash(self.type) + + def __eq__(self, other: object) -> bool: + return isinstance(other, UnpackType) and self.type == other.type + class AnyType(ProperType): """The type 'Any'.""" @@ -3300,6 +3318,45 @@ def has_recursive_types(typ: Type) -> bool: return typ.accept(_has_recursive_type) +def split_with_prefix_and_suffix( + types: tuple[Type, ...], prefix: int, suffix: int +) -> tuple[tuple[Type, ...], tuple[Type, ...], tuple[Type, ...]]: + if len(types) <= prefix + suffix: + types = extend_args_for_prefix_and_suffix(types, prefix, suffix) + if suffix: + return types[:prefix], types[prefix:-suffix], types[-suffix:] + else: + return types[:prefix], types[prefix:], () + + +def extend_args_for_prefix_and_suffix( + types: tuple[Type, ...], prefix: int, suffix: int +) -> tuple[Type, ...]: + """Extend list of types by eating out from variadic tuple to satisfy prefix and suffix.""" + idx = None + item = None + for i, t in enumerate(types): + if isinstance(t, UnpackType): + p_type = get_proper_type(t.type) + if isinstance(p_type, Instance) and p_type.type.fullname == "builtins.tuple": + item = p_type.args[0] + idx = i + break + + if idx is None: + return types + assert item is not None + if idx < prefix: + start = (item,) * (prefix - idx) + else: + start = () + if len(types) - idx - 1 < suffix: + end = (item,) * (suffix - len(types) + idx + 1) + else: + end = () + return types[:idx] + start + (types[idx],) + end + types[idx + 1 :] + + def flatten_nested_unions( types: Sequence[Type], handle_type_alias_type: bool = True ) -> list[Type]: @@ -3326,6 +3383,27 @@ def flatten_nested_unions( return flat_items +def flatten_nested_tuples(types: Sequence[Type]) -> list[Type]: + """Recursively flatten TupleTypes nested with Unpack. + + For example this will transform + Tuple[A, Unpack[Tuple[B, Unpack[Tuple[C, D]]]]] + into + Tuple[A, B, C, D] + """ + res = [] + for typ in types: + if not isinstance(typ, UnpackType): + res.append(typ) + continue + p_type = get_proper_type(typ.type) + if not isinstance(p_type, TupleType): + res.append(typ) + continue + res.extend(flatten_nested_tuples(p_type.items)) + return res + + def is_literal_type(typ: ProperType, fallback_fullname: str, value: LiteralValue) -> bool: """Check if this type is a LiteralType with the given fallback type and value.""" if isinstance(typ, Instance) and typ.last_known_value: diff --git a/mypy/typevartuples.py b/mypy/typevartuples.py index e6d9a1128aa5..ac5f4e43c3bf 100644 --- a/mypy/typevartuples.py +++ b/mypy/typevartuples.py @@ -2,10 +2,18 @@ from __future__ import annotations -from typing import Sequence, TypeVar +from typing import Sequence from mypy.nodes import ARG_POS, ARG_STAR -from mypy.types import CallableType, Instance, ProperType, Type, UnpackType, get_proper_type +from mypy.types import ( + CallableType, + Instance, + ProperType, + Type, + UnpackType, + get_proper_type, + split_with_prefix_and_suffix, +) def find_unpack_in_list(items: Sequence[Type]) -> int | None: @@ -22,18 +30,6 @@ def find_unpack_in_list(items: Sequence[Type]) -> int | None: return unpack_index -T = TypeVar("T") - - -def split_with_prefix_and_suffix( - types: tuple[T, ...], prefix: int, suffix: int -) -> tuple[tuple[T, ...], tuple[T, ...], tuple[T, ...]]: - if suffix: - return (types[:prefix], types[prefix:-suffix], types[-suffix:]) - else: - return (types[:prefix], types[prefix:], ()) - - def split_with_instance( typ: Instance, ) -> tuple[tuple[Type, ...], tuple[Type, ...], tuple[Type, ...]]: diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 3a77537c4f8d..e1fae05eac63 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -570,3 +570,237 @@ call(A().func3, 0, 1, 2) call(A().func3) [builtins fixtures/tuple.pyi] + +[case testVariadicAliasBasicTuple] +from typing import Tuple, List, TypeVar +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +Ts = TypeVarTuple("Ts") + +A = List[Tuple[T, Unpack[Ts], T]] +B = A[Unpack[Ts]] +x: B[int, str, str] +reveal_type(x) # N: Revealed type is "builtins.list[Tuple[builtins.int, builtins.str, builtins.str, builtins.int]]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasBasicCallable] +from typing import TypeVar, Callable +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") + +A = Callable[[T, Unpack[Ts]], S] +x: A[int, str, int, str] +reveal_type(x) # N: Revealed type is "def (builtins.int, builtins.str, builtins.int) -> builtins.str" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasBasicInstance] +from typing import TypeVar, Generic +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +Ts = TypeVarTuple("Ts") + +class G(Generic[Unpack[Ts], T]): ... + +A = G[T, Unpack[Ts], T] +x: A[int, str, str] +reveal_type(x) # N: Revealed type is "__main__.G[builtins.int, builtins.str, builtins.str, builtins.int]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasUnpackFixedTupleArgs] +from typing import Tuple, List, TypeVar +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") + +Start = Tuple[int, str] +A = List[Tuple[T, Unpack[Ts], S]] +x: A[Unpack[Start], int] +reveal_type(x) # N: Revealed type is "builtins.list[Tuple[builtins.int, builtins.str, builtins.int]]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasUnpackFixedTupleTarget] +from typing import Tuple, TypeVar +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") + +Prefix = Tuple[int, int] +A = Tuple[Unpack[Prefix], Unpack[Ts]] +x: A[str, str] +reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.int, builtins.str, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasWrongCallable] +from typing import TypeVar, Callable +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") + +A = Callable[[T, Unpack[Ts], S], int] # E: Required positional args may not appear after default, named or var args +x: A[int, str, int, str] +reveal_type(x) # N: Revealed type is "def (builtins.int, builtins.str, builtins.int, builtins.str) -> builtins.int" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasMultipleUnpacks] +from typing import Tuple, Generic, Callable +from typing_extensions import Unpack, TypeVarTuple + +Ts = TypeVarTuple("Ts") +Us = TypeVarTuple("Us") +class G(Generic[Unpack[Ts]]): ... + +A = Tuple[Unpack[Ts], Unpack[Us]] # E: More than one Unpack in a type is not allowed +x: A[int, str] +reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.str]" + +B = Callable[[Unpack[Ts], Unpack[Us]], int] # E: Var args may not appear after named or var args \ + # E: More than one Unpack in a type is not allowed +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 Unpack in a type is not allowed +z: C[int, str] +reveal_type(z) # N: Revealed type is "__main__.G[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasNoArgs] +from typing import Tuple, TypeVar, Generic, Callable, List +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +Ts = TypeVarTuple("Ts") +class G(Generic[Unpack[Ts]]): ... + +A = List[Tuple[T, Unpack[Ts], T]] +x: A +reveal_type(x) # N: Revealed type is "builtins.list[Tuple[Any, Unpack[builtins.tuple[Any, ...]], Any]]" + +B = Callable[[T, Unpack[Ts]], int] +y: B +reveal_type(y) # N: Revealed type is "def (Any, *Unpack[builtins.tuple[Any, ...]]) -> builtins.int" + +C = G[T, Unpack[Ts], T] +z: C +reveal_type(z) # N: Revealed type is "__main__.G[Any, Unpack[builtins.tuple[Any, ...]], Any]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasFewArgs] +from typing import Tuple, List, TypeVar, Generic, Callable +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") +class G(Generic[Unpack[Ts]]): ... + +A = List[Tuple[T, Unpack[Ts], S]] +x: A[int] # E: Bad number of arguments for type alias, expected: at least 2, given: 1 +reveal_type(x) # N: Revealed type is "builtins.list[Tuple[Any, Unpack[builtins.tuple[Any, ...]], Any]]" + +B = Callable[[T, S, Unpack[Ts]], int] +y: B[int] # E: Bad number of arguments for type alias, expected: at least 2, given: 1 +reveal_type(y) # N: Revealed type is "def (Any, Any, *Unpack[builtins.tuple[Any, ...]]) -> builtins.int" + +C = G[T, Unpack[Ts], S] +z: C[int] # E: Bad number of arguments for type alias, expected: at least 2, given: 1 +reveal_type(z) # N: Revealed type is "__main__.G[Any, Unpack[builtins.tuple[Any, ...]], Any]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasRecursiveUnpack] +from typing import Tuple, Optional +from typing_extensions import Unpack, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +A = Tuple[Unpack[Ts], Optional[A[Unpack[Ts]]]] +x: A[int, str] +reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.str, Union[..., None]]" + +*_, last = x +if last is not None: + reveal_type(last) # N: Revealed type is "Tuple[builtins.int, builtins.str, Union[Tuple[builtins.int, builtins.str, Union[..., None]], None]]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasUpperBoundCheck] +from typing import Tuple, TypeVar +from typing_extensions import Unpack, TypeVarTuple + +class A: ... +class B: ... +class C: ... +class D: ... + +T = TypeVar("T", bound=int) +S = TypeVar("S", bound=str) +Ts = TypeVarTuple("Ts") + +Alias = Tuple[T, Unpack[Ts], S] +First = Tuple[A, B] +Second = Tuple[C, D] +x: Alias[Unpack[First], Unpack[Second]] # E: Type argument "A" of "Alias" must be a subtype of "int" \ + # E: Type argument "D" of "Alias" must be a subtype of "str" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasEmptyArg] +from typing import Tuple +from typing_extensions import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +A = Tuple[int, Unpack[Ts], str] +x: A[()] +reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasVariadicTupleArg] +from typing import Tuple, TypeVar +from typing_extensions import Unpack, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +A = Tuple[int, Unpack[Ts]] +B = A[str, Unpack[Ts]] +C = B[Unpack[Tuple[bool, ...]]] +x: C +reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.str, Unpack[builtins.tuple[builtins.bool, ...]]]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasVariadicTupleArgGeneric] +from typing import Tuple, TypeVar +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +Ts = TypeVarTuple("Ts") + +A = Tuple[int, Unpack[Ts]] +B = A[Unpack[Tuple[T, ...]]] +x: B[str] +reveal_type(x) # N: Revealed type is "Tuple[builtins.int, Unpack[builtins.tuple[builtins.str, ...]]]" +[builtins fixtures/tuple.pyi] + +[case testVariadicAliasVariadicTupleArgSplit] +from typing import Tuple, TypeVar +from typing_extensions import Unpack, TypeVarTuple + +T = TypeVar("T") +S = TypeVar("S") +Ts = TypeVarTuple("Ts") + +A = Tuple[T, Unpack[Ts], S, T] + +x: A[int, Unpack[Tuple[bool, ...]], str] +reveal_type(x) # N: Revealed type is "Tuple[builtins.int, Unpack[builtins.tuple[builtins.bool, ...]], builtins.str, builtins.int]" + +y: A[Unpack[Tuple[bool, ...]]] +reveal_type(y) # N: Revealed type is "Tuple[builtins.bool, Unpack[builtins.tuple[builtins.bool, ...]], builtins.bool, builtins.bool]" +[builtins fixtures/tuple.pyi]