Skip to content
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

Warn on invalid *args and **kwargs with ParamSpec #13892

Merged
merged 9 commits into from Oct 30, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 61 additions & 0 deletions mypy/semanal.py
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
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]