Skip to content

Commit ea49e1f

Browse files
hamdanalsobolevn
andauthored
Support TypeAliasType (#16926)
Builds on top of and supersedes #16644 --------- Co-authored-by: sobolevn <mail@sobolevn.me>
1 parent 16abf5c commit ea49e1f

9 files changed

+373
-69
lines changed

Diff for: mypy/semanal.py

+116-16
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353
from contextlib import contextmanager
5454
from typing import Any, Callable, Collection, Final, Iterable, Iterator, List, TypeVar, cast
55-
from typing_extensions import TypeAlias as _TypeAlias
55+
from typing_extensions import TypeAlias as _TypeAlias, TypeGuard
5656

5757
from mypy import errorcodes as codes, message_registry
5858
from mypy.constant_fold import constant_fold_expr
@@ -2018,34 +2018,35 @@ def analyze_class_typevar_declaration(self, base: Type) -> tuple[TypeVarLikeList
20182018

20192019
def analyze_unbound_tvar(self, t: Type) -> tuple[str, TypeVarLikeExpr] | None:
20202020
if isinstance(t, UnpackType) and isinstance(t.type, UnboundType):
2021-
return self.analyze_unbound_tvar_impl(t.type, allow_tvt=True)
2021+
return self.analyze_unbound_tvar_impl(t.type, is_unpacked=True)
20222022
if isinstance(t, UnboundType):
20232023
sym = self.lookup_qualified(t.name, t)
20242024
if sym and sym.fullname in ("typing.Unpack", "typing_extensions.Unpack"):
20252025
inner_t = t.args[0]
20262026
if isinstance(inner_t, UnboundType):
2027-
return self.analyze_unbound_tvar_impl(inner_t, allow_tvt=True)
2027+
return self.analyze_unbound_tvar_impl(inner_t, is_unpacked=True)
20282028
return None
20292029
return self.analyze_unbound_tvar_impl(t)
20302030
return None
20312031

20322032
def analyze_unbound_tvar_impl(
2033-
self, t: UnboundType, allow_tvt: bool = False
2033+
self, t: UnboundType, is_unpacked: bool = False, is_typealias_param: bool = False
20342034
) -> tuple[str, TypeVarLikeExpr] | None:
2035+
assert not is_unpacked or not is_typealias_param, "Mutually exclusive conditions"
20352036
sym = self.lookup_qualified(t.name, t)
20362037
if sym and isinstance(sym.node, PlaceholderNode):
20372038
self.record_incomplete_ref()
2038-
if not allow_tvt and sym and isinstance(sym.node, ParamSpecExpr):
2039+
if not is_unpacked and sym and isinstance(sym.node, ParamSpecExpr):
20392040
if sym.fullname and not self.tvar_scope.allow_binding(sym.fullname):
20402041
# It's bound by our type variable scope
20412042
return None
20422043
return t.name, sym.node
2043-
if allow_tvt and sym and isinstance(sym.node, TypeVarTupleExpr):
2044+
if (is_unpacked or is_typealias_param) and sym and isinstance(sym.node, TypeVarTupleExpr):
20442045
if sym.fullname and not self.tvar_scope.allow_binding(sym.fullname):
20452046
# It's bound by our type variable scope
20462047
return None
20472048
return t.name, sym.node
2048-
if sym is None or not isinstance(sym.node, TypeVarExpr) or allow_tvt:
2049+
if sym is None or not isinstance(sym.node, TypeVarExpr) or is_unpacked:
20492050
return None
20502051
elif sym.fullname and not self.tvar_scope.allow_binding(sym.fullname):
20512052
# It's bound by our type variable scope
@@ -3515,7 +3516,11 @@ def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Typ
35153516
return typ
35163517

35173518
def analyze_alias(
3518-
self, name: str, rvalue: Expression, allow_placeholder: bool = False
3519+
self,
3520+
name: str,
3521+
rvalue: Expression,
3522+
allow_placeholder: bool = False,
3523+
declared_type_vars: TypeVarLikeList | None = None,
35193524
) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str], bool]:
35203525
"""Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).
35213526
@@ -3540,9 +3545,10 @@ def analyze_alias(
35403545
found_type_vars = self.find_type_var_likes(typ)
35413546
tvar_defs: list[TypeVarLikeType] = []
35423547
namespace = self.qualified_name(name)
3548+
alias_type_vars = found_type_vars if declared_type_vars is None else declared_type_vars
35433549
last_tvar_name_with_default: str | None = None
35443550
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
3545-
for name, tvar_expr in found_type_vars:
3551+
for name, tvar_expr in alias_type_vars:
35463552
tvar_expr.default = tvar_expr.default.accept(
35473553
TypeVarDefaultTranslator(self, tvar_expr.name, typ)
35483554
)
@@ -3567,6 +3573,7 @@ def analyze_alias(
35673573
in_dynamic_func=dynamic,
35683574
global_scope=global_scope,
35693575
allowed_alias_tvars=tvar_defs,
3576+
has_type_params=declared_type_vars is not None,
35703577
)
35713578

35723579
# There can be only one variadic variable at most, the error is reported elsewhere.
@@ -3579,7 +3586,7 @@ def analyze_alias(
35793586
variadic = True
35803587
new_tvar_defs.append(td)
35813588

3582-
qualified_tvars = [node.fullname for _name, node in found_type_vars]
3589+
qualified_tvars = [node.fullname for _name, node in alias_type_vars]
35833590
empty_tuple_index = typ.empty_tuple_index if isinstance(typ, UnboundType) else False
35843591
return analyzed, new_tvar_defs, depends_on, qualified_tvars, empty_tuple_index
35853592

@@ -3612,7 +3619,19 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
36123619
# unless using PEP 613 `cls: TypeAlias = A`
36133620
return False
36143621

3615-
if isinstance(s.rvalue, CallExpr) and s.rvalue.analyzed:
3622+
# It can be `A = TypeAliasType('A', ...)` call, in this case,
3623+
# we just take the second argument and analyze it:
3624+
type_params: TypeVarLikeList | None
3625+
if self.check_type_alias_type_call(s.rvalue, name=lvalue.name):
3626+
rvalue = s.rvalue.args[1]
3627+
pep_695 = True
3628+
type_params = self.analyze_type_alias_type_params(s.rvalue)
3629+
else:
3630+
rvalue = s.rvalue
3631+
pep_695 = False
3632+
type_params = None
3633+
3634+
if isinstance(rvalue, CallExpr) and rvalue.analyzed:
36163635
return False
36173636

36183637
existing = self.current_symbol_table().get(lvalue.name)
@@ -3638,7 +3657,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
36383657
return False
36393658

36403659
non_global_scope = self.type or self.is_func_scope()
3641-
if not pep_613 and isinstance(s.rvalue, RefExpr) and non_global_scope:
3660+
if not pep_613 and isinstance(rvalue, RefExpr) and non_global_scope:
36423661
# Fourth rule (special case): Non-subscripted right hand side creates a variable
36433662
# at class and function scopes. For example:
36443663
#
@@ -3650,8 +3669,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
36503669
# without this rule, this typical use case will require a lot of explicit
36513670
# annotations (see the second rule).
36523671
return False
3653-
rvalue = s.rvalue
3654-
if not pep_613 and not self.can_be_type_alias(rvalue):
3672+
if not pep_613 and not pep_695 and not self.can_be_type_alias(rvalue):
36553673
return False
36563674

36573675
if existing and not isinstance(existing.node, (PlaceholderNode, TypeAlias)):
@@ -3668,7 +3686,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
36683686
else:
36693687
tag = self.track_incomplete_refs()
36703688
res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias(
3671-
lvalue.name, rvalue, allow_placeholder=True
3689+
lvalue.name, rvalue, allow_placeholder=True, declared_type_vars=type_params
36723690
)
36733691
if not res:
36743692
return False
@@ -3698,13 +3716,15 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
36983716
# so we need to replace it with non-explicit Anys.
36993717
res = make_any_non_explicit(res)
37003718
# Note: with the new (lazy) type alias representation we only need to set no_args to True
3701-
# if the expected number of arguments is non-zero, so that aliases like A = List work.
3719+
# if the expected number of arguments is non-zero, so that aliases like `A = List` work
3720+
# but not aliases like `A = TypeAliasType("A", List)` as these need explicit type params.
37023721
# However, eagerly expanding aliases like Text = str is a nice performance optimization.
37033722
no_args = (
37043723
isinstance(res, ProperType)
37053724
and isinstance(res, Instance)
37063725
and not res.args
37073726
and not empty_tuple_index
3727+
and not pep_695
37083728
)
37093729
if isinstance(res, ProperType) and isinstance(res, Instance):
37103730
if not validate_instance(res, self.fail, empty_tuple_index):
@@ -3771,6 +3791,80 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
37713791
self.note("Use variable annotation syntax to define protocol members", s)
37723792
return True
37733793

3794+
def check_type_alias_type_call(self, rvalue: Expression, *, name: str) -> TypeGuard[CallExpr]:
3795+
if not isinstance(rvalue, CallExpr):
3796+
return False
3797+
3798+
names = ["typing_extensions.TypeAliasType"]
3799+
if self.options.python_version >= (3, 12):
3800+
names.append("typing.TypeAliasType")
3801+
if not refers_to_fullname(rvalue.callee, tuple(names)):
3802+
return False
3803+
3804+
return self.check_typevarlike_name(rvalue, name, rvalue)
3805+
3806+
def analyze_type_alias_type_params(self, rvalue: CallExpr) -> TypeVarLikeList:
3807+
if "type_params" in rvalue.arg_names:
3808+
type_params_arg = rvalue.args[rvalue.arg_names.index("type_params")]
3809+
if not isinstance(type_params_arg, TupleExpr):
3810+
self.fail(
3811+
"Tuple literal expected as the type_params argument to TypeAliasType",
3812+
type_params_arg,
3813+
)
3814+
return []
3815+
type_params = type_params_arg.items
3816+
else:
3817+
type_params = []
3818+
3819+
declared_tvars: TypeVarLikeList = []
3820+
have_type_var_tuple = False
3821+
for tp_expr in type_params:
3822+
if isinstance(tp_expr, StarExpr):
3823+
tp_expr.valid = False
3824+
self.analyze_type_expr(tp_expr)
3825+
try:
3826+
base = self.expr_to_unanalyzed_type(tp_expr)
3827+
except TypeTranslationError:
3828+
continue
3829+
if not isinstance(base, UnboundType):
3830+
continue
3831+
3832+
tag = self.track_incomplete_refs()
3833+
tvar = self.analyze_unbound_tvar_impl(base, is_typealias_param=True)
3834+
if tvar:
3835+
if isinstance(tvar[1], TypeVarTupleExpr):
3836+
if have_type_var_tuple:
3837+
self.fail(
3838+
"Can only use one TypeVarTuple in type_params argument to TypeAliasType",
3839+
base,
3840+
code=codes.TYPE_VAR,
3841+
)
3842+
have_type_var_tuple = True
3843+
continue
3844+
have_type_var_tuple = True
3845+
elif not self.found_incomplete_ref(tag):
3846+
self.fail(
3847+
"Free type variable expected in type_params argument to TypeAliasType",
3848+
base,
3849+
code=codes.TYPE_VAR,
3850+
)
3851+
sym = self.lookup_qualified(base.name, base)
3852+
if sym and sym.fullname in ("typing.Unpack", "typing_extensions.Unpack"):
3853+
self.note(
3854+
"Don't Unpack type variables in type_params", base, code=codes.TYPE_VAR
3855+
)
3856+
continue
3857+
if tvar in declared_tvars:
3858+
self.fail(
3859+
f'Duplicate type variable "{tvar[0]}" in type_params argument to TypeAliasType',
3860+
base,
3861+
code=codes.TYPE_VAR,
3862+
)
3863+
continue
3864+
if tvar:
3865+
declared_tvars.append(tvar)
3866+
return declared_tvars
3867+
37743868
def disable_invalid_recursive_aliases(
37753869
self, s: AssignmentStmt, current_node: TypeAlias
37763870
) -> None:
@@ -5187,6 +5281,12 @@ def visit_call_expr(self, expr: CallExpr) -> None:
51875281
expr.analyzed = OpExpr("divmod", expr.args[0], expr.args[1])
51885282
expr.analyzed.line = expr.line
51895283
expr.analyzed.accept(self)
5284+
elif refers_to_fullname(
5285+
expr.callee, ("typing.TypeAliasType", "typing_extensions.TypeAliasType")
5286+
):
5287+
with self.allow_unbound_tvars_set():
5288+
for a in expr.args:
5289+
a.accept(self)
51905290
else:
51915291
# Normal call expression.
51925292
for a in expr.args:

Diff for: mypy/typeanal.py

+37-12
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def analyze_type_alias(
141141
in_dynamic_func: bool = False,
142142
global_scope: bool = True,
143143
allowed_alias_tvars: list[TypeVarLikeType] | None = None,
144+
has_type_params: bool = False,
144145
) -> tuple[Type, set[str]]:
145146
"""Analyze r.h.s. of a (potential) type alias definition.
146147
@@ -158,6 +159,7 @@ def analyze_type_alias(
158159
allow_placeholder=allow_placeholder,
159160
prohibit_self_type="type alias target",
160161
allowed_alias_tvars=allowed_alias_tvars,
162+
has_type_params=has_type_params,
161163
)
162164
analyzer.in_dynamic_func = in_dynamic_func
163165
analyzer.global_scope = global_scope
@@ -210,6 +212,7 @@ def __init__(
210212
prohibit_self_type: str | None = None,
211213
allowed_alias_tvars: list[TypeVarLikeType] | None = None,
212214
allow_type_any: bool = False,
215+
has_type_params: bool = False,
213216
) -> None:
214217
self.api = api
215218
self.fail_func = api.fail
@@ -231,6 +234,7 @@ def __init__(
231234
if allowed_alias_tvars is None:
232235
allowed_alias_tvars = []
233236
self.allowed_alias_tvars = allowed_alias_tvars
237+
self.has_type_params = has_type_params
234238
# If false, record incomplete ref if we generate PlaceholderType.
235239
self.allow_placeholder = allow_placeholder
236240
# Are we in a context where Required[] is allowed?
@@ -325,7 +329,11 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
325329
if tvar_def is None:
326330
if self.allow_unbound_tvars:
327331
return t
328-
self.fail(f'ParamSpec "{t.name}" is unbound', t, code=codes.VALID_TYPE)
332+
if self.defining_alias and self.has_type_params:
333+
msg = f'ParamSpec "{t.name}" is not included in type_params'
334+
else:
335+
msg = f'ParamSpec "{t.name}" is unbound'
336+
self.fail(msg, t, code=codes.VALID_TYPE)
329337
return AnyType(TypeOfAny.from_error)
330338
assert isinstance(tvar_def, ParamSpecType)
331339
if len(t.args) > 0:
@@ -349,11 +357,11 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
349357
and not defining_literal
350358
and (tvar_def is None or tvar_def not in self.allowed_alias_tvars)
351359
):
352-
self.fail(
353-
f'Can\'t use bound type variable "{t.name}" to define generic alias',
354-
t,
355-
code=codes.VALID_TYPE,
356-
)
360+
if self.has_type_params:
361+
msg = f'Type variable "{t.name}" is not included in type_params'
362+
else:
363+
msg = f'Can\'t use bound type variable "{t.name}" to define generic alias'
364+
self.fail(msg, t, code=codes.VALID_TYPE)
357365
return AnyType(TypeOfAny.from_error)
358366
if isinstance(sym.node, TypeVarExpr) and tvar_def is not None:
359367
assert isinstance(tvar_def, TypeVarType)
@@ -368,17 +376,21 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
368376
and self.defining_alias
369377
and tvar_def not in self.allowed_alias_tvars
370378
):
371-
self.fail(
372-
f'Can\'t use bound type variable "{t.name}" to define generic alias',
373-
t,
374-
code=codes.VALID_TYPE,
375-
)
379+
if self.has_type_params:
380+
msg = f'Type variable "{t.name}" is not included in type_params'
381+
else:
382+
msg = f'Can\'t use bound type variable "{t.name}" to define generic alias'
383+
self.fail(msg, t, code=codes.VALID_TYPE)
376384
return AnyType(TypeOfAny.from_error)
377385
if isinstance(sym.node, TypeVarTupleExpr):
378386
if tvar_def is None:
379387
if self.allow_unbound_tvars:
380388
return t
381-
self.fail(f'TypeVarTuple "{t.name}" is unbound', t, code=codes.VALID_TYPE)
389+
if self.defining_alias and self.has_type_params:
390+
msg = f'TypeVarTuple "{t.name}" is not included in type_params'
391+
else:
392+
msg = f'TypeVarTuple "{t.name}" is unbound'
393+
self.fail(msg, t, code=codes.VALID_TYPE)
382394
return AnyType(TypeOfAny.from_error)
383395
assert isinstance(tvar_def, TypeVarTupleType)
384396
if not self.allow_type_var_tuple:
@@ -1267,6 +1279,19 @@ def analyze_callable_args_for_paramspec(
12671279
AnyType(TypeOfAny.explicit), ret_type=ret_type, fallback=fallback
12681280
)
12691281
return None
1282+
elif (
1283+
self.defining_alias
1284+
and self.has_type_params
1285+
and tvar_def not in self.allowed_alias_tvars
1286+
):
1287+
self.fail(
1288+
f'ParamSpec "{callable_args.name}" is not included in type_params',
1289+
callable_args,
1290+
code=codes.VALID_TYPE,
1291+
)
1292+
return callable_with_ellipsis(
1293+
AnyType(TypeOfAny.special_form), ret_type=ret_type, fallback=fallback
1294+
)
12701295

12711296
return CallableType(
12721297
[

0 commit comments

Comments
 (0)