Skip to content

Commit 6f650cf

Browse files
authored
Streamline some elements of variadic types support (#15924)
Fixes #13981 Fixes #15241 Fixes #15495 Fixes #15633 Fixes #15667 Fixes #15897 Fixes #15929 OK, I started following the plan outlined in #15879. In this PR I focused mostly on "kinematics". Here are some notes (in random order): * I decided to normalize `TupleType` and `Instance` items in `semanal_typeargs.py` (not in the type constructors, like for unions). It looks like a simpler way to normalize for now. After this, we can rely on the fact that only non-trivial (more below on what is trivial) variadic items in a type list is either `*Ts` or `*tuple[X, ...]`. A single top-level `TupleType` can appear in an unpack only as type of `*args`. * Callables turned out to be tricky. There is certain tight coupling between `FuncDef.type` and `FuncDef.arguments` that makes it hard to normalize prefix to be expressed as individual arguments _at definition_. I faced exactly the same problem when I implemented `**` unpacking for TypedDicts. So we have two choices: either handle prefixes everywhere, or use normalization helper in relevant code. I propose to go with the latter (it worked well for `**` unpacking). * I decided to switch `Unpack` to be disallowed by default in `typeanal.py`, only very specific potions are allowed now. Although this required plumbing `allow_unpack` all the way from `semanal.py`, conceptually it is simple. This is similar to how `ParamSpec` is handled. * This PR fixes all currently open crash issues (some intentionally, some accidentally) plus a bunch of TODOs I found in the tests (but not all). * I decided to simplify `TypeAliasExpr` (and made it simple reference to the `SymbolNode`, like e.g. `TypedDictExpr` and `NamedTupleExpr`). This is not strictly necessary for this PR, but it makes some parts of it a bit simpler, and I wanted to do it for long time. Here is a more detailed plan of what I am leaving for future PRs (in rough order of priority): * Close non-crash open issues (it looks like there are only three, and all seem to be straightforward) * Handle trivial items in `UnpackType` gracefully. These are `<nothing>` and `Any` (and also potentially `object`). They can appear e.g. after a user error. Currently they can cause assert crashes. (Not sure what is the best way to do this). * Go over current places where `Unpack` is handled, and verify both possible variadic items are handled. * Audit variadic `Instance` constrains and subtyping (the latter is probably OK, but the former may be broken). * Audit `Callable` and `Tuple` subtyping for variadic-related edge cases (constraints seem OK for these). * Figure out story about `map_instance_to_supertype()` (if no changes are needed, add tests for subclassing). * Clear most remaining TODOs. * Go once more over the large scale picture and check whether we have some important parts missing (or unhandled interactions between those). * Verify various "advanced" typing features work well with `TypeVarTuple`s (and add some support if missing but looks important). * Enable this feature by default. I hope to finish these in next few weeks.
1 parent 48835a3 commit 6f650cf

22 files changed

+439
-229
lines changed

mypy/checker.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -4665,10 +4665,7 @@ def analyze_iterable_item_type(self, expr: Expression) -> tuple[Type, Type]:
46654665
isinstance(iterable, TupleType)
46664666
and iterable.partial_fallback.type.fullname == "builtins.tuple"
46674667
):
4668-
joined: Type = UninhabitedType()
4669-
for item in iterable.items:
4670-
joined = join_types(joined, item)
4671-
return iterator, joined
4668+
return iterator, tuple_fallback(iterable).args[0]
46724669
else:
46734670
# Non-tuple iterable.
46744671
return iterator, echk.check_method_call_by_name("__next__", iterator, [], [], expr)[0]

mypy/checkexpr.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
UninhabitedType,
169169
UnionType,
170170
UnpackType,
171-
flatten_nested_tuples,
171+
find_unpack_in_list,
172172
flatten_nested_unions,
173173
get_proper_type,
174174
get_proper_types,
@@ -185,7 +185,6 @@
185185
)
186186
from mypy.typestate import type_state
187187
from mypy.typevars import fill_typevars
188-
from mypy.typevartuples import find_unpack_in_list
189188
from mypy.util import split_module_names
190189
from mypy.visitor import ExpressionVisitor
191190

