Skip to content

Improve error message for bound typevar in TypeAliasType #17053

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3521,6 +3521,7 @@ def analyze_alias(
rvalue: Expression,
allow_placeholder: bool = False,
declared_type_vars: TypeVarLikeList | None = None,
all_declared_type_params_names: list[str] | None = None,
) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str], bool]:
"""Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).

Expand Down Expand Up @@ -3573,7 +3574,7 @@ def analyze_alias(
in_dynamic_func=dynamic,
global_scope=global_scope,
allowed_alias_tvars=tvar_defs,
has_type_params=declared_type_vars is not None,
alias_type_params_names=all_declared_type_params_names,
)

# There can be only one variadic variable at most, the error is reported elsewhere.
Expand Down Expand Up @@ -3622,14 +3623,16 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
# It can be `A = TypeAliasType('A', ...)` call, in this case,
# we just take the second argument and analyze it:
type_params: TypeVarLikeList | None
all_type_params_names: list[str] | None
if self.check_type_alias_type_call(s.rvalue, name=lvalue.name):
rvalue = s.rvalue.args[1]
pep_695 = True
type_params = self.analyze_type_alias_type_params(s.rvalue)
type_params, all_type_params_names = self.analyze_type_alias_type_params(s.rvalue)
else:
rvalue = s.rvalue
pep_695 = False
type_params = None
all_type_params_names = None

if isinstance(rvalue, CallExpr) and rvalue.analyzed:
return False
Expand Down Expand Up @@ -3686,7 +3689,11 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
else:
tag = self.track_incomplete_refs()
res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias(
lvalue.name, rvalue, allow_placeholder=True, declared_type_vars=type_params
lvalue.name,
rvalue,
allow_placeholder=True,
declared_type_vars=type_params,
all_declared_type_params_names=all_type_params_names,
)
if not res:
return False
Expand Down Expand Up @@ -3803,20 +3810,28 @@ def check_type_alias_type_call(self, rvalue: Expression, *, name: str) -> TypeGu

return self.check_typevarlike_name(rvalue, name, rvalue)

def analyze_type_alias_type_params(self, rvalue: CallExpr) -> TypeVarLikeList:
def analyze_type_alias_type_params(
self, rvalue: CallExpr
) -> tuple[TypeVarLikeList, list[str]]:
"""Analyze type_params of TypeAliasType.

Returns declared unbound type variable expressions and a list of all decalred type
variable names for error reporting.
"""
if "type_params" in rvalue.arg_names:
type_params_arg = rvalue.args[rvalue.arg_names.index("type_params")]
if not isinstance(type_params_arg, TupleExpr):
self.fail(
"Tuple literal expected as the type_params argument to TypeAliasType",
type_params_arg,
)
return []
return [], []
type_params = type_params_arg.items
else:
type_params = []
return [], []

declared_tvars: TypeVarLikeList = []
all_declared_tvar_names: list[str] = [] # includes bound type variables
have_type_var_tuple = False
for tp_expr in type_params:
if isinstance(tp_expr, StarExpr):
Expand All @@ -3843,16 +3858,19 @@ def analyze_type_alias_type_params(self, rvalue: CallExpr) -> TypeVarLikeList:
continue
have_type_var_tuple = True
elif not self.found_incomplete_ref(tag):
self.fail(
"Free type variable expected in type_params argument to TypeAliasType",
base,
code=codes.TYPE_VAR,
)
sym = self.lookup_qualified(base.name, base)
if sym and sym.fullname in ("typing.Unpack", "typing_extensions.Unpack"):
self.note(
"Don't Unpack type variables in type_params", base, code=codes.TYPE_VAR
if sym and isinstance(sym.node, TypeVarLikeExpr):
all_declared_tvar_names.append(sym.node.name) # Error will be reported later
else:
self.fail(
"Free type variable expected in type_params argument to TypeAliasType",
base,
code=codes.TYPE_VAR,
)
if sym and sym.fullname in ("typing.Unpack", "typing_extensions.Unpack"):
self.note(
"Don't Unpack type variables in type_params", base, code=codes.TYPE_VAR
)
continue
if tvar in declared_tvars:
self.fail(
Expand All @@ -3862,8 +3880,9 @@ def analyze_type_alias_type_params(self, rvalue: CallExpr) -> TypeVarLikeList:
)
continue
if tvar:
all_declared_tvar_names.append(tvar[0])
declared_tvars.append(tvar)
return declared_tvars
return declared_tvars, all_declared_tvar_names

def disable_invalid_recursive_aliases(
self, s: AssignmentStmt, current_node: TypeAlias
Expand Down
26 changes: 16 additions & 10 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def analyze_type_alias(
in_dynamic_func: bool = False,
global_scope: bool = True,
allowed_alias_tvars: list[TypeVarLikeType] | None = None,
has_type_params: bool = False,
alias_type_params_names: list[str] | None = None,
) -> tuple[Type, set[str]]:
"""Analyze r.h.s. of a (potential) type alias definition.

