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 7 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
68 changes: 68 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,71 @@ 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)

param_spec_var = next(
(var for var in func.variables if isinstance(var, ParamSpecType)), None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A function could have multiple ParamSpec type variables. This only picks the first one. Maybe it's enough to look at the *args and **kwargs arguments only, and ignore the variables? I.e., if either *args or **kwargs has a ParamSpec type, then we'd require that both of them are defined and they both refer to a ParamSpec (and the same ParamSpec).

)
if param_spec_var is None:
return

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

has_paramspec_callable = any(
arg.from_concatenate
or (arg.arg_types and isinstance(get_proper_type(arg.arg_types[0]), ParamSpecType))
for arg in get_proper_types(func.arg_types)
if isinstance(arg, CallableType)
)
if not has_paramspec_callable:
return # Callable[ParamSpec, ...] was not found
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this check necessary? I think that we should also reject things like this:

def f(*x: int, **y: P.kwargs) -> C[P]:
    pass


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

args_defn = next(
(
arg_def
for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds)
if arg_kind == ARG_STAR
),
None,
)
kwargs_defn = next(
(
arg_def
for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds)
if arg_kind == ARG_STAR2
),
None,
)

args_defn_type = args_defn.type_annotation if args_defn is not None else None
kwargs_defn_type = kwargs_defn.type_annotation if kwargs_defn is not None else None

# This may happen on invalid `ParamSpec` args / kwargs definition:
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

if not isinstance(args_type, ParamSpecType) or not isinstance(kwargs_type, ParamSpecType):
self.fail(
f'ParamSpec must have "*args" typed as "{param_spec_var.name}.args" and "**kwargs" typed as "{param_spec_var.name}.kwargs"',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if these refer to different ParamSpec variables? E.g. *args: P1.args, **kwargs: P2.kwargs? Could we catch this error as well (or perhaps we are catching it)?

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
74 changes: 74 additions & 0 deletions test-data/unit/check-parameter-specification.test
Expand Up @@ -1166,3 +1166,77 @@ 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]