diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0804917476a9..12480cf9ab93 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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) @@ -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( diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 5530bc0ecbf9..6f01b15e11f6 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -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]