Skip to content

Commit d0e27fc

Browse files
authored
Reject duplicate ParamSpec.{args,kwargs} at call site (#18854)
Fixes #18035
1 parent 715b982 commit d0e27fc

File tree

2 files changed

+64
-12
lines changed

2 files changed

+64
-12
lines changed

mypy/checkexpr.py

+21-12
Original file line numberDiff line numberDiff line change
@@ -2357,7 +2357,8 @@ def check_argument_count(
23572357

23582358
# Check for too many or few values for formals.
23592359
for i, kind in enumerate(callee.arg_kinds):
2360-
if kind.is_required() and not formal_to_actual[i] and not is_unexpected_arg_error:
2360+
mapped_args = formal_to_actual[i]
2361+
if kind.is_required() and not mapped_args and not is_unexpected_arg_error:
23612362
# No actual for a mandatory formal
23622363
if kind.is_positional():
23632364
self.msg.too_few_arguments(callee, context, actual_names)
@@ -2368,28 +2369,36 @@ def check_argument_count(
23682369
self.msg.missing_named_argument(callee, context, argname)
23692370
ok = False
23702371
elif not kind.is_star() and is_duplicate_mapping(
2371-
formal_to_actual[i], actual_types, actual_kinds
2372+
mapped_args, actual_types, actual_kinds
23722373
):
23732374
if self.chk.in_checked_function() or isinstance(
2374-
get_proper_type(actual_types[formal_to_actual[i][0]]), TupleType
2375+
get_proper_type(actual_types[mapped_args[0]]), TupleType
23752376
):
23762377
self.msg.duplicate_argument_value(callee, i, context)
23772378
ok = False
23782379
elif (
23792380
kind.is_named()
2380-
and formal_to_actual[i]
2381-
and actual_kinds[formal_to_actual[i][0]] not in [nodes.ARG_NAMED, nodes.ARG_STAR2]
2381+
and mapped_args
2382+
and actual_kinds[mapped_args[0]] not in [nodes.ARG_NAMED, nodes.ARG_STAR2]
23822383
):
23832384
# Positional argument when expecting a keyword argument.
23842385
self.msg.too_many_positional_arguments(callee, context)
23852386
ok = False
2386-
elif (
2387-
callee.param_spec() is not None
2388-
and not formal_to_actual[i]
2389-
and callee.special_sig != "partial"
2390-
):
2391-
self.msg.too_few_arguments(callee, context, actual_names)
2392-
ok = False
2387+
elif callee.param_spec() is not None:
2388+
if not mapped_args and callee.special_sig != "partial":
2389+
self.msg.too_few_arguments(callee, context, actual_names)
2390+
ok = False
2391+
elif len(mapped_args) > 1:
2392+
paramspec_entries = sum(
2393+
isinstance(get_proper_type(actual_types[k]), ParamSpecType)
2394+
for k in mapped_args
2395+
)
2396+
if actual_kinds[mapped_args[0]] == nodes.ARG_STAR and paramspec_entries > 1:
2397+
self.msg.fail("ParamSpec.args should only be passed once", context)
2398+
ok = False
2399+
if actual_kinds[mapped_args[0]] == nodes.ARG_STAR2 and paramspec_entries > 1:
2400+
self.msg.fail("ParamSpec.kwargs should only be passed once", context)
2401+
ok = False
23932402
return ok
23942403

23952404
def check_for_extra_actual_arguments(

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

+43
Original file line numberDiff line numberDiff line change
@@ -2560,3 +2560,46 @@ def fn(f: MiddlewareFactory[P]) -> Capture[P]: ...
25602560

25612561
reveal_type(fn(ServerErrorMiddleware)) # N: Revealed type is "__main__.Capture[[handler: Union[builtins.str, None] =, debug: builtins.bool =]]"
25622562
[builtins fixtures/paramspec.pyi]
2563+
2564+
[case testRunParamSpecDuplicateArgsKwargs]
2565+
from typing_extensions import ParamSpec, Concatenate
2566+
from typing import Callable, Union
2567+
2568+
_P = ParamSpec("_P")
2569+
2570+
def run(predicate: Callable[_P, None], *args: _P.args, **kwargs: _P.kwargs) -> None:
2571+
predicate(*args, *args, **kwargs) # E: ParamSpec.args should only be passed once
2572+
predicate(*args, **kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once
2573+
predicate(*args, *args, **kwargs, **kwargs) # E: ParamSpec.args should only be passed once \
2574+
# E: ParamSpec.kwargs should only be passed once
2575+
copy_args = args
2576+
copy_kwargs = kwargs
2577+
predicate(*args, *copy_args, **kwargs) # E: ParamSpec.args should only be passed once
2578+
predicate(*copy_args, *args, **kwargs) # E: ParamSpec.args should only be passed once
2579+
predicate(*args, **copy_kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once
2580+
predicate(*args, **kwargs, **copy_kwargs) # E: ParamSpec.kwargs should only be passed once
2581+
2582+
def run2(predicate: Callable[Concatenate[int, _P], None], *args: _P.args, **kwargs: _P.kwargs) -> None:
2583+
predicate(*args, *args, **kwargs) # E: ParamSpec.args should only be passed once \
2584+
# E: Argument 1 has incompatible type "*_P.args"; expected "int"
2585+
predicate(*args, **kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once \
2586+
# E: Argument 1 has incompatible type "*_P.args"; expected "int"
2587+
predicate(1, *args, *args, **kwargs) # E: ParamSpec.args should only be passed once
2588+
predicate(1, *args, **kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once
2589+
predicate(1, *args, *args, **kwargs, **kwargs) # E: ParamSpec.args should only be passed once \
2590+
# E: ParamSpec.kwargs should only be passed once
2591+
copy_args = args
2592+
copy_kwargs = kwargs
2593+
predicate(1, *args, *copy_args, **kwargs) # E: ParamSpec.args should only be passed once
2594+
predicate(1, *copy_args, *args, **kwargs) # E: ParamSpec.args should only be passed once
2595+
predicate(1, *args, **copy_kwargs, **kwargs) # E: ParamSpec.kwargs should only be passed once
2596+
predicate(1, *args, **kwargs, **copy_kwargs) # E: ParamSpec.kwargs should only be passed once
2597+
2598+
def run3(predicate: Callable[Concatenate[int, str, _P], None], *args: _P.args, **kwargs: _P.kwargs) -> None:
2599+
base_ok: tuple[int, str]
2600+
predicate(*base_ok, *args, **kwargs)
2601+
base_bad: tuple[Union[int, str], ...]
2602+
predicate(*base_bad, *args, **kwargs) # E: Argument 1 has incompatible type "*Tuple[Union[int, str], ...]"; expected "int" \
2603+
# E: Argument 1 has incompatible type "*Tuple[Union[int, str], ...]"; expected "str" \
2604+
# E: Argument 1 has incompatible type "*Tuple[Union[int, str], ...]"; expected "_P.args"
2605+
[builtins fixtures/paramspec.pyi]

0 commit comments

Comments
 (0)