From 45bab8caa824e418d88863d8a753b769ce31347a Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 21 Jul 2025 18:55:44 +0200 Subject: [PATCH 1/2] When selecting an overload item for constraint template matching, treat any ParamSpec in the template as free --- mypy/constraints.py | 1 + mypy/subtypes.py | 11 ++++++--- .../unit/check-parameter-specification.test | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 9eeea3cb2c26..6f1e369769eb 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1434,6 +1434,7 @@ def find_matching_overload_item(overloaded: Overloaded, template: CallableType) is_compat=mypy.subtypes.is_subtype, is_proper_subtype=False, ignore_return=True, + map_template_paramspec=True, ): return item # Fall back to the first item if we can't find a match. This is totally arbitrary -- diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 7da258a827f3..d5c918b234e2 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1576,6 +1576,7 @@ def is_callable_compatible( check_args_covariantly: bool = False, allow_partial_overlap: bool = False, strict_concatenate: bool = False, + map_template_paramspec: bool = False, ) -> bool: """Is the left compatible with the right, using the provided compatibility check? @@ -1717,6 +1718,7 @@ def g(x: int) -> int: ... ignore_pos_arg_names=ignore_pos_arg_names, allow_partial_overlap=allow_partial_overlap, strict_concatenate_check=strict_concatenate_check, + map_template_paramspec=map_template_paramspec, ) @@ -1753,6 +1755,7 @@ def are_parameters_compatible( ignore_pos_arg_names: bool = False, allow_partial_overlap: bool = False, strict_concatenate_check: bool = False, + map_template_paramspec: bool = False, ) -> bool: """Helper function for is_callable_compatible, used for Parameter compatibility""" if right.is_ellipsis_args and not is_proper_subtype: @@ -1781,6 +1784,8 @@ def are_parameters_compatible( # a subtype of erased template type. trivial_vararg_suffix = True + right_is_pspec = map_template_paramspec and right.param_spec() is not None + # Match up corresponding arguments and check them for compatibility. In # every pair (argL, argR) of corresponding arguments from L and R, argL must # be "more general" than argR if L is to be a subtype of R. @@ -1817,7 +1822,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N _incompatible(left_star, right_star) and not trivial_vararg_suffix or _incompatible(left_star2, right_star2) - ): + ) and not right_is_pspec: return False # Phase 1b: Check non-star args: for every arg right can accept, left must @@ -1848,7 +1853,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N # arguments. Get all further positional args of left, and make sure # they're more general than the corresponding member in right. # TODO: handle suffix in UnpackType (i.e. *args: *Tuple[Ts, X, Y]). - if right_star is not None and not trivial_vararg_suffix: + if right_star is not None and not trivial_vararg_suffix and not right_is_pspec: # Synthesize an anonymous formal argument for the right right_by_position = right.try_synthesizing_arg_from_vararg(None) assert right_by_position is not None @@ -1875,7 +1880,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N # Phase 1d: Check kw args. Right has an infinite series of optional named # arguments. Get all further named args of left, and make sure # they're more general than the corresponding member in right. - if right_star2 is not None: + if right_star2 is not None and not right_is_pspec: right_names = {name for name in right.arg_names if name is not None} left_only_names = set() for name, kind in zip(left.arg_names, left.arg_kinds): diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 0835ba7ac57d..b8331af2b613 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2603,3 +2603,26 @@ def run3(predicate: Callable[Concatenate[int, str, _P], None], *args: _P.args, * # 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] + +[case testParamSpecOverloadProtocol] +from typing import ParamSpec, Protocol, TypeVar, overload + +_A_contra = TypeVar("_A_contra", bound=str, contravariant=True) +_P = ParamSpec("_P") + +class Callback(Protocol[_A_contra, _P]): + def method(self, a: _A_contra, *args: _P.args, **kwargs: _P.kwargs) -> None: ... + +class Impl: + @overload + def method(self, a: int, b: str) -> None: ... + @overload + def method(self, a: str, b: int) -> None: ... + def method(self, a, b) -> None: ... + +def accepts_callback(cb: Callback[str, _P], *args: _P.args, **kwargs: _P.kwargs) -> int: + return 1 + +a = accepts_callback(Impl(), 1) +reveal_type(a) # N: Revealed type is "builtins.int" +[builtins fixtures/paramspec.pyi] From ff26a142d2c932838a58354a0a9b0c4bfbce3835 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 21 Jul 2025 19:05:32 +0200 Subject: [PATCH 2/2] `.param_spec()` not available here --- mypy/subtypes.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index d5c918b234e2..4d2ac9536d68 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1718,7 +1718,7 @@ def g(x: int) -> int: ... ignore_pos_arg_names=ignore_pos_arg_names, allow_partial_overlap=allow_partial_overlap, strict_concatenate_check=strict_concatenate_check, - map_template_paramspec=map_template_paramspec, + template_has_paramspec=map_template_paramspec and right.param_spec() is not None, ) @@ -1755,7 +1755,7 @@ def are_parameters_compatible( ignore_pos_arg_names: bool = False, allow_partial_overlap: bool = False, strict_concatenate_check: bool = False, - map_template_paramspec: bool = False, + template_has_paramspec: bool = False, ) -> bool: """Helper function for is_callable_compatible, used for Parameter compatibility""" if right.is_ellipsis_args and not is_proper_subtype: @@ -1784,8 +1784,6 @@ def are_parameters_compatible( # a subtype of erased template type. trivial_vararg_suffix = True - right_is_pspec = map_template_paramspec and right.param_spec() is not None - # Match up corresponding arguments and check them for compatibility. In # every pair (argL, argR) of corresponding arguments from L and R, argL must # be "more general" than argR if L is to be a subtype of R. @@ -1822,7 +1820,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N _incompatible(left_star, right_star) and not trivial_vararg_suffix or _incompatible(left_star2, right_star2) - ) and not right_is_pspec: + ) and not template_has_paramspec: return False # Phase 1b: Check non-star args: for every arg right can accept, left must @@ -1853,7 +1851,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N # arguments. Get all further positional args of left, and make sure # they're more general than the corresponding member in right. # TODO: handle suffix in UnpackType (i.e. *args: *Tuple[Ts, X, Y]). - if right_star is not None and not trivial_vararg_suffix and not right_is_pspec: + if right_star is not None and not trivial_vararg_suffix and not template_has_paramspec: # Synthesize an anonymous formal argument for the right right_by_position = right.try_synthesizing_arg_from_vararg(None) assert right_by_position is not None @@ -1880,7 +1878,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N # Phase 1d: Check kw args. Right has an infinite series of optional named # arguments. Get all further named args of left, and make sure # they're more general than the corresponding member in right. - if right_star2 is not None and not right_is_pspec: + if right_star2 is not None and not template_has_paramspec: right_names = {name for name in right.arg_names if name is not None} left_only_names = set() for name, kind in zip(left.arg_names, left.arg_kinds):