@@ -1600,7 +1599,7 @@ def check_callable_call(
16001599
See the docstring of check_call for more information.
16011600
"""
16021601
# Always unpack **kwargs before checking a call.
1603-
callee = callee.with_unpacked_kwargs()
1602+
callee = callee.with_unpacked_kwargs().with_normalized_var_args()
16041603
if callable_name is None and callee.name:
16051604
callable_name = callee.name
16061605
ret_type = get_proper_type(callee.ret_type)
@@ -2409,7 +2408,12 @@ def check_argument_types(
24092408
+ unpacked_type.items[inner_unpack_index + 1 :]
24102409
)
24112410
callee_arg_kinds = [ARG_POS] * len(actuals)
2411+
elif isinstance(unpacked_type, TypeVarTupleType):
2412+
callee_arg_types = [orig_callee_arg_type]
2413+
callee_arg_kinds = [ARG_STAR]
24122414
else:
2415+
# TODO: Any and <nothing> can appear in Unpack (as a result of user error),
2416+
# fail gracefully here and elsewhere (and/or normalize them away).
24132417
assert isinstance(unpacked_type, Instance)
24142418
assert unpacked_type.type.fullname == "builtins.tuple"
24152419
callee_arg_types = [unpacked_type.args[0]] * len(actuals)
@@ -4451,7 +4455,6 @@ class C(Generic[T, Unpack[Ts]]): ...
44514455

44524456
prefix = next(i for (i, v) in enumerate(vars) if isinstance(v, TypeVarTupleType))
44534457
suffix = len(vars) - prefix - 1
4454-
args = flatten_nested_tuples(args)
44554458
if len(args) < len(vars) - 1:
44564459
self.msg.incompatible_type_application(len(vars), len(args), ctx)
44574460
return [AnyType(TypeOfAny.from_error)] * len(vars)

mypy/constraints.py

+37-9
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
UninhabitedType,
5050
UnionType,
5151
UnpackType,
52+
find_unpack_in_list,
5253
get_proper_type,
5354
has_recursive_types,
5455
has_type_vars,
@@ -57,7 +58,7 @@
5758
)
5859
from mypy.types_utils import is_union_with_any
5960
from mypy.typestate import type_state
60-
from mypy.typevartuples import extract_unpack, find_unpack_in_list, split_with_mapped_and_template
61+
from mypy.typevartuples import extract_unpack, split_with_mapped_and_template
6162

6263
if TYPE_CHECKING:
6364
from mypy.infer import ArgumentInferContext
@@ -155,16 +156,33 @@ def infer_constraints_for_callable(
155156
# not to hold we can always handle the prefixes too.
156157
inner_unpack = unpacked_type.items[0]
157158
assert isinstance(inner_unpack, UnpackType)
158-
inner_unpacked_type = inner_unpack.type
159-
assert isinstance(inner_unpacked_type, TypeVarTupleType)
159+
inner_unpacked_type = get_proper_type(inner_unpack.type)
160160
suffix_len = len(unpacked_type.items) - 1
161-
constraints.append(
162-
Constraint(
163-
inner_unpacked_type,
164-
SUPERTYPE_OF,
165-
TupleType(actual_types[:-suffix_len], inner_unpacked_type.tuple_fallback),
161+
if isinstance(inner_unpacked_type, TypeVarTupleType):
162+
# Variadic item can be either *Ts...
163+
constraints.append(
164+
Constraint(
165+
inner_unpacked_type,
166+
SUPERTYPE_OF,
167+
TupleType(
168+
actual_types[:-suffix_len], inner_unpacked_type.tuple_fallback
169+
),
170+
)
166171
)
167-
)
172+
else:
173+
# ...or it can be a homogeneous tuple.
174+
assert (
175+
isinstance(inner_unpacked_type, Instance)
176+
and inner_unpacked_type.type.fullname == "builtins.tuple"
177+
)
178+
for at in actual_types[:-suffix_len]:
179+
constraints.extend(
180+
infer_constraints(inner_unpacked_type.args[0], at, SUPERTYPE_OF)
181+
)
182+
# Now handle the suffix (if any).
183+
if suffix_len:
184+
for tt, at in zip(unpacked_type.items[1:], actual_types[-suffix_len:]):
185+
constraints.extend(infer_constraints(tt, at, SUPERTYPE_OF))
168186
else:
169187
assert False, "mypy bug: unhandled constraint inference case"
170188
else:
@@ -863,6 +881,16 @@ def visit_instance(self, template: Instance) -> list[Constraint]:
863881
and self.direction == SUPERTYPE_OF
864882
):
865883
for item in actual.items:
884+
if isinstance(item, UnpackType):
885+
unpacked = get_proper_type(item.type)
886+
if isinstance(unpacked, TypeVarType):
887+
# Cannot infer anything for T from [T, ...] <: *Ts
888+
continue
889+
assert (
890+
isinstance(unpacked, Instance)
891+
and unpacked.type.fullname == "builtins.tuple"
892+
)
893+
item = unpacked.args[0]
866894
cb = infer_constraints(template.args[0], item, SUPERTYPE_OF)
867895
res.extend(cb)
868896
return res

mypy/expandtype.py

+18-93
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import Final, Iterable, Mapping, Sequence, TypeVar, cast, overload
44

5-
from mypy.nodes import ARG_POS, ARG_STAR, ArgKind, Var
5+
from mypy.nodes import ARG_STAR, Var
66
from mypy.state import state
77
from mypy.types import (
88
ANY_STRATEGY,
@@ -35,12 +35,11 @@
3535
UninhabitedType,
3636
UnionType,
3737
UnpackType,
38-
flatten_nested_tuples,
3938
flatten_nested_unions,
4039
get_proper_type,
4140
split_with_prefix_and_suffix,
4241
)
43-
from mypy.typevartuples import find_unpack_in_list, split_with_instance
42+
from mypy.typevartuples import split_with_instance
4443

4544
# Solving the import cycle:
4645
import mypy.type_visitor # ruff: isort: skip
@@ -294,101 +293,30 @@ def expand_unpack(self, t: UnpackType) -> list[Type] | AnyType | UninhabitedType
294293
def visit_parameters(self, t: Parameters) -> Type:
295294
return t.copy_modified(arg_types=self.expand_types(t.arg_types))
296295

297-
# TODO: can we simplify this method? It is too long.
298-
def interpolate_args_for_unpack(
299-
self, t: CallableType, var_arg: UnpackType
300-
) -> tuple[list[str | None], list[ArgKind], list[Type]]:
296+
def interpolate_args_for_unpack(self, t: CallableType, var_arg: UnpackType) -> list[Type]:
301297
star_index = t.arg_kinds.index(ARG_STAR)
298+
prefix = self.expand_types(t.arg_types[:star_index])
299+
suffix = self.expand_types(t.arg_types[star_index + 1 :])
302300

303301
var_arg_type = get_proper_type(var_arg.type)
304302
# We have something like Unpack[Tuple[Unpack[Ts], X1, X2]]
305303
if isinstance(var_arg_type, TupleType):
306304
expanded_tuple = var_arg_type.accept(self)
307305
assert isinstance(expanded_tuple, ProperType) and isinstance(expanded_tuple, TupleType)
308306
expanded_items = expanded_tuple.items
307+
fallback = var_arg_type.partial_fallback
309308
else:
310309
# We have plain Unpack[Ts]
310+
assert isinstance(var_arg_type, TypeVarTupleType)
311+
fallback = var_arg_type.tuple_fallback
311312
expanded_items_res = self.expand_unpack(var_arg)
312313
if isinstance(expanded_items_res, list):
313314
expanded_items = expanded_items_res
314315
else:
315316
# We got Any or <nothing>
316-
arg_types = (
317-
t.arg_types[:star_index] + [expanded_items_res] + t.arg_types[star_index + 1 :]
318-
)
319-
return t.arg_names, t.arg_kinds, arg_types
320-
321-
expanded_unpack_index = find_unpack_in_list(expanded_items)
322-
# This is the case where we just have Unpack[Tuple[X1, X2, X3]]
323-
# (for example if either the tuple had no unpacks, or the unpack in the
324-
# tuple got fully expanded to something with fixed length)
325-
if expanded_unpack_index is None:
326-
arg_names = (
327-
t.arg_names[:star_index]
328-
+ [None] * len(expanded_items)
329-
+ t.arg_names[star_index + 1 :]
330-
)
331-
arg_kinds = (
332-
t.arg_kinds[:star_index]
333-
+ [ARG_POS] * len(expanded_items)
334-
+ t.arg_kinds[star_index + 1 :]
335-
)
336-
arg_types = (
337-
self.expand_types(t.arg_types[:star_index])
338-
+ expanded_items
339-
+ self.expand_types(t.arg_types[star_index + 1 :])
340-
)
341-
else:
342-
# If Unpack[Ts] simplest form still has an unpack or is a
343-
# homogenous tuple, then only the prefix can be represented as
344-
# positional arguments, and we pass Tuple[Unpack[Ts-1], Y1, Y2]
345-
# as the star arg, for example.
346-
expanded_unpack = expanded_items[expanded_unpack_index]
347-
assert isinstance(expanded_unpack, UnpackType)
348-
349-
# Extract the TypeVarTuple, so we can get a tuple fallback from it.
350-
expanded_unpacked_tvt = expanded_unpack.type
351-
if isinstance(expanded_unpacked_tvt, TypeVarTupleType):
352-
fallback = expanded_unpacked_tvt.tuple_fallback
353-
else:
354-
# This can happen when tuple[Any, ...] is used to "patch" a variadic
355-
# generic type without type arguments provided, or when substitution is
356-
# homogeneous tuple.
357-
assert isinstance(expanded_unpacked_tvt, ProperType)
358-
assert isinstance(expanded_unpacked_tvt, Instance)
359-
assert expanded_unpacked_tvt.type.fullname == "builtins.tuple"
360-
fallback = expanded_unpacked_tvt
361-
362-
prefix_len = expanded_unpack_index
363-
arg_names = t.arg_names[:star_index] + [None] * prefix_len + t.arg_names[star_index:]
364-
arg_kinds = (
365-
t.arg_kinds[:star_index] + [ARG_POS] * prefix_len + t.arg_kinds[star_index:]
366-
)
367-
if (
368-
len(expanded_items) == 1
369-
and isinstance(expanded_unpack.type, ProperType)
370-
and isinstance(expanded_unpack.type, Instance)
371-
):
372-
assert expanded_unpack.type.type.fullname == "builtins.tuple"
373-
# Normalize *args: *tuple[X, ...] -> *args: X
374-
arg_types = (
375-
self.expand_types(t.arg_types[:star_index])
376-
+ [expanded_unpack.type.args[0]]
377-
+ self.expand_types(t.arg_types[star_index + 1 :])
378-
)
379-
else:
380-
arg_types = (
381-
self.expand_types(t.arg_types[:star_index])
382-
+ expanded_items[:prefix_len]
383-
# Constructing the Unpack containing the tuple without the prefix.
384-
+ [
385-
UnpackType(TupleType(expanded_items[prefix_len:], fallback))
386-
if len(expanded_items) - prefix_len > 1
387-
else expanded_items[prefix_len]
388-
]
389-
+ self.expand_types(t.arg_types[star_index + 1 :])
390-
)
391-
return arg_names, arg_kinds, arg_types
317+
return prefix + [expanded_items_res] + suffix
318+
new_unpack = UnpackType(TupleType(expanded_items, fallback))
319+
return prefix + [new_unpack] + suffix
392320

393321
def visit_callable_type(self, t: CallableType) -> CallableType:
394322
param_spec = t.param_spec()
@@ -427,20 +355,20 @@ def visit_callable_type(self, t: CallableType) -> CallableType:
427355
)
428356

429357
var_arg = t.var_arg()
358+
needs_normalization = False
430359
if var_arg is not None and isinstance(var_arg.typ, UnpackType):
431-
arg_names, arg_kinds, arg_types = self.interpolate_args_for_unpack(t, var_arg.typ)
360+
needs_normalization = True
361+
arg_types = self.interpolate_args_for_unpack(t, var_arg.typ)
432362
else:
433-
arg_names = t.arg_names
434-
arg_kinds = t.arg_kinds
435363
arg_types = self.expand_types(t.arg_types)
436-
437-
return t.copy_modified(
364+
expanded = t.copy_modified(
438365
arg_types=arg_types,
439-
arg_names=arg_names,
440-
arg_kinds=arg_kinds,
441366
ret_type=t.ret_type.accept(self),
442367
type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None),
443368
)
369+
if needs_normalization:
370+
return expanded.with_normalized_var_args()
371+
return expanded
444372

445373
def visit_overloaded(self, t: Overloaded) -> Type:
446374
items: list[CallableType] = []
@@ -460,9 +388,6 @@ def expand_types_with_unpack(
460388
indicates use of Any or some error occurred earlier. In this case callers should
461389
simply propagate the resulting type.
462390
"""
463-
# TODO: this will cause a crash on aliases like A = Tuple[int, Unpack[A]].
464-
# Although it is unlikely anyone will write this, we should fail gracefully.
465-
typs = flatten_nested_tuples(typs)
466391
items: list[Type] = []
467392
for item in typs:
468393
if isinstance(item, UnpackType) and isinstance(item.type, TypeVarTupleType):

mypy/message_registry.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
171171
IMPLICIT_GENERIC_ANY_BUILTIN: Final = (
172172
'Implicit generic "Any". Use "{}" and specify generic parameters'
173173
)
174-
INVALID_UNPACK = "{} cannot be unpacked (must be tuple or TypeVarTuple)"
174+
INVALID_UNPACK: Final = "{} cannot be unpacked (must be tuple or TypeVarTuple)"
175+
INVALID_UNPACK_POSITION: Final = "Unpack is only valid in a variadic position"
175176

176177
# TypeVar
177178
INCOMPATIBLE_TYPEVAR_VALUE: Final = 'Value of type variable "{}" of {} cannot be {}'

mypy/mixedtraverser.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def visit_class_def(self, o: ClassDef) -> None:
4949
def visit_type_alias_expr(self, o: TypeAliasExpr) -> None:
5050
super().visit_type_alias_expr(o)
5151
self.in_type_alias_expr = True
52-
o.type.accept(self)
52+
o.node.target.accept(self)
5353
self.in_type_alias_expr = False
5454

5555
def visit_type_var_expr(self, o: TypeVarExpr) -> None:

mypy/nodes.py

+2-15
Original file line numberDiff line numberDiff line change
@@ -2625,27 +2625,14 @@ def deserialize(cls, data: JsonDict) -> TypeVarTupleExpr:
26252625
class TypeAliasExpr(Expression):
26262626
"""Type alias expression (rvalue)."""
26272627

2628-
__slots__ = ("type", "tvars", "no_args", "node")
2628+
__slots__ = ("node",)
26292629

2630-
__match_args__ = ("type", "tvars", "no_args", "node")
2630+
__match_args__ = ("node",)
26312631

2632-
# The target type.
2633-
type: mypy.types.Type
2634-
# Names of type variables used to define the alias
2635-
tvars: list[str]
2636-
# Whether this alias was defined in bare form. Used to distinguish
2637-
# between
2638-
# A = List
2639-
# and
2640-
# A = List[Any]
2641-
no_args: bool
26422632
node: TypeAlias
26432633

26442634
def __init__(self, node: TypeAlias) -> None:
26452635
super().__init__()
2646-
self.type = node.target
2647-
self.tvars = [v.name for v in node.alias_tvars]
2648-
self.no_args = node.no_args
26492636
self.node = node
26502637

26512638
def accept(self, visitor: ExpressionVisitor[T]) -> T:

0 commit comments

Comments
 (0)