Skip to content

Commit

Permalink
Warn on invalid *args and **kwargs with ParamSpec (#13892)
Browse files Browse the repository at this point in the history
Closes #13890
  • Loading branch information
sobolevn authored and hauntsaninja committed Oct 30, 2022
1 parent 184add9 commit 5fad1ac
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 0 deletions.
61 changes: 61 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
from mypy.nodes import (
ARG_NAMED,
ARG_POS,
ARG_STAR,
ARG_STAR2,
CONTRAVARIANT,
COVARIANT,
GDEF,
Expand Down Expand Up @@ -843,6 +845,7 @@ def analyze_func_def(self, defn: FuncDef) -> None:
defn.type = result
self.add_type_alias_deps(analyzer.aliases_used)
self.check_function_signature(defn)
self.check_paramspec_definition(defn)
if isinstance(defn, FuncDef):
assert isinstance(defn.type, CallableType)
defn.type = set_callable_name(defn.type, defn)
Expand Down Expand Up @@ -1282,6 +1285,64 @@ def check_function_signature(self, fdef: FuncItem) -> None:
elif len(sig.arg_types) > len(fdef.arguments):
self.fail("Type signature has too many arguments", fdef, blocker=True)

def check_paramspec_definition(self, defn: FuncDef) -> None:
func = defn.type
assert isinstance(func, CallableType)

if not any(isinstance(var, ParamSpecType) for var in func.variables):
return # Function does not have param spec variables

args = func.var_arg()
kwargs = func.kw_arg()
if args is None and kwargs is None:
return # Looks like this function does not have starred args

args_defn_type = None
kwargs_defn_type = None
for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds):
if arg_kind == ARG_STAR:
args_defn_type = arg_def.type_annotation
elif arg_kind == ARG_STAR2:
kwargs_defn_type = arg_def.type_annotation

# This may happen on invalid `ParamSpec` args / kwargs definition,
# type analyzer sets types of arguments to `Any`, but keeps
# definition types as `UnboundType` for now.
if not (
(isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"))
or (
isinstance(kwargs_defn_type, UnboundType)
and kwargs_defn_type.name.endswith(".kwargs")
)
):
# Looks like both `*args` and `**kwargs` are not `ParamSpec`
# It might be something else, skipping.
return

args_type = args.typ if args is not None else None
kwargs_type = kwargs.typ if kwargs is not None else None

if (
not isinstance(args_type, ParamSpecType)
or not isinstance(kwargs_type, ParamSpecType)
or args_type.name != kwargs_type.name
):
if isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"):
param_name = args_defn_type.name.split(".")[0]
elif isinstance(kwargs_defn_type, UnboundType) and kwargs_defn_type.name.endswith(
".kwargs"
):
param_name = kwargs_defn_type.name.split(".")[0]
else:
# Fallback for cases that probably should not ever happen:
param_name = "P"

self.fail(
f'ParamSpec must have "*args" typed as "{param_name}.args" and "**kwargs" typed as "{param_name}.kwargs"',
func,
code=codes.VALID_TYPE,
)

def visit_decorator(self, dec: Decorator) -> None:
self.statement = dec
# TODO: better don't modify them at all.
Expand Down
113 changes: 113 additions & 0 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -1166,3 +1166,116 @@ def func3(callback: Callable[P1, str]) -> Callable[P1, str]:
return "foo"
return inner
[builtins fixtures/paramspec.pyi]


[case testInvalidParamSpecDefinitionsWithArgsKwargs]
from typing import Callable, ParamSpec

P = ParamSpec('P')

def c1(f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: ...
def c2(f: Callable[P, int]) -> int: ...
def c3(f: Callable[P, int], *args, **kwargs) -> int: ...

# It is ok to define,
def c4(f: Callable[P, int], *args: int, **kwargs: str) -> int:
# but not ok to call:
f(*args, **kwargs) # E: Argument 1 has incompatible type "*Tuple[int, ...]"; expected "P.args" \
# E: Argument 2 has incompatible type "**Dict[str, str]"; expected "P.kwargs"
return 1

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"
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"
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"
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"

# Error message test:
P1 = ParamSpec('P1')

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"
[builtins fixtures/paramspec.pyi]


[case testInvalidParamSpecAndConcatenateDefinitionsWithArgsKwargs]
from typing import Callable, ParamSpec
from typing_extensions import Concatenate

P = ParamSpec('P')

def c1(f: Callable[Concatenate[int, P], int], *args: P.args, **kwargs: P.kwargs) -> int: ...
def c2(f: Callable[Concatenate[int, P], int]) -> int: ...
def c3(f: Callable[Concatenate[int, P], int], *args, **kwargs) -> int: ...

# It is ok to define,
def c4(f: Callable[Concatenate[int, P], int], *args: int, **kwargs: str) -> int:
# but not ok to call:
f(1, *args, **kwargs) # E: Argument 2 has incompatible type "*Tuple[int, ...]"; expected "P.args" \
# E: Argument 3 has incompatible type "**Dict[str, str]"; expected "P.kwargs"
return 1

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"
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"
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"
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"
[builtins fixtures/paramspec.pyi]


[case testValidParamSpecInsideGenericWithoutArgsAndKwargs]
from typing import Callable, ParamSpec, Generic
from typing_extensions import Concatenate

P = ParamSpec('P')

class Some(Generic[P]): ...

def create(s: Some[P], *args: int): ...
def update(s: Some[P], **kwargs: int): ...
def delete(s: Some[P]): ...

def from_callable1(c: Callable[P, int], *args: int, **kwargs: int) -> Some[P]: ...
def from_callable2(c: Callable[P, int], **kwargs: int) -> Some[P]: ...
def from_callable3(c: Callable[P, int], *args: int) -> Some[P]: ...

def from_extra1(c: Callable[Concatenate[int, P], int], *args: int, **kwargs: int) -> Some[P]: ...
def from_extra2(c: Callable[Concatenate[int, P], int], **kwargs: int) -> Some[P]: ...
def from_extra3(c: Callable[Concatenate[int, P], int], *args: int) -> Some[P]: ...
[builtins fixtures/paramspec.pyi]


[case testUnboundParamSpec]
from typing import Callable, ParamSpec

P1 = ParamSpec('P1')
P2 = ParamSpec('P2')

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"

def f1(*args: P1.args): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
def f2(**kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
def f3(*args: P1.args, **kwargs: int): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
def f4(*args: int, **kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"

# Error message is based on the `args` definition:
def f5(*args: P2.args, **kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P2.args" and "**kwargs" typed as "P2.kwargs"
def f6(*args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"

# Multiple `ParamSpec` variables can be found, they should not affect error message:
P3 = ParamSpec('P3')

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"
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"
[builtins fixtures/paramspec.pyi]


[case testArgsKwargsWithoutParamSpecVar]
from typing import Generic, Callable, ParamSpec

P = ParamSpec('P')

# This must be allowed:
class Some(Generic[P]):
def call(self, *args: P.args, **kwargs: P.kwargs): ...

# TODO: this probably should be reported.
def call(*args: P.args, **kwargs: P.kwargs): ...
[builtins fixtures/paramspec.pyi]

0 comments on commit 5fad1ac

Please sign in to comment.