Skip to content

Reject duplicate ParamSpec.{args,kwargs} at call site #18854

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

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2357,7 +2357,8 @@ def check_argument_count(

# Check for too many or few values for formals.
for i, kind in enumerate(callee.arg_kinds):
if kind.is_required() and not formal_to_actual[i] and not is_unexpected_arg_error:
mapped_args = formal_to_actual[i]
if kind.is_required() and not mapped_args and not is_unexpected_arg_error:
# No actual for a mandatory formal
if kind.is_positional():
self.msg.too_few_arguments(callee, context, actual_names)
Expand All @@ -2368,28 +2369,36 @@ def check_argument_count(
self.msg.missing_named_argument(callee, context, argname)
ok = False
elif not kind.is_star() and is_duplicate_mapping(
formal_to_actual[i], actual_types, actual_kinds
mapped_args, actual_types, actual_kinds
):
if self.chk.in_checked_function() or isinstance(
get_proper_type(actual_types[formal_to_actual[i][0]]), TupleType
get_proper_type(actual_types[mapped_args[0]]), TupleType
):
self.msg.duplicate_argument_value(callee, i, context)
ok = False
elif (
kind.is_named()
and formal_to_actual[i]
and actual_kinds[formal_to_actual[i][0]] not in [nodes.ARG_NAMED, nodes.ARG_STAR2]
and mapped_args
and actual_kinds[mapped_args[0]] not in [nodes.ARG_NAMED, nodes.ARG_STAR2]
):
# Positional argument when expecting a keyword argument.
self.msg.too_many_positional_arguments(callee, context)
ok = False
elif (
callee.param_spec() is not None
and not formal_to_actual[i]
and callee.special_sig != "partial"
):
self.msg.too_few_arguments(callee, context, actual_names)
ok = False
elif callee.param_spec() is not None:
if not mapped_args and callee.special_sig != "partial":
self.msg.too_few_arguments(callee, context, actual_names)
ok = False
elif len(mapped_args) > 1:
paramspec_entries = sum(
isinstance(get_proper_type(actual_types[k]), ParamSpecType)
for k in mapped_args
)
if actual_kinds[mapped_args[0]] == nodes.ARG_STAR and paramspec_entries > 1:
self.msg.fail("ParamSpec.args should only be passed once", context)
ok = False
if actual_kinds[mapped_args[0]] == nodes.ARG_STAR2 and paramspec_entries > 1:
self.msg.fail("ParamSpec.kwargs should only be passed once", context)
ok = False
return ok

def check_for_extra_actual_arguments(
Expand Down
43 changes: 43 additions & 0 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -2560,3 +2560,46 @@ def fn(f: MiddlewareFactory[P]) -> Capture[P]: ...

reveal_type(fn(ServerErrorMiddleware)) # N: Revealed type is "__main__.Capture[[handler: Union[builtins.str, None] =, debug: builtins.bool =]]"
[builtins fixtures/paramspec.pyi]

[case testRunParamSpecDuplicateArgsKwargs]
from typing_extensions import ParamSpec, Concatenate
from typing import Callable, Union

_P = ParamSpec("_P")

def run(predicate: Callable[_P, None], *args: _P.args, **kwargs: _P.kwargs) -> None:
predicate(*args, *args, **kwargs) # E: ParamSpec.args should only be passed once
predicate(*args, **kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once
predicate(*args, *args, **kwargs, **kwargs) # E: ParamSpec.args should only be passed once \
# E: ParamSpec.kwargs should only be passed once
copy_args = args
copy_kwargs = kwargs
predicate(*args, *copy_args, **kwargs) # E: ParamSpec.args should only be passed once
predicate(*copy_args, *args, **kwargs) # E: ParamSpec.args should only be passed once
predicate(*args, **copy_kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once
predicate(*args, **kwargs, **copy_kwargs) # E: ParamSpec.kwargs should only be passed once

def run2(predicate: Callable[Concatenate[int, _P], None], *args: _P.args, **kwargs: _P.kwargs) -> None:
predicate(*args, *args, **kwargs) # E: ParamSpec.args should only be passed once \
# E: Argument 1 has incompatible type "*_P.args"; expected "int"
predicate(*args, **kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once \
# E: Argument 1 has incompatible type "*_P.args"; expected "int"
predicate(1, *args, *args, **kwargs) # E: ParamSpec.args should only be passed once
predicate(1, *args, **kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once
predicate(1, *args, *args, **kwargs, **kwargs) # E: ParamSpec.args should only be passed once \
# E: ParamSpec.kwargs should only be passed once
copy_args = args
copy_kwargs = kwargs
predicate(1, *args, *copy_args, **kwargs) # E: ParamSpec.args should only be passed once
predicate(1, *copy_args, *args, **kwargs) # E: ParamSpec.args should only be passed once
predicate(1, *args, **copy_kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once
predicate(1, *args, **kwargs, **copy_kwargs) # E: ParamSpec.kwargs should only be passed once

def run3(predicate: Callable[Concatenate[int, str, _P], None], *args: _P.args, **kwargs: _P.kwargs) -> None:
base_ok: tuple[int, str]
predicate(*base_ok, *args, **kwargs)
base_bad: tuple[Union[int, str], ...]
predicate(*base_bad, *args, **kwargs) # E: Argument 1 has incompatible type "*Tuple[Union[int, str], ...]"; expected "int" \
# E: Argument 1 has incompatible type "*Tuple[Union[int, str], ...]"; expected "str" \
# E: Argument 1 has incompatible type "*Tuple[Union[int, str], ...]"; expected "_P.args"
[builtins fixtures/paramspec.pyi]