From 993afe56f2e8c64a2620a9e84682d870223ad033 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 7 Feb 2025 15:25:26 +0900 Subject: [PATCH 1/5] Merge arg types from *args and **args --- mypy/typeops.py | 7 ++++--- test-data/unit/check-overloading.test | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index 1667e8431a17..f6b781ec1fea 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -464,15 +464,16 @@ def callable_corresponding_argument( # def right(a: int = ...) -> None: ... # def left(__a: int = ..., *, a: int = ...) -> None: ... - from mypy.subtypes import is_equivalent + from mypy.meet import meet_types if ( not (by_name.required or by_pos.required) and by_pos.name is None and by_name.pos is None - and is_equivalent(by_name.typ, by_pos.typ) ): - return FormalArgument(by_name.name, by_pos.pos, by_name.typ, False) + return FormalArgument( + by_name.name, by_pos.pos, meet_types(by_pos.typ, by_name.typ), False + ) return by_name if by_name is not None else by_pos diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 5b8bd51ff9dc..25dfcbcf2015 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6768,3 +6768,19 @@ class D(Generic[T]): a: D[str] # E: Type argument "str" of "D" must be a subtype of "C" reveal_type(a.f(1)) # N: Revealed type is "builtins.int" reveal_type(a.f("x")) # N: Revealed type is "builtins.str" + +[case testOverloadWithTwoRelevantArgsWithDifferentType] +from typing import overload, Union + +@overload +def set(year: int) -> None: + ... + +@overload +def set() -> None: + ... + +# no error here: +def set(*args: object, **kw: int) -> None: + pass +[builtins fixtures/tuple.pyi] From da42901650a95ddaf598f9b2238ff933e079184a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 7 Feb 2025 15:38:47 +0900 Subject: [PATCH 2/5] Solve a followup problem without a tracking issue --- mypy/typeops.py | 16 +++++++--------- test-data/unit/check-overloading.test | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index f6b781ec1fea..0afaaa9a7a3c 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -458,19 +458,17 @@ def callable_corresponding_argument( if by_name is not None and by_pos is not None: if by_name == by_pos: return by_name - # If we're dealing with an optional pos-only and an optional - # name-only arg, merge them. This is the case for all functions - # taking both *args and **args, or a pair of functions like so: + # If we're dealing with an optional pos and an optional + # name arg, merge them. This is the case for all functions + # taking both *args and **args, or a functions like so: # def right(a: int = ...) -> None: ... - # def left(__a: int = ..., *, a: int = ...) -> None: ... + # def left1(__a: int = ..., *, a: int = ...) -> None: ... + # def left2(x: int = ..., a: int = ...) -> None: ... + from mypy.meet import meet_types - if ( - not (by_name.required or by_pos.required) - and by_pos.name is None - and by_name.pos is None - ): + if not (by_name.required or by_pos.required): return FormalArgument( by_name.name, by_pos.pos, meet_types(by_pos.typ, by_name.typ), False ) diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 25dfcbcf2015..98b171c1522b 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6784,3 +6784,18 @@ def set() -> None: def set(*args: object, **kw: int) -> None: pass [builtins fixtures/tuple.pyi] + +[case testOverloadWithTwoRelevantOptionalArgs] +from typing import overload + +@overload +def set(year: int) -> None: + ... + +@overload +def set() -> None: + ... + +# no error: +def set(x: int = 42, year: int = 42) -> None: + pass From 9e768ae604e54e4baf5a254564e3733681b75ea6 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sun, 9 Feb 2025 11:33:05 +0900 Subject: [PATCH 3/5] Don't use a heuristic for ParamSpec inference from arguments The fallout is worse lambda inference, but this is more correct --- mypy/constraints.py | 7 +--- .../unit/check-parameter-specification.test | 33 +++++++++++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 45a96b993563..748c1a93e8d7 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -228,18 +228,13 @@ def infer_constraints_for_callable( if param_spec and callee.arg_kinds[i] in (ARG_STAR, ARG_STAR2): # If actual arguments are mapped to ParamSpec type, we can't infer individual # constraints, instead store them and infer single constraint at the end. - # It is impossible to map actual kind to formal kind, so use some heuristic. - # This inference is used as a fallback, so relying on heuristic should be OK. if not incomplete_star_mapping: param_spec_arg_types.append( mapper.expand_actual_type( actual_arg_type, arg_kinds[actual], None, arg_kinds[actual] ) ) - actual_kind = arg_kinds[actual] - param_spec_arg_kinds.append( - ARG_POS if actual_kind not in (ARG_STAR, ARG_STAR2) else actual_kind - ) + param_spec_arg_kinds.append(arg_kinds[actual]) param_spec_arg_names.append(arg_names[actual] if arg_names else None) else: actual_type = mapper.expand_actual_type( diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 352503023f97..6870d9a53cc4 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -439,13 +439,16 @@ def f(x: int) -> None: pass def g(x: int, y: str) -> None: pass reveal_type(register(lambda: f(1))) # N: Revealed type is "def ()" -reveal_type(register(lambda x: f(x), x=1)) # N: Revealed type is "def (x: Literal[1]?)" +reveal_type(register(lambda x: f(x), x=1)) # N: Revealed type is "def (*, x: Literal[1]?)" \ + # E: Cannot infer type of lambda register(lambda x: f(x)) # E: Cannot infer type of lambda \ # E: Argument 1 to "register" has incompatible type "Callable[[Any], None]"; expected "Callable[[], None]" -register(lambda x: f(x), y=1) # E: Argument 1 to "register" has incompatible type "Callable[[Arg(int, 'x')], None]"; expected "Callable[[Arg(int, 'y')], None]" +register(lambda x: f(x), y=1) # E: Cannot infer type of lambda \ + # E: Argument 1 to "register" has incompatible type "Callable[[Any], None]"; expected "Callable[[NamedArg(int, 'y')], None]" reveal_type(register(lambda x: f(x), 1)) # N: Revealed type is "def (Literal[1]?)" reveal_type(register(lambda x, y: g(x, y), 1, "a")) # N: Revealed type is "def (Literal[1]?, Literal['a']?)" -reveal_type(register(lambda x, y: g(x, y), 1, y="a")) # N: Revealed type is "def (Literal[1]?, y: Literal['a']?)" +reveal_type(register(lambda x, y: g(x, y), 1, y="a")) # N: Revealed type is "def (Literal[1]?, *, y: Literal['a']?)" \ + # E: Cannot infer type of lambda [builtins fixtures/dict.pyi] [case testParamSpecInvalidCalls] @@ -1677,7 +1680,7 @@ class Foo(Generic[P]): def test(*args: P.args, **kwargs: P.kwargs) -> Foo[P]: ... reveal_type(test(1, 2)) # N: Revealed type is "__main__.Foo[[Literal[1]?, Literal[2]?]]" -reveal_type(test(x=1, y=2)) # N: Revealed type is "__main__.Foo[[x: Literal[1]?, y: Literal[2]?]]" +reveal_type(test(x=1, y=2)) # N: Revealed type is "__main__.Foo[[*, x: Literal[1]?, y: Literal[2]?]]" ints = [1, 2, 3] reveal_type(test(*ints)) # N: Revealed type is "__main__.Foo[[*builtins.int]]" [builtins fixtures/paramspec.pyi] @@ -1732,7 +1735,7 @@ apply(apply, test2, 42, "yes") apply(apply, test2, "no", 42) # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int, str], None], str, int], None]" apply(apply, test2, x=42, y="yes") apply(apply, test2, y="yes", x=42) -apply(apply, test2, y=42, x="no") # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int, str], None], int, str], None]" +apply(apply, test2, y=42, x="no") # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int, str], None], NamedArg(int, 'y'), NamedArg(str, 'x')], None]" [builtins fixtures/paramspec.pyi] [case testParamSpecApplyPosVsNamedOptional] @@ -2157,7 +2160,7 @@ reveal_type(submit( # N: Revealed type is "__main__.Result" backend="asyncio", )) submit( - run, # E: Argument 1 to "submit" has incompatible type "Callable[[Callable[[], R], VarArg(object), DefaultNamedArg(str, 'backend')], R]"; expected "Callable[[Callable[[], Result], int], Result]" + run, # E: Argument 1 to "submit" has incompatible type "Callable[[Callable[[], R], VarArg(object), DefaultNamedArg(str, 'backend')], R]"; expected "Callable[[Callable[[], Result], NamedArg(int, 'backend')], Result]" run_portal, backend=int(), ) @@ -2532,3 +2535,21 @@ class GenericWrapper(Generic[P]): def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... [builtins fixtures/paramspec.pyi] + +[case testParamSpecInferenceFromArgs] +from typing_extensions import ParamSpec +from typing import Any, Callable, Union + +P = ParamSpec("P") + +def into(f: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: + return None + +class C: + def f(self, y: bool = False, *, x: int = 42) -> None: + return None + +ex: Union[C, Any] = C() + +into(ex.f, x=-1) +[builtins fixtures/paramspec.pyi] From cc16b253ccd95934b4a153cc69bc5920644ea0a2 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 18 Feb 2025 00:07:43 +0900 Subject: [PATCH 4/5] Revert "Don't use a heuristic for ParamSpec inference from arguments" This reverts commit 9e768ae604e54e4baf5a254564e3733681b75ea6. --- mypy/constraints.py | 7 +++- .../unit/check-parameter-specification.test | 33 ++++--------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index efb8894e4e10..defcac21bc66 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -228,13 +228,18 @@ def infer_constraints_for_callable( if param_spec and callee.arg_kinds[i] in (ARG_STAR, ARG_STAR2): # If actual arguments are mapped to ParamSpec type, we can't infer individual # constraints, instead store them and infer single constraint at the end. + # It is impossible to map actual kind to formal kind, so use some heuristic. + # This inference is used as a fallback, so relying on heuristic should be OK. if not incomplete_star_mapping: param_spec_arg_types.append( mapper.expand_actual_type( actual_arg_type, arg_kinds[actual], None, arg_kinds[actual] ) ) - param_spec_arg_kinds.append(arg_kinds[actual]) + actual_kind = arg_kinds[actual] + param_spec_arg_kinds.append( + ARG_POS if actual_kind not in (ARG_STAR, ARG_STAR2) else actual_kind + ) param_spec_arg_names.append(arg_names[actual] if arg_names else None) else: actual_type = mapper.expand_actual_type( diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index c0275cc550e5..5530bc0ecbf9 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -440,16 +440,13 @@ def f(x: int) -> None: pass def g(x: int, y: str) -> None: pass reveal_type(register(lambda: f(1))) # N: Revealed type is "def ()" -reveal_type(register(lambda x: f(x), x=1)) # N: Revealed type is "def (*, x: Literal[1]?)" \ - # E: Cannot infer type of lambda +reveal_type(register(lambda x: f(x), x=1)) # N: Revealed type is "def (x: Literal[1]?)" register(lambda x: f(x)) # E: Cannot infer type of lambda \ # E: Argument 1 to "register" has incompatible type "Callable[[Any], None]"; expected "Callable[[], None]" -register(lambda x: f(x), y=1) # E: Cannot infer type of lambda \ - # E: Argument 1 to "register" has incompatible type "Callable[[Any], None]"; expected "Callable[[NamedArg(int, 'y')], None]" +register(lambda x: f(x), y=1) # E: Argument 1 to "register" has incompatible type "Callable[[Arg(int, 'x')], None]"; expected "Callable[[Arg(int, 'y')], None]" reveal_type(register(lambda x: f(x), 1)) # N: Revealed type is "def (Literal[1]?)" reveal_type(register(lambda x, y: g(x, y), 1, "a")) # N: Revealed type is "def (Literal[1]?, Literal['a']?)" -reveal_type(register(lambda x, y: g(x, y), 1, y="a")) # N: Revealed type is "def (Literal[1]?, *, y: Literal['a']?)" \ - # E: Cannot infer type of lambda +reveal_type(register(lambda x, y: g(x, y), 1, y="a")) # N: Revealed type is "def (Literal[1]?, y: Literal['a']?)" [builtins fixtures/dict.pyi] [case testParamSpecInvalidCalls] @@ -1681,7 +1678,7 @@ class Foo(Generic[P]): def test(*args: P.args, **kwargs: P.kwargs) -> Foo[P]: ... reveal_type(test(1, 2)) # N: Revealed type is "__main__.Foo[[Literal[1]?, Literal[2]?]]" -reveal_type(test(x=1, y=2)) # N: Revealed type is "__main__.Foo[[*, x: Literal[1]?, y: Literal[2]?]]" +reveal_type(test(x=1, y=2)) # N: Revealed type is "__main__.Foo[[x: Literal[1]?, y: Literal[2]?]]" ints = [1, 2, 3] reveal_type(test(*ints)) # N: Revealed type is "__main__.Foo[[*builtins.int]]" [builtins fixtures/paramspec.pyi] @@ -1736,7 +1733,7 @@ apply(apply, test2, 42, "yes") apply(apply, test2, "no", 42) # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int, str], None], str, int], None]" apply(apply, test2, x=42, y="yes") apply(apply, test2, y="yes", x=42) -apply(apply, test2, y=42, x="no") # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int, str], None], NamedArg(int, 'y'), NamedArg(str, 'x')], None]" +apply(apply, test2, y=42, x="no") # E: Argument 1 to "apply" has incompatible type "Callable[[Callable[P, T], **P], None]"; expected "Callable[[Callable[[int, str], None], int, str], None]" [builtins fixtures/paramspec.pyi] [case testParamSpecApplyPosVsNamedOptional] @@ -2161,7 +2158,7 @@ reveal_type(submit( # N: Revealed type is "__main__.Result" backend="asyncio", )) submit( - run, # E: Argument 1 to "submit" has incompatible type "Callable[[Callable[[], R], VarArg(object), DefaultNamedArg(str, 'backend')], R]"; expected "Callable[[Callable[[], Result], NamedArg(int, 'backend')], Result]" + run, # E: Argument 1 to "submit" has incompatible type "Callable[[Callable[[], R], VarArg(object), DefaultNamedArg(str, 'backend')], R]"; expected "Callable[[Callable[[], Result], int], Result]" run_portal, backend=int(), ) @@ -2537,24 +2534,6 @@ class GenericWrapper(Generic[P]): def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... [builtins fixtures/paramspec.pyi] -[case testParamSpecInferenceFromArgs] -from typing_extensions import ParamSpec -from typing import Any, Callable, Union - -P = ParamSpec("P") - -def into(f: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: - return None - -class C: - def f(self, y: bool = False, *, x: int = 42) -> None: - return None - -ex: Union[C, Any] = C() - -into(ex.f, x=-1) -[builtins fixtures/paramspec.pyi] - [case testCallbackProtocolClassObjectParamSpec] from typing import Any, Callable, Protocol, Optional, Generic from typing_extensions import ParamSpec From 59ce4408ef58669e0a5d38e2be04329bda9fc485 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 18 Feb 2025 00:30:27 +0900 Subject: [PATCH 5/5] Expand heuristic for inferring Parameters from a Callable --- mypy/checkexpr.py | 16 ++++++++++++++++ .../unit/check-parameter-specification.test | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 286ef0dab6ae..2f872212640a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2278,6 +2278,22 @@ def get_arg_infer_passes( # run(test, 1, 2) # we will use `test` for inference, since it will allow to infer also # argument *names* for P <: [x: int, y: int]. + if isinstance(p_actual, UnionType): + new_items = [] + for item in p_actual.items: + # narrow the union based on some approximations + p_item = get_proper_type(item) + if isinstance(p_item, CallableType) or ( + isinstance(p_item, Instance) + and find_member("__call__", p_item, p_item, is_operator=True) + is not None + ): + new_items.append(p_item) + if len(new_items) == 2: + break + + if len(new_items) == 1: + p_actual = new_items[0] if isinstance(p_actual, Instance): call_method = find_member("__call__", p_actual, p_actual, is_operator=True) if call_method is not None: diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 5530bc0ecbf9..d61f3f9b4536 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2560,3 +2560,21 @@ 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 testParamSpecInferenceWithAny] +from typing_extensions import ParamSpec +from typing import Any, Callable, Union + +P = ParamSpec("P") + +def into(f: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: + return None + +class C: + def f(self, y: bool = False, *, x: int = 42) -> None: + return None + +ex: Union[C, Any] = C() + +into(ex.f, x=-1) +[builtins fixtures/paramspec.pyi]