Skip to content

Commit 8b73cc2

Browse files
Complete type analysis of variadic types (#15991)
This PR closes the first part of support for `TypeVarTuple`: the "static" analysis of types (of course everything is static in mypy, but some parts are more static): `semanal`/`typeanal`, `expand_type()`, `map_instance_to_supertype()`, `erase_type()` (things that precede and/or form foundation for type inference and subtyping). This one was quite tricky, supporting unpacks of forward references required some thinking. What is included in this PR: * Moving argument count validation from `semanal_typeargs` to `typeanal`. In one of previous PRs I mentioned that `get_proper_type()` may be called during semantic analysis causing troubles if we have invalid aliases. So we need to move validation to early stage. For instances, this is not required, but I strongly prefer keeping instances and aliases similar. And ideally at some point we can combine the logic, since it gets more and more similar. At some point we may want to prohibit using `get_proper_type()` during semantic analysis, but I don't want to block `TypeVarTuple` support on this, since this may be a significant refactoring. * Fixing `map_instance_to_supertype()` and `erase_type()`. These two are straightforward, we either use `expand_type()` logic directly (by calling it), or following the same logic. * Few simplifications in `expandtype` and `typeops` following previous normalizations of representation, unless there is a flaw in my logic, removed branches should be all dead code. * Allow (only fixed) unpacks in argument lists for non-variadic types. They were prohibited for no good reason. * (Somewhat limited) support for forward references in unpacks. As I mentioned this one is tricky because of how forward references are represented. Usually they follow either a life cycle like: `Any` -> `<known type>`, or `<Any>` -> `<placeholder>` -> `<known type>` (second one is relatively rare and usually only appears for potentially recursive things like base classes or type alias targets). It looks like `<placeholder>` can never appear as a _valid_ unpack target, I don't have a proof for this, but I was not able to trigger this, so I am not handling it (possible downside is that there may be extra errors about invalid argument count for invalid unpack targets). If I am wrong and this can happen in some valid cases, we can add handling for unpacks of placeholders later. Currently, the handling for `Any` stage of forward references is following: if we detect it, we simply create a dummy valid alias or instance. This logic should work for the same reason having plain `Any` worked in the first place (and why all tests pass if we delete `visit_placeholder_type()`): because (almost) each time we analyze a type, it is either already complete, or we analyze it _from scratch_, i.e. we call `expr_to_unanalyzed_type()`, then `visit_unbound_type()` etc. We almost never store "partially analyzed" types (there are guards against incomplete references and placeholders in annotations), and when we do, it is done in a controlled way that guarantees a type will be re-analyzed again. Since this is such a tricky subject, I didn't add any complex logic to support more tricky use cases (like multiple forward references to fixed unpacks in single list). I propose that we release this, and then see what kind of bug reports we will get. * Additional validation for type arguments position to ensure that `TypeVarTuple`s are never split. Total count is not enough to ban case where we have type variables `[T, *Ts, S, U]` and arguments `[int, int, *Us, int]`. We need to explicitly ensure that actual suffix and prefix are longer or equal to formal ones. Such splitting would be very hard to support, and is explicitly banned by the PEP. * Few minor cleanups. Some random comments: * It is tricky to preserve valid parts of type arguments, if there is an argument count error involving an unpack. So after such error I simply set all arguments to `Any` (or `*tuple[Any, ...]` when needed). * I know there is some code duplication. I tried to factor it away, but it turned out non-trivial. I may do some de-duplication pass after everything is done, and it is easier to see the big picture. * Type applications (i.e. when we have `A[int, int]` in runtime context) are wild west currently. I decided to postpone variadic support for them to a separate PR, because there is already some support (we will just need to handle edge cases and more error conditions) and I wanted minimize size of this PR. * Something I wanted to mention in one of previous PRs but forgot: Long time ago I proposed to normalize away type aliases inside `Unpack`, but I abandoned this idea, it doesn't really give us any benefits. As I said, this is the last PR for the "static part", in the next PR I will work on fixing subtyping and inference for variadic instances. And then will continue with remaining items I mentioned in my master plan in #15924 Fixes #15978 --------- Co-authored-by: Shantanu <[email protected]>
1 parent 816ba3b commit 8b73cc2

File tree

8 files changed

+329
-160
lines changed

8 files changed

+329
-160
lines changed

mypy/erasetype.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,41 @@ def visit_type_var(self, t: TypeVarType) -> Type:
165165
return self.replacement
166166
return t
167167

168+
# TODO: below two methods duplicate some logic with expand_type().
169+
# In fact, we may want to refactor this whole visitor to use expand_type().
170+
def visit_instance(self, t: Instance) -> Type:
171+
result = super().visit_instance(t)
172+
assert isinstance(result, ProperType) and isinstance(result, Instance)
173+
if t.type.fullname == "builtins.tuple":
174+
# Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...]
175+
arg = result.args[0]
176+
if isinstance(arg, UnpackType):
177+
unpacked = get_proper_type(arg.type)
178+
if isinstance(unpacked, Instance):
179+
assert unpacked.type.fullname == "builtins.tuple"
180+
return unpacked
181+
return result
182+
183+
def visit_tuple_type(self, t: TupleType) -> Type:
184+
result = super().visit_tuple_type(t)
185+
assert isinstance(result, ProperType) and isinstance(result, TupleType)
186+
if len(result.items) == 1:
187+
# Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...]
188+
item = result.items[0]
189+
if isinstance(item, UnpackType):
190+
unpacked = get_proper_type(item.type)
191+
if isinstance(unpacked, Instance):
192+
assert unpacked.type.fullname == "builtins.tuple"
193+
if result.partial_fallback.type.fullname != "builtins.tuple":
194+
# If it is a subtype (like named tuple) we need to preserve it,
195+
# this essentially mimics the logic in tuple_fallback().
196+
return result.partial_fallback.accept(self)
197+
return unpacked
198+
return result
199+
168200
def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type:
169201
if self.erase_id(t.id):
170-
return self.replacement
202+
return t.tuple_fallback.copy_modified(args=[self.replacement])
171203
return t
172204

173205
def visit_param_spec(self, t: ParamSpecType) -> Type:

mypy/expandtype.py

+29-37
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,15 @@ def visit_erased_type(self, t: ErasedType) -> Type:
212212

213213
def visit_instance(self, t: Instance) -> Type:
214214
args = self.expand_types_with_unpack(list(t.args))
215-
if isinstance(args, list):
216-
return t.copy_modified(args=args)
217-
else:
218-
return args
215+
if t.type.fullname == "builtins.tuple":
216+
# Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...]
217+
arg = args[0]
218+
if isinstance(arg, UnpackType):
219+
unpacked = get_proper_type(arg.type)
220+
if isinstance(unpacked, Instance):
221+
assert unpacked.type.fullname == "builtins.tuple"
222+
args = list(unpacked.args)
223+
return t.copy_modified(args=args)
219224

220225
def visit_type_var(self, t: TypeVarType) -> Type:
221226
# Normally upper bounds can't contain other type variables, the only exception is
@@ -285,7 +290,7 @@ def expand_unpack(self, t: UnpackType) -> list[Type]:
285290
):
286291
return [UnpackType(typ=repl)]
287292
elif isinstance(repl, (AnyType, UninhabitedType)):
288-
# Replace *Ts = Any with *Ts = *tuple[Any, ...] and some for Never.
293+
# Replace *Ts = Any with *Ts = *tuple[Any, ...] and same for Never.
289294
# These types may appear here as a result of user error or failed inference.
290295
return [UnpackType(t.type.tuple_fallback.copy_modified(args=[repl]))]
291296
else:
@@ -377,15 +382,8 @@ def visit_overloaded(self, t: Overloaded) -> Type:
377382
items.append(new_item)
378383
return Overloaded(items)
379384

