From d3e7a9e34987cbf2ba8b446759c5ece77e9960fe Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 30 Mar 2025 01:48:10 +0100 Subject: [PATCH 1/5] Reject duplicate ParamSpec components --- mypy/checkexpr.py | 29 +++++++++++-------- .../unit/check-parameter-specification.test | 23 +++++++++++++++ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0804917476a9..55aba820d5bf 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,32 @@ 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: + if actual_kinds[mapped_args[0]] == nodes.ARG_STAR: + self.msg.fail("ParamSpec.args should only be passed once", context) + ok = False + if actual_kinds[mapped_args[0]] == nodes.ARG_STAR2: + 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..4a025430d2e4 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2560,3 +2560,26 @@ 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 + +_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 + +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 +[builtins fixtures/paramspec.pyi] From 64316750196bcf2c1ce7c29c063b596232faba7a Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 30 Mar 2025 03:14:35 +0200 Subject: [PATCH 2/5] Update other affected tests --- .../unit/check-parameter-specification.test | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 4a025430d2e4..f5eb93e471ca 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2413,8 +2413,10 @@ def run2(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P.kwar func2(1, 2, *p) # E: Too few arguments \ # E: Argument 2 has incompatible type "int"; expected "P.args" \ # E: Argument 3 has incompatible type "*List[str]"; expected "P.args" - func2(1, *args, *p) # E: Argument 3 has incompatible type "*List[str]"; expected "P.args" - func2(1, *p, *args) # E: Argument 2 has incompatible type "*List[str]"; expected "P.args" + func2(1, *args, *p) # E: ParamSpec.args should only be passed once \ + # E: Argument 3 has incompatible type "*List[str]"; expected "P.args" + func2(1, *p, *args) # E: ParamSpec.args should only be passed once \ + # E: Argument 2 has incompatible type "*List[str]"; expected "P.args" return func2(1, *args) def run3(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P.kwargs) -> T: @@ -2458,6 +2460,12 @@ def run_bad4(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P. # E: Argument 1 has incompatible type "int"; expected "P.args" return func2(**kwargs) # E: Too few arguments + + + + + + [builtins fixtures/paramspec.pyi] [case testOtherVarArgs] @@ -2472,11 +2480,14 @@ def run(func: Callable[Concatenate[int, str, P], T], *args: P.args, **kwargs: P. func2 = partial(func, **kwargs) args_prefix: Tuple[int, str] = (1, 'a') func2(*args_prefix) # E: Too few arguments - func2(*args, *args_prefix) # E: Argument 1 has incompatible type "*P.args"; expected "int" \ + func2(*args, *args_prefix) # E: ParamSpec.args should only be passed once \ + # E: Argument 1 has incompatible type "*P.args"; expected "int" \ # E: Argument 1 has incompatible type "*P.args"; expected "str" \ # E: Argument 2 has incompatible type "*Tuple[int, str]"; expected "P.args" return func2(*args_prefix, *args) + + [builtins fixtures/paramspec.pyi] [case testParamSpecScoping] From 11e0d837da84a599c9ac93fd8bdc9c8c46211fe0 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 30 Mar 2025 03:27:56 +0200 Subject: [PATCH 3/5] Only emit this diagnistic if they are actually ParamSpec parts --- mypy/checkexpr.py | 8 +++-- .../unit/check-parameter-specification.test | 30 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 55aba820d5bf..12480cf9ab93 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2389,10 +2389,14 @@ def check_argument_count( self.msg.too_few_arguments(callee, context, actual_names) ok = False elif len(mapped_args) > 1: - if actual_kinds[mapped_args[0]] == nodes.ARG_STAR: + 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: + 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 diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index f5eb93e471ca..33cb3d35a8fd 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2413,10 +2413,8 @@ def run2(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P.kwar func2(1, 2, *p) # E: Too few arguments \ # E: Argument 2 has incompatible type "int"; expected "P.args" \ # E: Argument 3 has incompatible type "*List[str]"; expected "P.args" - func2(1, *args, *p) # E: ParamSpec.args should only be passed once \ - # E: Argument 3 has incompatible type "*List[str]"; expected "P.args" - func2(1, *p, *args) # E: ParamSpec.args should only be passed once \ - # E: Argument 2 has incompatible type "*List[str]"; expected "P.args" + func2(1, *args, *p) # E: Argument 3 has incompatible type "*List[str]"; expected "P.args" + func2(1, *p, *args) # E: Argument 2 has incompatible type "*List[str]"; expected "P.args" return func2(1, *args) def run3(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P.kwargs) -> T: @@ -2466,6 +2464,14 @@ def run_bad4(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P. + + + + + + + + [builtins fixtures/paramspec.pyi] [case testOtherVarArgs] @@ -2480,14 +2486,16 @@ def run(func: Callable[Concatenate[int, str, P], T], *args: P.args, **kwargs: P. func2 = partial(func, **kwargs) args_prefix: Tuple[int, str] = (1, 'a') func2(*args_prefix) # E: Too few arguments - func2(*args, *args_prefix) # E: ParamSpec.args should only be passed once \ - # E: Argument 1 has incompatible type "*P.args"; expected "int" \ + func2(*args, *args_prefix) # E: Argument 1 has incompatible type "*P.args"; expected "int" \ # E: Argument 1 has incompatible type "*P.args"; expected "str" \ # E: Argument 2 has incompatible type "*Tuple[int, str]"; expected "P.args" return func2(*args_prefix, *args) + + + [builtins fixtures/paramspec.pyi] [case testParamSpecScoping] @@ -2574,7 +2582,7 @@ reveal_type(fn(ServerErrorMiddleware)) # N: Revealed type is "__main__.Capture[ [case testRunParamSpecDuplicateArgsKwargs] from typing_extensions import ParamSpec, Concatenate -from typing import Callable +from typing import Callable, Union _P = ParamSpec("_P") @@ -2593,4 +2601,12 @@ def run2(predicate: Callable[Concatenate[int, _P], None], *args: _P.args, **kwar 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 + +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] From e4ca07896910a84a1040ce374611c859557a3bff Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 30 Mar 2025 03:29:48 +0200 Subject: [PATCH 4/5] Trim whitespace from --update-data --- .../unit/check-parameter-specification.test | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 33cb3d35a8fd..c2d9cc012ae1 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2458,20 +2458,6 @@ def run_bad4(func: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P. # E: Argument 1 has incompatible type "int"; expected "P.args" return func2(**kwargs) # E: Too few arguments - - - - - - - - - - - - - - [builtins fixtures/paramspec.pyi] [case testOtherVarArgs] @@ -2491,11 +2477,6 @@ def run(func: Callable[Concatenate[int, str, P], T], *args: P.args, **kwargs: P. # E: Argument 2 has incompatible type "*Tuple[int, str]"; expected "P.args" return func2(*args_prefix, *args) - - - - - [builtins fixtures/paramspec.pyi] [case testParamSpecScoping] From daa3ccdd19e1e50ca36f559ca54bd3b2feb5fb68 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 31 Mar 2025 18:46:46 +0200 Subject: [PATCH 5/5] Add test lines with aliased args/kwargs --- test-data/unit/check-parameter-specification.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index c2d9cc012ae1..6f01b15e11f6 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2572,6 +2572,12 @@ def run(predicate: Callable[_P, None], *args: _P.args, **kwargs: _P.kwargs) -> N 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 \ @@ -2582,6 +2588,12 @@ def run2(predicate: Callable[Concatenate[int, _P], None], *args: _P.args, **kwar 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]