Expand All @@ -159,7 +159,7 @@ def analyze_type_alias(
allow_placeholder=allow_placeholder,
prohibit_self_type="type alias target",
allowed_alias_tvars=allowed_alias_tvars,
has_type_params=has_type_params,
alias_type_params_names=alias_type_params_names,
)
analyzer.in_dynamic_func = in_dynamic_func
analyzer.global_scope = global_scope
Expand Down Expand Up @@ -212,7 +212,7 @@ def __init__(
prohibit_self_type: str | None = None,
allowed_alias_tvars: list[TypeVarLikeType] | None = None,
allow_type_any: bool = False,
has_type_params: bool = False,
alias_type_params_names: list[str] | None = None,
) -> None:
self.api = api
self.fail_func = api.fail
Expand All @@ -234,7 +234,7 @@ def __init__(
if allowed_alias_tvars is None:
allowed_alias_tvars = []
self.allowed_alias_tvars = allowed_alias_tvars
self.has_type_params = has_type_params
self.alias_type_params_names = alias_type_params_names
# If false, record incomplete ref if we generate PlaceholderType.
self.allow_placeholder = allow_placeholder
# Are we in a context where Required[] is allowed?
Expand Down Expand Up @@ -275,6 +275,12 @@ def visit_unbound_type(self, t: UnboundType, defining_literal: bool = False) ->
return make_optional_type(typ)
return typ

def not_declared_in_type_params(self, tvar_name: str) -> bool:
return (
self.alias_type_params_names is not None
and tvar_name not in self.alias_type_params_names
)

def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) -> Type:
sym = self.lookup_qualified(t.name, t)
if sym is not None:
Expand Down Expand Up @@ -329,7 +335,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
if tvar_def is None:
if self.allow_unbound_tvars:
return t
if self.defining_alias and self.has_type_params:
if self.defining_alias and self.not_declared_in_type_params(t.name):
msg = f'ParamSpec "{t.name}" is not included in type_params'
else:
msg = f'ParamSpec "{t.name}" is unbound'
Expand Down Expand Up @@ -357,7 +363,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
and not defining_literal
and (tvar_def is None or tvar_def not in self.allowed_alias_tvars)
):
if self.has_type_params:
if self.not_declared_in_type_params(t.name):
msg = f'Type variable "{t.name}" is not included in type_params'
else:
msg = f'Can\'t use bound type variable "{t.name}" to define generic alias'
Expand All @@ -376,7 +382,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
and self.defining_alias
and tvar_def not in self.allowed_alias_tvars
):
if self.has_type_params:
if self.not_declared_in_type_params(t.name):
msg = f'Type variable "{t.name}" is not included in type_params'
else:
msg = f'Can\'t use bound type variable "{t.name}" to define generic alias'
Expand All @@ -386,7 +392,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
if tvar_def is None:
if self.allow_unbound_tvars:
return t
if self.defining_alias and self.has_type_params:
if self.defining_alias and self.not_declared_in_type_params(t.name):
msg = f'TypeVarTuple "{t.name}" is not included in type_params'
else:
msg = f'TypeVarTuple "{t.name}" is unbound'
Expand Down Expand Up @@ -1281,11 +1287,11 @@ def analyze_callable_args_for_paramspec(
return None
elif (
self.defining_alias
and self.has_type_params
and self.not_declared_in_type_params(tvar_def.name)
and tvar_def not in self.allowed_alias_tvars
):
self.fail(
f'ParamSpec "{callable_args.name}" is not included in type_params',
f'ParamSpec "{tvar_def.name}" is not included in type_params',
callable_args,
code=codes.VALID_TYPE,
)
Expand Down
5 changes: 5 additions & 0 deletions test-data/unit/check-type-aliases.test
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,11 @@ reveal_type(unbound_ps_alias3) # N: Revealed type is "def [P] (*Any, **Any) ->
#unbound_tvt_alias2: Ta10[int]
#reveal_type(unbound_tvt_alias2)

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

[case testTypeAliasTypeNoUnpackInTypeParams311]
Expand Down