380-
def expand_types_with_unpack(
381-
self, typs: Sequence[Type]
382-
) -> list[Type] | AnyType | UninhabitedType:
383-
"""Expands a list of types that has an unpack.
384-
385-
In corner cases, this can return a type rather than a list, in which case this
386-
indicates use of Any or some error occurred earlier. In this case callers should
387-
simply propagate the resulting type.
388-
"""
385+
def expand_types_with_unpack(self, typs: Sequence[Type]) -> list[Type]:
386+
"""Expands a list of types that has an unpack."""
389387
items: list[Type] = []
390388
for item in typs:
391389
if isinstance(item, UnpackType) and isinstance(item.type, TypeVarTupleType):
@@ -396,24 +394,21 @@ def expand_types_with_unpack(
396394

397395
def visit_tuple_type(self, t: TupleType) -> Type:
398396
items = self.expand_types_with_unpack(t.items)
399-
if isinstance(items, list):
400-
if len(items) == 1:
401-
# Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...]
402-
item = items[0]
403-
if isinstance(item, UnpackType):
404-
unpacked = get_proper_type(item.type)
405-
if isinstance(unpacked, Instance):
406-
assert unpacked.type.fullname == "builtins.tuple"
407-
if t.partial_fallback.type.fullname != "builtins.tuple":
408-
# If it is a subtype (like named tuple) we need to preserve it,
409-
# this essentially mimics the logic in tuple_fallback().
410-
return t.partial_fallback.accept(self)
411-
return unpacked
412-
fallback = t.partial_fallback.accept(self)
413-
assert isinstance(fallback, ProperType) and isinstance(fallback, Instance)
414-
return t.copy_modified(items=items, fallback=fallback)
415-
else:
416-
return items
397+
if len(items) == 1:
398+
# Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...]
399+
item = items[0]
400+
if isinstance(item, UnpackType):
401+
unpacked = get_proper_type(item.type)
402+
if isinstance(unpacked, Instance):
403+
assert unpacked.type.fullname == "builtins.tuple"
404+
if t.partial_fallback.type.fullname != "builtins.tuple":
405+
# If it is a subtype (like named tuple) we need to preserve it,
406+
# this essentially mimics the logic in tuple_fallback().
407+
return t.partial_fallback.accept(self)
408+
return unpacked
409+
fallback = t.partial_fallback.accept(self)
410+
assert isinstance(fallback, ProperType) and isinstance(fallback, Instance)
411+
return t.copy_modified(items=items, fallback=fallback)
417412

418413
def visit_typeddict_type(self, t: TypedDictType) -> Type:
419414
fallback = t.fallback.accept(self)
@@ -453,11 +448,8 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type:
453448
# Target of the type alias cannot contain type variables (not bound by the type
454449
# alias itself), so we just expand the arguments.
455450
args = self.expand_types_with_unpack(t.args)
456-
if isinstance(args, list):
457-
# TODO: normalize if target is Tuple, and args are [*tuple[X, ...]]?
458-
return t.copy_modified(args=args)
459-
else:
460-
return args
451+
# TODO: normalize if target is Tuple, and args are [*tuple[X, ...]]?
452+
return t.copy_modified(args=args)
461453

462454
def expand_types(self, types: Iterable[Type]) -> list[Type]:
463455
a: list[Type] = []

mypy/maptype.py

+4-18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3-
from mypy.expandtype import expand_type
3+
from mypy.expandtype import expand_type_by_instance
44
from mypy.nodes import TypeInfo
5-
from mypy.types import AnyType, Instance, TupleType, Type, TypeOfAny, TypeVarId, has_type_vars
5+
from mypy.types import AnyType, Instance, TupleType, TypeOfAny, has_type_vars
66

77

88
def map_instance_to_supertype(instance: Instance, superclass: TypeInfo) -> Instance:
@@ -25,8 +25,7 @@ def map_instance_to_supertype(instance: Instance, superclass: TypeInfo) -> Insta
2525
if not alias._is_recursive:
2626
# Unfortunately we can't support this for generic recursive tuples.
2727
# If we skip this special casing we will fall back to tuple[Any, ...].
28-
env = instance_to_type_environment(instance)
29-
tuple_type = expand_type(instance.type.tuple_type, env)
28+
tuple_type = expand_type_by_instance(instance.type.tuple_type, instance)
3029
if isinstance(tuple_type, TupleType):
3130
# Make the import here to avoid cyclic imports.
3231
import mypy.typeops
@@ -91,8 +90,7 @@ def map_instance_to_direct_supertypes(instance: Instance, supertype: TypeInfo) -
9190

9291
for b in typ.bases:
9392
if b.type == supertype:
94-
env = instance_to_type_environment(instance)
95-
t = expand_type(b, env)
93+
t = expand_type_by_instance(b, instance)
9694
assert isinstance(t, Instance)
9795
result.append(t)
9896

@@ -103,15 +101,3 @@ def map_instance_to_direct_supertypes(instance: Instance, supertype: TypeInfo) -
103101
# type arguments implicitly.
104102
any_type = AnyType(TypeOfAny.unannotated)
105103
return [Instance(supertype, [any_type] * len(supertype.type_vars))]
106-
107-
108-
def instance_to_type_environment(instance: Instance) -> dict[TypeVarId, Type]:
109-
"""Given an Instance, produce the resulting type environment for type
110-
variables bound by the Instance's class definition.
111-
112-
An Instance is a type application of a class (a TypeInfo) to its
113-
required number of type arguments. So this environment consists
114-
of the class's type variables mapped to the Instance's actual
115-
arguments. The type variables are mapped by their `id`.
116-
"""
117-
return {binder.id: arg for binder, arg in zip(instance.type.defn.type_vars, instance.args)}

mypy/semanal_typeargs.py

+9-52
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from mypy.options import Options
1919
from mypy.scope import Scope
2020
from mypy.subtypes import is_same_type, is_subtype
21-
from mypy.typeanal import fix_type_var_tuple_argument, set_any_tvars
2221
from mypy.types import (
2322
AnyType,
2423
CallableType,
@@ -88,36 +87,7 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None:
8887
# types, since errors there have already been reported.
8988
return
9089
self.seen_aliases.add(t)
91-
# Some recursive aliases may produce spurious args. In principle this is not very
92-
# important, as we would simply ignore them when expanding, but it is better to keep
93-
# correct aliases. Also, variadic aliases are better to check when fully analyzed,
94-
# so we do this here.
9590
assert t.alias is not None, f"Unfixed type alias {t.type_ref}"
96-
# TODO: consider moving this validation to typeanal.py, expanding invalid aliases
97-
# during semantic analysis may cause crashes.
98-
if t.alias.tvar_tuple_index is not None:
99-
correct = len(t.args) >= len(t.alias.alias_tvars) - 1
100-
if any(
101-
isinstance(a, UnpackType) and isinstance(get_proper_type(a.type), Instance)
102-
for a in t.args
103-
):
104-
correct = True
105-
else:
106-
correct = len(t.args) == len(t.alias.alias_tvars)
107-
if not correct:
108-
if t.alias.tvar_tuple_index is not None:
109-
exp_len = f"at least {len(t.alias.alias_tvars) - 1}"
110-
else:
111-
exp_len = f"{len(t.alias.alias_tvars)}"
112-
self.fail(
113-
"Bad number of arguments for type alias,"
114-
f" expected: {exp_len}, given: {len(t.args)}",
115-
t,
116-
code=codes.TYPE_ARG,
117-
)
118-
t.args = set_any_tvars(
119-
t.alias, t.line, t.column, self.options, from_error=True, fail=self.fail
120-
).args
12191
is_error = self.validate_args(t.alias.name, t.args, t.alias.alias_tvars, t)
12292
if not is_error:
12393
# If there was already an error for the alias itself, there is no point in checking
@@ -144,34 +114,21 @@ def visit_callable_type(self, t: CallableType) -> None:
144114
t.arg_types[star_index] = p_type.args[0]
145115

146116
def visit_instance(self, t: Instance) -> None:
117+
super().visit_instance(t)
147118
# Type argument counts were checked in the main semantic analyzer pass. We assume
148119
# that the counts are correct here.
149120
info = t.type
150121
if isinstance(info, FakeInfo):
151122
return # https://github.com/python/mypy/issues/11079
152-
t.args = tuple(flatten_nested_tuples(t.args))
153-
if t.type.has_type_var_tuple_type:
154-
# Regular Instances are already validated in typeanal.py.
155-
# TODO: do something with partial overlap (probably just reject).
156-
# also in other places where split_with_prefix_and_suffix() is used.
157-
correct = len(t.args) >= len(t.type.type_vars) - 1
158-
if any(
159-
isinstance(a, UnpackType) and isinstance(get_proper_type(a.type), Instance)
160-
for a in t.args
161-
):
162-
correct = True
163-
if not correct:
164-
exp_len = f"at least {len(t.type.type_vars) - 1}"
165-
self.fail(
166-
f"Bad number of arguments, expected: {exp_len}, given: {len(t.args)}",
167-
t,
168-
code=codes.TYPE_ARG,
169-
)
170-
any_type = AnyType(TypeOfAny.from_error)
171-
t.args = (any_type,) * len(t.type.type_vars)
172-
fix_type_var_tuple_argument(any_type, t)
173123
self.validate_args(info.name, t.args, info.defn.type_vars, t)
174-
super().visit_instance(t)
124+
if t.type.fullname == "builtins.tuple" and len(t.args) == 1:
125+
# Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...]
126+
arg = t.args[0]
127+
if isinstance(arg, UnpackType):
128+
unpacked = get_proper_type(arg.type)
129+
if isinstance(unpacked, Instance):
130+
assert unpacked.type.fullname == "builtins.tuple"
131+
t.args = unpacked.args
175132

176133
def validate_args(
177134
self, name: str, args: Sequence[Type], type_vars: list[TypeVarLikeType], ctx: Context

mypy/test/testtypes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1464,7 +1464,7 @@ def make_call(*items: tuple[str, str | None]) -> CallExpr:
14641464
class TestExpandTypeLimitGetProperType(TestCase):
14651465
# WARNING: do not increase this number unless absolutely necessary,
14661466
# and you understand what you are doing.
1467-
ALLOWED_GET_PROPER_TYPES = 7
1467+
ALLOWED_GET_PROPER_TYPES = 8
14681468

14691469
@skipUnless(mypy.expandtype.__file__.endswith(".py"), "Skip for compiled mypy")
14701470
def test_count_get_proper_type(self) -> None:

0 commit comments

Comments
 (0)