Skip to content

Commit 025642b

Browse files
authored
Reject invalid ParamSpec locations (#18278)
Fixes #14832, fixes #13966, fixes #14622. Still does not report error in #14777, I'll work separately on that. Move all `ParamSpec` validity checking to `typeanal.py`. Stop treating `P.args` and `P.kwargs` as binding - only bare typevar makes it available in scope. Reject keyword arguments following `P.args`. This also makes one more conformance test pass.
1 parent e05770d commit 025642b

File tree

3 files changed

+161
-147
lines changed

3 files changed

+161
-147
lines changed

mypy/semanal.py

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@
7272
from mypy.nodes import (
7373
ARG_NAMED,
7474
ARG_POS,
75-
ARG_STAR,
7675
ARG_STAR2,
7776
CONTRAVARIANT,
7877
COVARIANT,
@@ -981,7 +980,6 @@ def analyze_func_def(self, defn: FuncDef) -> None:
981980
defn.type = result
982981
self.add_type_alias_deps(analyzer.aliases_used)
983982
self.check_function_signature(defn)
984-
self.check_paramspec_definition(defn)
985983
if isinstance(defn, FuncDef):
986984
assert isinstance(defn.type, CallableType)
987985
defn.type = set_callable_name(defn.type, defn)
@@ -1610,64 +1608,6 @@ def check_function_signature(self, fdef: FuncItem) -> None:
16101608
elif len(sig.arg_types) > len(fdef.arguments):
16111609
self.fail("Type signature has too many arguments", fdef, blocker=True)
16121610

1613-
def check_paramspec_definition(self, defn: FuncDef) -> None:
1614-
func = defn.type
1615-
assert isinstance(func, CallableType)
1616-
1617-
if not any(isinstance(var, ParamSpecType) for var in func.variables):
1618-
return # Function does not have param spec variables
1619-
1620-
args = func.var_arg()
1621-
kwargs = func.kw_arg()
1622-
if args is None and kwargs is None:
1623-
return # Looks like this function does not have starred args
1624-
1625-
args_defn_type = None
1626-
kwargs_defn_type = None
1627-
for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds):
1628-
if arg_kind == ARG_STAR:
1629-
args_defn_type = arg_def.type_annotation
1630-
elif arg_kind == ARG_STAR2:
1631-
kwargs_defn_type = arg_def.type_annotation
1632-
1633-
# This may happen on invalid `ParamSpec` args / kwargs definition,
1634-
# type analyzer sets types of arguments to `Any`, but keeps
1635-
# definition types as `UnboundType` for now.
1636-
if not (
1637-
(isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"))
1638-
or (
1639-
isinstance(kwargs_defn_type, UnboundType)
1640-
and kwargs_defn_type.name.endswith(".kwargs")
1641-
)
1642-
):
1643-
# Looks like both `*args` and `**kwargs` are not `ParamSpec`
1644-
# It might be something else, skipping.
1645-
return
1646-
1647-
args_type = args.typ if args is not None else None
1648-
kwargs_type = kwargs.typ if kwargs is not None else None
1649-
1650-
if (
1651-
not isinstance(args_type, ParamSpecType)
1652-
or not isinstance(kwargs_type, ParamSpecType)
1653-
or args_type.name != kwargs_type.name
1654-
):
1655-
if isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"):
1656-
param_name = args_defn_type.name.split(".")[0]
1657-
elif isinstance(kwargs_defn_type, UnboundType) and kwargs_defn_type.name.endswith(
1658-
".kwargs"
1659-
):
1660-
param_name = kwargs_defn_type.name.split(".")[0]
1661-
else:
1662-
# Fallback for cases that probably should not ever happen:
1663-
param_name = "P"
1664-
1665-
self.fail(
1666-
f'ParamSpec must have "*args" typed as "{param_name}.args" and "**kwargs" typed as "{param_name}.kwargs"',
1667-
func,
1668-
code=codes.VALID_TYPE,
1669-
)
1670-
16711611
def visit_decorator(self, dec: Decorator) -> None:
16721612
self.statement = dec
16731613
# TODO: better don't modify them at all.

mypy/typeanal.py

Lines changed: 67 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,15 @@ def not_declared_in_type_params(self, tvar_name: str) -> bool:
310310

311311
def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) -> Type:
312312
sym = self.lookup_qualified(t.name, t)
313+
param_spec_name = None
314+
if t.name.endswith((".args", ".kwargs")):
315+
param_spec_name = t.name.rsplit(".", 1)[0]
316+
maybe_param_spec = self.lookup_qualified(param_spec_name, t)
317+
if maybe_param_spec and isinstance(maybe_param_spec.node, ParamSpecExpr):
318+
sym = maybe_param_spec
319+
else:
320+
param_spec_name = None
321+
313322
if sym is not None:
314323
node = sym.node
315324
if isinstance(node, PlaceholderNode):
@@ -362,17 +371,23 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
362371
if tvar_def is None:
363372
if self.allow_unbound_tvars:
364373
return t
374+
name = param_spec_name or t.name
365375
if self.defining_alias and self.not_declared_in_type_params(t.name):
366-
msg = f'ParamSpec "{t.name}" is not included in type_params'
376+
msg = f'ParamSpec "{name}" is not included in type_params'
367377
else:
368-
msg = f'ParamSpec "{t.name}" is unbound'
378+
msg = f'ParamSpec "{name}" is unbound'
369379
self.fail(msg, t, code=codes.VALID_TYPE)
370380
return AnyType(TypeOfAny.from_error)
371381
assert isinstance(tvar_def, ParamSpecType)
372382
if len(t.args) > 0:
373383
self.fail(
374384
f'ParamSpec "{t.name}" used with arguments', t, code=codes.VALID_TYPE
375385
)
386+
if param_spec_name is not None and not self.allow_param_spec_literals:
387+
self.fail(
388+
"ParamSpec components are not allowed here", t, code=codes.VALID_TYPE
389+
)
390+
return AnyType(TypeOfAny.from_error)
376391
# Change the line number
377392
return ParamSpecType(
378393
tvar_def.name,
@@ -1113,46 +1128,57 @@ def visit_callable_type(
11131128
variables, _ = self.bind_function_type_variables(t, t)
11141129
type_guard = self.anal_type_guard(t.ret_type)
11151130
type_is = self.anal_type_is(t.ret_type)
1131+
11161132
arg_kinds = t.arg_kinds
1117-
if len(arg_kinds) >= 2 and arg_kinds[-2] == ARG_STAR and arg_kinds[-1] == ARG_STAR2:
1118-
arg_types = self.anal_array(t.arg_types[:-2], nested=nested) + [
1119-
self.anal_star_arg_type(t.arg_types[-2], ARG_STAR, nested=nested),
1120-
self.anal_star_arg_type(t.arg_types[-1], ARG_STAR2, nested=nested),
1121-
]
1122-
# If nested is True, it means we are analyzing a Callable[...] type, rather
1123-
# than a function definition type. We need to "unpack" ** TypedDict annotation
1124-
# here (for function definitions it is done in semanal).
1125-
if nested and isinstance(arg_types[-1], UnpackType):
1133+
arg_types = []
1134+
param_spec_with_args = param_spec_with_kwargs = None
1135+
param_spec_invalid = False
1136+
for kind, ut in zip(arg_kinds, t.arg_types):
1137+
if kind == ARG_STAR:
1138+
param_spec_with_args, at = self.anal_star_arg_type(ut, kind, nested=nested)
1139+
elif kind == ARG_STAR2:
1140+
param_spec_with_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested)
1141+
else:
1142+
if param_spec_with_args:
1143+
param_spec_invalid = True
1144+
self.fail(
1145+
"Arguments not allowed after ParamSpec.args", t, code=codes.VALID_TYPE
1146+
)
1147+
at = self.anal_type(ut, nested=nested, allow_unpack=False)
1148+
arg_types.append(at)
1149+
1150+
if nested and arg_types:
1151+
# If we've got a Callable[[Unpack[SomeTypedDict]], None], make sure
1152+
# Unpack is interpreted as `**` and not as `*`.
1153+
last = arg_types[-1]
1154+
if isinstance(last, UnpackType):
11261155
# TODO: it would be better to avoid this get_proper_type() call.
1127-
unpacked = get_proper_type(arg_types[-1].type)
1128-
if isinstance(unpacked, TypedDictType):
1129-
arg_types[-1] = unpacked
1156+
p_at = get_proper_type(last.type)
1157+
if isinstance(p_at, TypedDictType) and not last.from_star_syntax:
1158+
# Automatically detect Unpack[Foo] in Callable as backwards
1159+
# compatible syntax for **Foo, if Foo is a TypedDict.
1160+
arg_kinds[-1] = ARG_STAR2
1161+
arg_types[-1] = p_at
11301162
unpacked_kwargs = True
1131-
arg_types = self.check_unpacks_in_list(arg_types)
1132-
else:
1133-
star_index = None
1163+
arg_types = self.check_unpacks_in_list(arg_types)
1164+
1165+
if not param_spec_invalid and param_spec_with_args != param_spec_with_kwargs:
1166+
# If already invalid, do not report more errors - definition has
1167+
# to be fixed anyway
1168+
name = param_spec_with_args or param_spec_with_kwargs
1169+
self.fail(
1170+
f'ParamSpec must have "*args" typed as "{name}.args" and "**kwargs" typed as "{name}.kwargs"',
1171+
t,
1172+
code=codes.VALID_TYPE,
1173+
)
1174+
param_spec_invalid = True
1175+
1176+
if param_spec_invalid:
11341177
if ARG_STAR in arg_kinds:
1135-
star_index = arg_kinds.index(ARG_STAR)
1136-
star2_index = None
1178+
arg_types[arg_kinds.index(ARG_STAR)] = AnyType(TypeOfAny.from_error)
11371179
if ARG_STAR2 in arg_kinds:
1138-
star2_index = arg_kinds.index(ARG_STAR2)
1139-
arg_types = []
1140-
for i, ut in enumerate(t.arg_types):
1141-
at = self.anal_type(
1142-
ut, nested=nested, allow_unpack=i in (star_index, star2_index)
1143-
)
1144-
if nested and isinstance(at, UnpackType) and i == star_index:
1145-
# TODO: it would be better to avoid this get_proper_type() call.
1146-
p_at = get_proper_type(at.type)
1147-
if isinstance(p_at, TypedDictType) and not at.from_star_syntax:
1148-
# Automatically detect Unpack[Foo] in Callable as backwards
1149-
# compatible syntax for **Foo, if Foo is a TypedDict.
1150-
at = p_at
1151-
arg_kinds[i] = ARG_STAR2
1152-
unpacked_kwargs = True
1153-
arg_types.append(at)
1154-
if nested:
1155-
arg_types = self.check_unpacks_in_list(arg_types)
1180+
arg_types[arg_kinds.index(ARG_STAR2)] = AnyType(TypeOfAny.from_error)
1181+
11561182
# If there were multiple (invalid) unpacks, the arg types list will become shorter,
11571183
# we need to trim the kinds/names as well to avoid crashes.
11581184
arg_kinds = t.arg_kinds[: len(arg_types)]
@@ -1207,7 +1233,7 @@ def anal_type_is_arg(self, t: UnboundType, fullname: str) -> Type | None:
12071233
return self.anal_type(t.args[0])
12081234
return None
12091235

1210-
def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type:
1236+
def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> tuple[str | None, Type]:
12111237
"""Analyze signature argument type for *args and **kwargs argument."""
12121238
if isinstance(t, UnboundType) and t.name and "." in t.name and not t.args:
12131239
components = t.name.split(".")
@@ -1234,15 +1260,15 @@ def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type:
12341260
)
12351261
else:
12361262
assert False, kind
1237-
return make_paramspec(
1263+
return tvar_name, make_paramspec(
12381264
tvar_def.name,
12391265
tvar_def.fullname,
12401266
tvar_def.id,
12411267
named_type_func=self.named_type,
12421268
line=t.line,
12431269
column=t.column,
12441270
)
1245-
return self.anal_type(t, nested=nested, allow_unpack=True)
1271+
return None, self.anal_type(t, nested=nested, allow_unpack=True)
12461272

12471273
def visit_overloaded(self, t: Overloaded) -> Type:
12481274
# Overloaded types are manually constructed in semanal.py by analyzing the
@@ -2586,18 +2612,7 @@ def _seems_like_callable(self, type: UnboundType) -> bool:
25862612

25872613
def visit_unbound_type(self, t: UnboundType) -> None:
25882614
name = t.name
2589-
node = None
2590-
2591-
# Special case P.args and P.kwargs for ParamSpecs only.
2592-
if name.endswith("args"):
2593-
if name.endswith((".args", ".kwargs")):
2594-
base = ".".join(name.split(".")[:-1])
2595-
n = self.api.lookup_qualified(base, t)
2596-
if n is not None and isinstance(n.node, ParamSpecExpr):
2597-
node = n
2598-
name = base
2599-
if node is None:
2600-
node = self.api.lookup_qualified(name, t)
2615+
node = self.api.lookup_qualified(name, t)
26012616
if node and node.fullname in SELF_TYPE_NAMES:
26022617
self.has_self_type = True
26032618
if (

0 commit comments

Comments
 (0)