Skip to content

Commit 5fad1ac

Browse files
sobolevnhauntsaninja
authored andcommitted
Warn on invalid *args and **kwargs with ParamSpec (#13892)
Closes #13890
1 parent 184add9 commit 5fad1ac

File tree

2 files changed

+174
-0
lines changed

2 files changed

+174
-0
lines changed

mypy/semanal.py

+61
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
from mypy.nodes import (
7070
ARG_NAMED,
7171
ARG_POS,
72+
ARG_STAR,
73+
ARG_STAR2,
7274
CONTRAVARIANT,
7375
COVARIANT,
7476
GDEF,
@@ -843,6 +845,7 @@ def analyze_func_def(self, defn: FuncDef) -> None:
843845
defn.type = result
844846
self.add_type_alias_deps(analyzer.aliases_used)
845847
self.check_function_signature(defn)
848+
self.check_paramspec_definition(defn)
846849
if isinstance(defn, FuncDef):
847850
assert isinstance(defn.type, CallableType)
848851
defn.type = set_callable_name(defn.type, defn)
@@ -1282,6 +1285,64 @@ def check_function_signature(self, fdef: FuncItem) -> None:
12821285
elif len(sig.arg_types) > len(fdef.arguments):
12831286
self.fail("Type signature has too many arguments", fdef, blocker=True)
12841287

1288+
def check_paramspec_definition(self, defn: FuncDef) -> None:
1289+
func = defn.type
1290+
assert isinstance(func, CallableType)
1291+
1292+
if not any(isinstance(var, ParamSpecType) for var in func.variables):
1293+
return # Function does not have param spec variables
1294+
1295+
args = func.var_arg()
1296+
kwargs = func.kw_arg()
1297+
if args is None and kwargs is None:
1298+
return # Looks like this function does not have starred args
1299+
1300+
args_defn_type = None
1301+
kwargs_defn_type = None
1302+
for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds):
1303+
if arg_kind == ARG_STAR:
1304+
args_defn_type = arg_def.type_annotation
1305+
elif arg_kind == ARG_STAR2:
1306+
kwargs_defn_type = arg_def.type_annotation
1307+
1308+
# This may happen on invalid `ParamSpec` args / kwargs definition,
1309+
# type analyzer sets types of arguments to `Any`, but keeps
1310+
# definition types as `UnboundType` for now.
1311+
if not (
1312+
(isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"))
1313+
or (
1314+
isinstance(kwargs_defn_type, UnboundType)
1315+
and kwargs_defn_type.name.endswith(".kwargs")
1316+
)
1317+
):
1318+
# Looks like both `*args` and `**kwargs` are not `ParamSpec`
1319+
# It might be something else, skipping.
1320+
return
1321+
1322+
args_type = args.typ if args is not None else None
1323+
kwargs_type = kwargs.typ if kwargs is not None else None
1324+
1325+
if (
1326+
not isinstance(args_type, ParamSpecType)
1327+
or not isinstance(kwargs_type, ParamSpecType)
1328+
or args_type.name != kwargs_type.name
1329+
):
1330+
if isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"):
1331+
param_name = args_defn_type.name.split(".")[0]
1332+
elif isinstance(kwargs_defn_type, UnboundType) and kwargs_defn_type.name.endswith(
1333+
".kwargs"
1334+
):
1335+
param_name = kwargs_defn_type.name.split(".")[0]
1336+
else:
1337+
# Fallback for cases that probably should not ever happen:
1338+
param_name = "P"
1339+
1340+
self.fail(
1341+
f'ParamSpec must have "*args" typed as "{param_name}.args" and "**kwargs" typed as "{param_name}.kwargs"',
1342+
func,
1343+
code=codes.VALID_TYPE,
1344+
)
1345+
12851346
def visit_decorator(self, dec: Decorator) -> None:
12861347
self.statement = dec
12871348
# TODO: better don't modify them at all.

test-data/unit/check-parameter-specification.test

+113
Original file line numberDiff line numberDiff line change
@@ -1166,3 +1166,116 @@ def func3(callback: Callable[P1, str]) -> Callable[P1, str]:
11661166
return "foo"
11671167
return inner
11681168
[builtins fixtures/paramspec.pyi]
1169+
1170+
1171+
[case testInvalidParamSpecDefinitionsWithArgsKwargs]
1172+
from typing import Callable, ParamSpec
1173+
1174+
P = ParamSpec('P')
1175+
1176+
def c1(f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: ...
1177+
def c2(f: Callable[P, int]) -> int: ...
1178+
def c3(f: Callable[P, int], *args, **kwargs) -> int: ...
1179+
1180+
# It is ok to define,
1181+
def c4(f: Callable[P, int], *args: int, **kwargs: str) -> int:
1182+
# but not ok to call:
1183+
f(*args, **kwargs) # E: Argument 1 has incompatible type "*Tuple[int, ...]"; expected "P.args" \
1184+
# E: Argument 2 has incompatible type "**Dict[str, str]"; expected "P.kwargs"
1185+
return 1
1186+
1187+
def f1(f: Callable[P, int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
1188+
def f2(f: Callable[P, int], *args: P.args, **kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
1189+
def f3(f: Callable[P, int], *args: P.args) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
1190+
def f4(f: Callable[P, int], **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
1191+
1192+
# Error message test:
1193+
P1 = ParamSpec('P1')
1194+
1195+
def m1(f: Callable[P1, int], *a, **k: P1.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
1196+
[builtins fixtures/paramspec.pyi]
1197+
1198+
1199+
[case testInvalidParamSpecAndConcatenateDefinitionsWithArgsKwargs]
1200+
from typing import Callable, ParamSpec
1201+
from typing_extensions import Concatenate
1202+
1203+
P = ParamSpec('P')
1204+
1205+
def c1(f: Callable[Concatenate[int, P], int], *args: P.args, **kwargs: P.kwargs) -> int: ...
1206+
def c2(f: Callable[Concatenate[int, P], int]) -> int: ...
1207+
def c3(f: Callable[Concatenate[int, P], int], *args, **kwargs) -> int: ...
1208+
1209+
# It is ok to define,
1210+
def c4(f: Callable[Concatenate[int, P], int], *args: int, **kwargs: str) -> int:
1211+
# but not ok to call:
1212+
f(1, *args, **kwargs) # E: Argument 2 has incompatible type "*Tuple[int, ...]"; expected "P.args" \
1213+
# E: Argument 3 has incompatible type "**Dict[str, str]"; expected "P.kwargs"
1214+
return 1
1215+
1216+
def f1(f: Callable[Concatenate[int, P], int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
1217+
def f2(f: Callable[Concatenate[int, P], int], *args: P.args, **kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
1218+
def f3(f: Callable[Concatenate[int, P], int], *args: P.args) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
1219+
def f4(f: Callable[Concatenate[int, P], int], **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
1220+
[builtins fixtures/paramspec.pyi]
1221+
1222+
1223+
[case testValidParamSpecInsideGenericWithoutArgsAndKwargs]
1224+
from typing import Callable, ParamSpec, Generic
1225+
from typing_extensions import Concatenate
1226+
1227+
P = ParamSpec('P')
1228+
1229+
class Some(Generic[P]): ...
1230+
1231+
def create(s: Some[P], *args: int): ...
1232+
def update(s: Some[P], **kwargs: int): ...
1233+
def delete(s: Some[P]): ...
1234+
1235+
def from_callable1(c: Callable[P, int], *args: int, **kwargs: int) -> Some[P]: ...
1236+
def from_callable2(c: Callable[P, int], **kwargs: int) -> Some[P]: ...
1237+
def from_callable3(c: Callable[P, int], *args: int) -> Some[P]: ...
1238+
1239+
def from_extra1(c: Callable[Concatenate[int, P], int], *args: int, **kwargs: int) -> Some[P]: ...
1240+
def from_extra2(c: Callable[Concatenate[int, P], int], **kwargs: int) -> Some[P]: ...
1241+
def from_extra3(c: Callable[Concatenate[int, P], int], *args: int) -> Some[P]: ...
1242+
[builtins fixtures/paramspec.pyi]
1243+
1244+
1245+
[case testUnboundParamSpec]
1246+
from typing import Callable, ParamSpec
1247+
1248+
P1 = ParamSpec('P1')
1249+
P2 = ParamSpec('P2')
1250+
1251+
def f0(f: Callable[P1, int], *args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
1252+
1253+
def f1(*args: P1.args): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
1254+
def f2(**kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
1255+
def f3(*args: P1.args, **kwargs: int): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
1256+
def f4(*args: int, **kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
1257+
1258+
# Error message is based on the `args` definition:
1259+
def f5(*args: P2.args, **kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P2.args" and "**kwargs" typed as "P2.kwargs"
1260+
def f6(*args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
1261+
1262+
# Multiple `ParamSpec` variables can be found, they should not affect error message:
1263+
P3 = ParamSpec('P3')
1264+
1265+
def f7(first: Callable[P3, int], *args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
1266+
def f8(first: Callable[P3, int], *args: P2.args, **kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P2.args" and "**kwargs" typed as "P2.kwargs"
1267+
[builtins fixtures/paramspec.pyi]
1268+
1269+
1270+
[case testArgsKwargsWithoutParamSpecVar]
1271+
from typing import Generic, Callable, ParamSpec
1272+
1273+
P = ParamSpec('P')
1274+
1275+
# This must be allowed:
1276+
class Some(Generic[P]):
1277+
def call(self, *args: P.args, **kwargs: P.kwargs): ...
1278+
1279+
# TODO: this probably should be reported.
1280+
def call(*args: P.args, **kwargs: P.kwargs): ...
1281+
[builtins fixtures/paramspec.pyi]

0 commit comments

Comments
 (0)