Skip to content

Commit 337bcf9

Browse files
authored
Improve error message for bound typevar in TypeAliasType (#17053)
Follow up to #17038 When a type variable is bound to a class, it cannot be reused in a type alias. Previously in `TypeAliasType`, this error was reported as "not included in type_params". However in the following example, the error is misleading: ```python from typing import Dict, Generic, TypeVar from typing_extensions import TypeAliasType T = TypeVar("T") class A(Generic[T]): Ta11 = TypeAliasType("Ta11", Dict[str, T], type_params=(T,)) x: A.Ta11 = {"a": 1} reveal_type(x) ``` On the master branch: ``` main.py:8: error: Type variable "T" is not included in type_params [valid-type] main.py:8: error: "T" is a type variable and only valid in type context [misc] main.py:8: error: Free type variable expected in type_params argument to TypeAliasType [type-var] main.py:12: note: Revealed type is "builtins.dict[builtins.str, Any]" Found 3 errors in 1 file (checked 1 source file) ``` With this PR: ``` typealiastype.py:8: error: Can't use bound type variable "T" to define generic alias [valid-type] typealiastype.py:8: error: "T" is a type variable and only valid in type context [misc] typealiastype.py:12: note: Revealed type is "builtins.dict[builtins.str, Any]" Found 2 errors in 1 file (checked 1 source file) ``` This is possible by storing the names of all the declared type_params, even those that are invalid, and checking if the offending type variables are in the list.
1 parent 4310586 commit 337bcf9

File tree

3 files changed

+55
-25
lines changed

3 files changed

+55
-25
lines changed

mypy/semanal.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3521,6 +3521,7 @@ def analyze_alias(
35213521
rvalue: Expression,
35223522
allow_placeholder: bool = False,
35233523
declared_type_vars: TypeVarLikeList | None = None,
3524+
all_declared_type_params_names: list[str] | None = None,
35243525
) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str], bool]:
35253526
"""Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).
35263527
@@ -3573,7 +3574,7 @@ def analyze_alias(
35733574
in_dynamic_func=dynamic,
35743575
global_scope=global_scope,
35753576
allowed_alias_tvars=tvar_defs,
3576-
has_type_params=declared_type_vars is not None,
3577+
alias_type_params_names=all_declared_type_params_names,
35773578
)
35783579

35793580
# There can be only one variadic variable at most, the error is reported elsewhere.
@@ -3622,14 +3623,16 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
36223623
# It can be `A = TypeAliasType('A', ...)` call, in this case,
36233624
# we just take the second argument and analyze it:
36243625
type_params: TypeVarLikeList | None
3626+
all_type_params_names: list[str] | None
36253627
if self.check_type_alias_type_call(s.rvalue, name=lvalue.name):
36263628
rvalue = s.rvalue.args[1]
36273629
pep_695 = True
3628-
type_params = self.analyze_type_alias_type_params(s.rvalue)
3630+
type_params, all_type_params_names = self.analyze_type_alias_type_params(s.rvalue)
36293631
else:
36303632
rvalue = s.rvalue
36313633
pep_695 = False
36323634
type_params = None
3635+
all_type_params_names = None
36333636

36343637
if isinstance(rvalue, CallExpr) and rvalue.analyzed:
36353638
return False
@@ -3686,7 +3689,11 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
36863689
else:
36873690
tag = self.track_incomplete_refs()
36883691
res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias(
3689-
lvalue.name, rvalue, allow_placeholder=True, declared_type_vars=type_params
3692+
lvalue.name,
3693+
rvalue,
3694+
allow_placeholder=True,
3695+
declared_type_vars=type_params,
3696+
all_declared_type_params_names=all_type_params_names,
36903697
)
36913698
if not res:
36923699
return False
@@ -3803,20 +3810,28 @@ def check_type_alias_type_call(self, rvalue: Expression, *, name: str) -> TypeGu
38033810

38043811
return self.check_typevarlike_name(rvalue, name, rvalue)
38053812

3806-
def analyze_type_alias_type_params(self, rvalue: CallExpr) -> TypeVarLikeList:
3813+
def analyze_type_alias_type_params(
3814+
self, rvalue: CallExpr
3815+
) -> tuple[TypeVarLikeList, list[str]]:
3816+
"""Analyze type_params of TypeAliasType.
3817+
3818+
Returns declared unbound type variable expressions and a list of all decalred type
3819+
variable names for error reporting.
3820+
"""
38073821
if "type_params" in rvalue.arg_names:
38083822
type_params_arg = rvalue.args[rvalue.arg_names.index("type_params")]
38093823
if not isinstance(type_params_arg, TupleExpr):
38103824
self.fail(
38113825
"Tuple literal expected as the type_params argument to TypeAliasType",
38123826
type_params_arg,
38133827
)
3814-
return []
3828+
return [], []
38153829
type_params = type_params_arg.items
38163830
else:
3817-
type_params = []
3831+
return [], []
38183832

38193833
declared_tvars: TypeVarLikeList = []
3834+
all_declared_tvar_names: list[str] = [] # includes bound type variables
38203835
have_type_var_tuple = False
38213836
for tp_expr in type_params:
38223837
if isinstance(tp_expr, StarExpr):
@@ -3843,16 +3858,19 @@ def analyze_type_alias_type_params(self, rvalue: CallExpr) -> TypeVarLikeList:
38433858
continue
38443859
have_type_var_tuple = True
38453860
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-
)
38513861
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
3862+
if sym and isinstance(sym.node, TypeVarLikeExpr):
3863+
all_declared_tvar_names.append(sym.node.name) # Error will be reported later
3864+
else:
3865+
self.fail(
3866+
"Free type variable expected in type_params argument to TypeAliasType",
3867+
base,
3868+
code=codes.TYPE_VAR,
38553869
)
3870+
if sym and sym.fullname in ("typing.Unpack", "typing_extensions.Unpack"):
3871+
self.note(
3872+
"Don't Unpack type variables in type_params", base, code=codes.TYPE_VAR
3873+
)
38563874
continue
38573875
if tvar in declared_tvars:
38583876
self.fail(
@@ -3862,8 +3880,9 @@ def analyze_type_alias_type_params(self, rvalue: CallExpr) -> TypeVarLikeList:
38623880
)
38633881
continue
38643882
if tvar:
3883+
all_declared_tvar_names.append(tvar[0])
38653884
declared_tvars.append(tvar)
3866-
return declared_tvars
3885+
return declared_tvars, all_declared_tvar_names
38673886

38683887
def disable_invalid_recursive_aliases(
38693888
self, s: AssignmentStmt, current_node: TypeAlias

mypy/typeanal.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +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,
144+
alias_type_params_names: list[str] | None = None,
145145
) -> tuple[Type, set[str]]:
146146
"""Analyze r.h.s. of a (potential) type alias definition.
147147
@@ -159,7 +159,7 @@ def analyze_type_alias(
159159
allow_placeholder=allow_placeholder,
160160
prohibit_self_type="type alias target",
161161
allowed_alias_tvars=allowed_alias_tvars,
162-
has_type_params=has_type_params,
162+
alias_type_params_names=alias_type_params_names,
163163
)
164164
analyzer.in_dynamic_func = in_dynamic_func
165165
analyzer.global_scope = global_scope
@@ -212,7 +212,7 @@ def __init__(
212212
prohibit_self_type: str | None = None,
213213
allowed_alias_tvars: list[TypeVarLikeType] | None = None,
214214
allow_type_any: bool = False,
215-
has_type_params: bool = False,
215+
alias_type_params_names: list[str] | None = None,
216216
) -> None:
217217
self.api = api
218218
self.fail_func = api.fail
@@ -234,7 +234,7 @@ def __init__(
234234
if allowed_alias_tvars is None:
235235
allowed_alias_tvars = []
236236
self.allowed_alias_tvars = allowed_alias_tvars
237-
self.has_type_params = has_type_params
237+
self.alias_type_params_names = alias_type_params_names
238238
# If false, record incomplete ref if we generate PlaceholderType.
239239
self.allow_placeholder = allow_placeholder
240240
# Are we in a context where Required[] is allowed?
@@ -275,6 +275,12 @@ def visit_unbound_type(self, t: UnboundType, defining_literal: bool = False) ->
275275
return make_optional_type(typ)
276276
return typ
277277

278+
def not_declared_in_type_params(self, tvar_name: str) -> bool:
279+
return (
280+
self.alias_type_params_names is not None
281+
and tvar_name not in self.alias_type_params_names
282+
)
283+
278284
def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) -> Type:
279285
sym = self.lookup_qualified(t.name, t)
280286
if sym is not None:
@@ -329,7 +335,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
329335
if tvar_def is None:
330336
if self.allow_unbound_tvars:
331337
return t
332-
if self.defining_alias and self.has_type_params:
338+
if self.defining_alias and self.not_declared_in_type_params(t.name):
333339
msg = f'ParamSpec "{t.name}" is not included in type_params'
334340
else:
335341
msg = f'ParamSpec "{t.name}" is unbound'
@@ -357,7 +363,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
357363
and not defining_literal
358364
and (tvar_def is None or tvar_def not in self.allowed_alias_tvars)
359365
):
360-
if self.has_type_params:
366+
if self.not_declared_in_type_params(t.name):
361367
msg = f'Type variable "{t.name}" is not included in type_params'
362368
else:
363369
msg = f'Can\'t use bound type variable "{t.name}" to define generic alias'
@@ -376,7 +382,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
376382
and self.defining_alias
377383
and tvar_def not in self.allowed_alias_tvars
378384
):
379-
if self.has_type_params:
385+
if self.not_declared_in_type_params(t.name):
380386
msg = f'Type variable "{t.name}" is not included in type_params'
381387
else:
382388
msg = f'Can\'t use bound type variable "{t.name}" to define generic alias'
@@ -386,7 +392,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
386392
if tvar_def is None:
387393
if self.allow_unbound_tvars:
388394
return t
389-
if self.defining_alias and self.has_type_params:
395+
if self.defining_alias and self.not_declared_in_type_params(t.name):
390396
msg = f'TypeVarTuple "{t.name}" is not included in type_params'
391397
else:
392398
msg = f'TypeVarTuple "{t.name}" is unbound'
@@ -1281,11 +1287,11 @@ def analyze_callable_args_for_paramspec(
12811287
return None
12821288
elif (
12831289
self.defining_alias
1284-
and self.has_type_params
1290+
and self.not_declared_in_type_params(tvar_def.name)
12851291
and tvar_def not in self.allowed_alias_tvars
12861292
):
12871293
self.fail(
1288-
f'ParamSpec "{callable_args.name}" is not included in type_params',
1294+
f'ParamSpec "{tvar_def.name}" is not included in type_params',
12891295
callable_args,
12901296
code=codes.VALID_TYPE,
12911297
)

test-data/unit/check-type-aliases.test

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,11 @@ reveal_type(unbound_ps_alias3) # N: Revealed type is "def [P] (*Any, **Any) ->
11951195
#unbound_tvt_alias2: Ta10[int]
11961196
#reveal_type(unbound_tvt_alias2)
11971197

1198+
class A(Generic[T]):
1199+
Ta11 = TypeAliasType("Ta11", Dict[str, T], type_params=(T,)) # E: Can't use bound type variable "T" to define generic alias \
1200+
# E: "T" is a type variable and only valid in type context
1201+
x: A.Ta11 = {"a": 1}
1202+
reveal_type(x) # N: Revealed type is "builtins.dict[builtins.str, Any]"
11981203
[builtins fixtures/dict.pyi]
11991204

12001205
[case testTypeAliasTypeNoUnpackInTypeParams311]

0 commit comments

Comments
 (0)