Skip to content

Commit 13bd201

Browse files
authored
Support ParamSpec variables in type aliases (#14159)
Fixes #11855 Fixes #7084 Fixes #10445 Should fix #4987 After thinking about this for some time, it looks like the best way to implement this is by switching type aliases from unbound to bound type variables. Then I can essentially simply share (or copy in one small place, to avoid cyclic imports) all the logic that currently exists for `ParamSpec` and `Concatenate` in `expand_type()` etc. This will also address a big piece of tech debt, and will get some benefits (almost) for free, such as checking bounds/values for alias type variables, and much tighter handling of unbound type variables. Note that in this PR I change logic for emitting some errors, I try to avoid showing multiple errors for the same location/reason. But this is not an essential part of this PR (it is just some test cases would otherwise fail with even more error messages), I can reconsider if there are objections.
1 parent 04d44c1 commit 13bd201

24 files changed

+554
-228
lines changed

docs/source/generics.rst

+2-6
Original file line numberDiff line numberDiff line change
@@ -916,9 +916,5 @@ defeating the purpose of using aliases. Example:
916916
917917
OIntVec = Optional[Vec[int]]
918918
919-
.. note::
920-
921-
A type alias does not define a new type. For generic type aliases
922-
this means that variance of type variables used for alias definition does not
923-
apply to aliases. A parameterized generic alias is treated simply as an original
924-
type with the corresponding type variables substituted.
919+
Using type variable bounds or values in generic aliases, has the same effect
920+
as in generic classes/functions.

mypy/checker.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7115,7 +7115,7 @@ def visit_uninhabited_type(self, t: UninhabitedType) -> Type:
71157115
return t
71167116

71177117
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
7118-
# Target of the alias cannot by an ambiguous <nothing>, so we just
7118+
# Target of the alias cannot be an ambiguous <nothing>, so we just
71197119
# replace the arguments.
71207120
return t.copy_modified(args=[a.accept(self) for a in t.args])
71217121

mypy/checkexpr.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -3854,10 +3854,8 @@ def visit_type_application(self, tapp: TypeApplication) -> Type:
38543854
38553855
There are two different options here, depending on whether expr refers
38563856
to a type alias or directly to a generic class. In the first case we need
3857-
to use a dedicated function typeanal.expand_type_aliases. This
3858-
is due to the fact that currently type aliases machinery uses
3859-
unbound type variables, while normal generics use bound ones;
3860-
see TypeAlias docstring for more details.
3857+
to use a dedicated function typeanal.expand_type_alias(). This
3858+
is due to some differences in how type arguments are applied and checked.
38613859
"""
38623860
if isinstance(tapp.expr, RefExpr) and isinstance(tapp.expr.node, TypeAlias):
38633861
# Subscription of a (generic) alias in runtime context, expand the alias.

mypy/erasetype.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ def visit_param_spec(self, t: ParamSpecType) -> Type:
176176
return t
177177

178178
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
179-
# Type alias target can't contain bound type variables, so
180-
# it is safe to just erase the arguments.
179+
# Type alias target can't contain bound type variables (not bound by the type
180+
# alias itself), so it is safe to just erase the arguments.
181181
return t.copy_modified(args=[a.accept(self) for a in t.args])
182182

183183

mypy/expandtype.py

+5-29
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
NoneType,
1616
Overloaded,
1717
Parameters,
18-
ParamSpecFlavor,
1918
ParamSpecType,
2019
PartialType,
2120
ProperType,
@@ -34,6 +33,7 @@
3433
UninhabitedType,
3534
UnionType,
3635
UnpackType,
36+
expand_param_spec,
3737
get_proper_type,
3838
)
3939
from mypy.typevartuples import (
@@ -212,32 +212,8 @@ def visit_param_spec(self, t: ParamSpecType) -> Type:
212212
# TODO: what does prefix mean in this case?
213213
# TODO: why does this case even happen? Instances aren't plural.
214214
return repl
215-
elif isinstance(repl, ParamSpecType):
216-
return repl.copy_modified(
217-
flavor=t.flavor,
218-
prefix=t.prefix.copy_modified(
219-
arg_types=t.prefix.arg_types + repl.prefix.arg_types,
220-
arg_kinds=t.prefix.arg_kinds + repl.prefix.arg_kinds,
221-
arg_names=t.prefix.arg_names + repl.prefix.arg_names,
222-
),
223-
)
224-
elif isinstance(repl, Parameters) or isinstance(repl, CallableType):
225-
# if the paramspec is *P.args or **P.kwargs:
226-
if t.flavor != ParamSpecFlavor.BARE:
227-
assert isinstance(repl, CallableType), "Should not be able to get here."
228-
# Is this always the right thing to do?
229-
param_spec = repl.param_spec()
230-
if param_spec:
231-
return param_spec.with_flavor(t.flavor)
232-
else:
233-
return repl
234-
else:
235-
return Parameters(
236-
t.prefix.arg_types + repl.arg_types,
237-
t.prefix.arg_kinds + repl.arg_kinds,
238-
t.prefix.arg_names + repl.arg_names,
239-
variables=[*t.prefix.variables, *repl.variables],
240-
)
215+
elif isinstance(repl, (ParamSpecType, Parameters, CallableType)):
216+
return expand_param_spec(t, repl)
241217
else:
242218
# TODO: should this branch be removed? better not to fail silently
243219
return repl
@@ -446,8 +422,8 @@ def visit_type_type(self, t: TypeType) -> Type:
446422
return TypeType.make_normalized(item)
447423

448424
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
449-
# Target of the type alias cannot contain type variables,
450-
# so we just expand the arguments.
425+
# Target of the type alias cannot contain type variables (not bound by the type
426+
# alias itself), so we just expand the arguments.
451427
return t.copy_modified(args=self.expand_types(t.args))
452428

453429
def expand_types(self, types: Iterable[Type]) -> list[Type]:

mypy/fixup.py

+2
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ def visit_var(self, v: Var) -> None:
180180

181181
def visit_type_alias(self, a: TypeAlias) -> None:
182182
a.target.accept(self.type_fixer)
183+
for v in a.alias_tvars:
184+
v.accept(self.type_fixer)
183185

184186

185187
class TypeFixer(TypeVisitor[None]):

mypy/mixedtraverser.py

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
class MixedTraverserVisitor(TraverserVisitor, TypeTraverserVisitor):
2626
"""Recursive traversal of both Node and Type objects."""
2727

28+
def __init__(self) -> None:
29+
self.in_type_alias_expr = False
30+
2831
# Symbol nodes
2932

3033
def visit_var(self, var: Var) -> None:
@@ -45,7 +48,9 @@ def visit_class_def(self, o: ClassDef) -> None:
4548

4649
def visit_type_alias_expr(self, o: TypeAliasExpr) -> None:
4750
super().visit_type_alias_expr(o)
51+
self.in_type_alias_expr = True
4852
o.type.accept(self)
53+
self.in_type_alias_expr = False
4954

5055
def visit_type_var_expr(self, o: TypeVarExpr) -> None:
5156
super().visit_type_var_expr(o)

mypy/nodes.py

+18-13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
Callable,
1313
Dict,
1414
Iterator,
15+
List,
1516
Optional,
1617
Sequence,
1718
Tuple,
@@ -2546,7 +2547,7 @@ class TypeAliasExpr(Expression):
25462547

25472548
# The target type.
25482549
type: mypy.types.Type
2549-
# Names of unbound type variables used to define the alias
2550+
# Names of type variables used to define the alias
25502551
tvars: list[str]
25512552
# Whether this alias was defined in bare form. Used to distinguish
25522553
# between
@@ -2559,7 +2560,7 @@ class TypeAliasExpr(Expression):
25592560
def __init__(self, node: TypeAlias) -> None:
25602561
super().__init__()
25612562
self.type = node.target
2562-
self.tvars = node.alias_tvars
2563+
self.tvars = [v.name for v in node.alias_tvars]
25632564
self.no_args = node.no_args
25642565
self.node = node
25652566

@@ -3309,10 +3310,9 @@ class TypeAlias(SymbolNode):
33093310
class-valued attributes. See SemanticAnalyzerPass2.check_and_set_up_type_alias
33103311
for details.
33113312
3312-
Aliases can be generic. Currently, mypy uses unbound type variables for
3313-
generic aliases and identifies them by name. Essentially, type aliases
3314-
work as macros that expand textually. The definition and expansion rules are
3315-
following:
3313+
Aliases can be generic. We use bound type variables for generic aliases, similar
3314+
to classes. Essentially, type aliases work as macros that expand textually.
3315+
The definition and expansion rules are following:
33163316
33173317
1. An alias targeting a generic class without explicit variables act as
33183318
the given class (this doesn't apply to TypedDict, Tuple and Callable, which
@@ -3363,11 +3363,11 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here
33633363
33643364
Meaning of other fields:
33653365
3366-
target: The target type. For generic aliases contains unbound type variables
3367-
as nested types.
3366+
target: The target type. For generic aliases contains bound type variables
3367+
as nested types (currently TypeVar and ParamSpec are supported).
33683368
_fullname: Qualified name of this type alias. This is used in particular
33693369
to track fine grained dependencies from aliases.
3370-
alias_tvars: Names of unbound type variables used to define this alias.
3370+
alias_tvars: Type variables used to define this alias.
33713371
normalized: Used to distinguish between `A = List`, and `A = list`. Both
33723372
are internally stored using `builtins.list` (because `typing.List` is
33733373
itself an alias), while the second cannot be subscripted because of
@@ -3396,7 +3396,7 @@ def __init__(
33963396
line: int,
33973397
column: int,
33983398
*,
3399-
alias_tvars: list[str] | None = None,
3399+
alias_tvars: list[mypy.types.TypeVarLikeType] | None = None,
34003400
no_args: bool = False,
34013401
normalized: bool = False,
34023402
eager: bool = False,
@@ -3446,12 +3446,16 @@ def name(self) -> str:
34463446
def fullname(self) -> str:
34473447
return self._fullname
34483448

3449+
@property
3450+
def has_param_spec_type(self) -> bool:
3451+
return any(isinstance(v, mypy.types.ParamSpecType) for v in self.alias_tvars)
3452+
34493453
def serialize(self) -> JsonDict:
34503454
data: JsonDict = {
34513455
".class": "TypeAlias",
34523456
"fullname": self._fullname,
34533457
"target": self.target.serialize(),
3454-
"alias_tvars": self.alias_tvars,
3458+
"alias_tvars": [v.serialize() for v in self.alias_tvars],
34553459
"no_args": self.no_args,
34563460
"normalized": self.normalized,
34573461
"line": self.line,
@@ -3466,7 +3470,8 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
34663470
def deserialize(cls, data: JsonDict) -> TypeAlias:
34673471
assert data[".class"] == "TypeAlias"
34683472
fullname = data["fullname"]
3469-
alias_tvars = data["alias_tvars"]
3473+
alias_tvars = [mypy.types.deserialize_type(v) for v in data["alias_tvars"]]
3474+
assert all(isinstance(t, mypy.types.TypeVarLikeType) for t in alias_tvars)
34703475
target = mypy.types.deserialize_type(data["target"])
34713476
no_args = data["no_args"]
34723477
normalized = data["normalized"]
@@ -3477,7 +3482,7 @@ def deserialize(cls, data: JsonDict) -> TypeAlias:
34773482
fullname,
34783483
line,
34793484
column,
3480-
alias_tvars=alias_tvars,
3485+
alias_tvars=cast(List[mypy.types.TypeVarLikeType], alias_tvars),
34813486
no_args=no_args,
34823487
normalized=normalized,
34833488
)

0 commit comments

Comments
 (0)