From daab192b5482f1dd441eaf464118e1c2c2250da8 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:51:35 +0300 Subject: [PATCH 1/3] Fix partial varargs inference after keyword binding --- CHANGELOG.md | 3 + .../mypy/_typeops/transform_callable.py | 4 +- .../test_partial/test_partial_arguments.yml | 60 ++++++++++++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f280a6..8a13c481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ See [0Ver](https://0ver.org/). ### Bugfixes +- Fixes `partial` retaining variadic positional arguments in its inferred + signature after a positional parameter is bound by keyword + - Relaxes `future` and `future_safe` decorator argument types from `Coroutine` to `Awaitable`, so plain `async def` functions wrapping another awaitable (instead of being coroutine functions themselves) diff --git a/returns/contrib/mypy/_typeops/transform_callable.py b/returns/contrib/mypy/_typeops/transform_callable.py index 450cbd57..bb58784a 100644 --- a/returns/contrib/mypy/_typeops/transform_callable.py +++ b/returns/contrib/mypy/_typeops/transform_callable.py @@ -163,7 +163,8 @@ def diff(self, applied_args: list[FuncArg]) -> CallableType: # longer be filled positionally, so every remaining positional # parameter after it must become keyword-only. Otherwise the partial # would accept positional arguments that raise a ``TypeError`` at - # runtime. See issue #2191. + # runtime. Variadic positional parameters become unreachable too. + # See issue #2191. keyword_hole_at = self._keyword_hole_at( original_args, applied_args, @@ -173,6 +174,7 @@ def diff(self, applied_args: list[FuncArg]) -> CallableType: self._remaining_arg(arg, keyword_only=index > keyword_hole_at) for index, arg in enumerate(original_args) if not self._was_applied(arg, index, intermediate_names) + and (arg.kind != ARG_STAR or index < keyword_hole_at) ]) def _keyword_hole_at( diff --git a/typesafety/test_curry/test_partial/test_partial_arguments.yml b/typesafety/test_curry/test_partial/test_partial_arguments.yml index f3342efe..929a03f2 100644 --- a/typesafety/test_curry/test_partial/test_partial_arguments.yml +++ b/typesafety/test_curry/test_partial/test_partial_arguments.yml @@ -28,9 +28,9 @@ main:14: note: Revealed type is "def (a: int, b: int, c: int =, *args: float, d: str, e: bool =, **kwargs: str) -> str" main:15: note: Revealed type is "def (b: int, c: int =, *args: float, d: str, e: bool =, **kwargs: str) -> str" main:16: note: Revealed type is "def (c: int =, *args: float, d: str, e: bool =, **kwargs: str) -> str" - main:17: note: Revealed type is "def (*args: float, d: str, e: bool =, **kwargs: str) -> str" - main:18: note: Revealed type is "def (*args: float, e: bool =, **kwargs: str) -> str" - main:19: note: Revealed type is "def (*args: float, d: str, **kwargs: str) -> str" + main:17: note: Revealed type is "def (*, d: str, e: bool =, **kwargs: str) -> str" + main:18: note: Revealed type is "def (*, e: bool =, **kwargs: str) -> str" + main:19: note: Revealed type is "def (*, d: str, **kwargs: str) -> str" main:20: note: Revealed type is "def (c: int =, *args: float, e: bool =, **kwargs: str) -> str" main:21: note: Revealed type is "def (c: int =, *args: float, d: str, **kwargs: str) -> str" main:22: note: Revealed type is "def (*args: float, d: str, e: bool =, **kwargs: str) -> str" @@ -211,3 +211,57 @@ from returns.curry import partial reveal_type(partial((lambda x, y: str(x + y)), 1)) # N: Revealed type is "def (y: Any) -> str" + + +- case: partial_keyword_bound_positional_removes_varargs + disable_cache: false + main: | + from returns.curry import partial + + def multiple(first: int, *args: int, flag: bool = False) -> str: + ... + + bound = partial(multiple, first=1) + reveal_type(bound) # N: Revealed type is "def (*, flag: bool =) -> str" + bound(flag=True) + bound(True) # E: Too many positional arguments [call-arg] + + +- case: partial_keyword_hole_before_varargs_keeps_prefix + disable_cache: false + main: | + from returns.curry import partial + + def multiple(first: int, second: int, *args: int, flag: bool = False) -> str: + ... + + bound = partial(multiple, second=2) + reveal_type(bound) # N: Revealed type is "def (first: int, *, flag: bool =) -> str" + bound(1, flag=True) + bound(1, True) # E: Too many positional arguments [call-arg] + + +- case: partial_keyword_only_binding_preserves_varargs + disable_cache: false + main: | + from returns.curry import partial + + def multiple(first: int, *args: int, flag: bool = False) -> str: + ... + + bound = partial(multiple, flag=True) + reveal_type(bound) # N: Revealed type is "def (first: int, *args: int) -> str" + bound(1, 2, 3) + + +- case: partial_positional_binding_preserves_varargs + disable_cache: false + main: | + from returns.curry import partial + + def multiple(first: int, *args: int, flag: bool = False) -> str: + ... + + bound = partial(multiple, 1, 2) + reveal_type(bound) # N: Revealed type is "def (*args: int, flag: bool =) -> str" + bound(3, 4, flag=True) From 4d74c0f015a790e1fdeaf95bd4b256b0258dd86f Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:47:25 +0300 Subject: [PATCH 2/3] Consolidate partial type tests requested in review --- CHANGELOG.md | 3 -- .../test_partial/test_partial_arguments.yml | 36 +++++-------------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a13c481..14f280a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,6 @@ See [0Ver](https://0ver.org/). ### Bugfixes -- Fixes `partial` retaining variadic positional arguments in its inferred - signature after a positional parameter is bound by keyword - - Relaxes `future` and `future_safe` decorator argument types from `Coroutine` to `Awaitable`, so plain `async def` functions wrapping another awaitable (instead of being coroutine functions themselves) diff --git a/typesafety/test_curry/test_partial/test_partial_arguments.yml b/typesafety/test_curry/test_partial/test_partial_arguments.yml index 929a03f2..8d7f58e3 100644 --- a/typesafety/test_curry/test_partial/test_partial_arguments.yml +++ b/typesafety/test_curry/test_partial/test_partial_arguments.yml @@ -213,7 +213,7 @@ reveal_type(partial((lambda x, y: str(x + y)), 1)) # N: Revealed type is "def (y: Any) -> str" -- case: partial_keyword_bound_positional_removes_varargs +- case: partial_variadic_argument_binding disable_cache: false main: | from returns.curry import partial @@ -226,6 +226,14 @@ bound(flag=True) bound(True) # E: Too many positional arguments [call-arg] + keyword_bound = partial(multiple, flag=True) + reveal_type(keyword_bound) # N: Revealed type is "def (first: int, *args: int) -> str" + keyword_bound(1, 2, 3) + + positional_bound = partial(multiple, 1, 2) + reveal_type(positional_bound) # N: Revealed type is "def (*args: int, flag: bool =) -> str" + positional_bound(3, 4, flag=True) + - case: partial_keyword_hole_before_varargs_keeps_prefix disable_cache: false @@ -239,29 +247,3 @@ reveal_type(bound) # N: Revealed type is "def (first: int, *, flag: bool =) -> str" bound(1, flag=True) bound(1, True) # E: Too many positional arguments [call-arg] - - -- case: partial_keyword_only_binding_preserves_varargs - disable_cache: false - main: | - from returns.curry import partial - - def multiple(first: int, *args: int, flag: bool = False) -> str: - ... - - bound = partial(multiple, flag=True) - reveal_type(bound) # N: Revealed type is "def (first: int, *args: int) -> str" - bound(1, 2, 3) - - -- case: partial_positional_binding_preserves_varargs - disable_cache: false - main: | - from returns.curry import partial - - def multiple(first: int, *args: int, flag: bool = False) -> str: - ... - - bound = partial(multiple, 1, 2) - reveal_type(bound) # N: Revealed type is "def (*args: int, flag: bool =) -> str" - bound(3, 4, flag=True) From 166d0b7116bb3326a5a158ae6e1ec37bb9c8783f Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:05:50 +0300 Subject: [PATCH 3/3] Assert return types for bound variadic partial calls --- .../test_curry/test_partial/test_partial_arguments.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typesafety/test_curry/test_partial/test_partial_arguments.yml b/typesafety/test_curry/test_partial/test_partial_arguments.yml index 8d7f58e3..01517e34 100644 --- a/typesafety/test_curry/test_partial/test_partial_arguments.yml +++ b/typesafety/test_curry/test_partial/test_partial_arguments.yml @@ -223,16 +223,16 @@ bound = partial(multiple, first=1) reveal_type(bound) # N: Revealed type is "def (*, flag: bool =) -> str" - bound(flag=True) + reveal_type(bound(flag=True)) # N: Revealed type is "str" bound(True) # E: Too many positional arguments [call-arg] keyword_bound = partial(multiple, flag=True) reveal_type(keyword_bound) # N: Revealed type is "def (first: int, *args: int) -> str" - keyword_bound(1, 2, 3) + reveal_type(keyword_bound(1, 2, 3)) # N: Revealed type is "str" positional_bound = partial(multiple, 1, 2) reveal_type(positional_bound) # N: Revealed type is "def (*args: int, flag: bool =) -> str" - positional_bound(3, 4, flag=True) + reveal_type(positional_bound(3, 4, flag=True)) # N: Revealed type is "str" - case: partial_keyword_hole_before_varargs_keeps_prefix