From e373e9e1913a618b203ce6f22266b8f7f193dc51 Mon Sep 17 00:00:00 2001 From: Zaid Date: Tue, 4 Aug 2026 22:33:32 +0530 Subject: [PATCH] Relax future() and future_safe() to accept Awaitable, not just Coroutine Both decorators typed their wrapped function argument as returning Coroutine, which rejects any callable typed as returning a plain Awaitable (e.g. an async def that awaits and returns another awaitable, rather than being annotated as returning a Coroutine itself), even though the runtime behavior only relies on awaiting the result. Widen the parameter type to Awaitable and drop the now unused _SecondType TypeVar. Fixes #2404 --- CHANGELOG.md | 10 ++++++ returns/future.py | 35 ++++--------------- .../test_future_decorator.yml | 15 ++++++++ .../test_future_safe_decorator.yml | 16 +++++++++ 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e906a7e6..8a4c613c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ incremental in minor, bugfixes only are patches. See [0Ver](https://0ver.org/). +## 0.29.1 WIP + +### Bugfixes + +- 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) + type-check correctly + + ## 0.29.0 ### Features diff --git a/returns/future.py b/returns/future.py index 12379c2d..e0c755eb 100644 --- a/returns/future.py +++ b/returns/future.py @@ -37,7 +37,6 @@ # Aliases: _FirstType = TypeVar('_FirstType') -_SecondType = TypeVar('_SecondType') # Public composition helpers: @@ -471,10 +470,7 @@ def from_future_result( def future( - function: Callable[ - _FuncParams, - Coroutine[_FirstType, _SecondType, _ValueType_co], - ], + function: Callable[_FuncParams, Awaitable[_ValueType_co]], ) -> Callable[_FuncParams, Future[_ValueType_co]]: """ Decorator to turn a coroutine definition into ``Future`` container. @@ -1526,10 +1522,7 @@ def FutureFailure( # noqa: N802 @overload def future_safe( - exceptions: Callable[ - _FuncParams, - Coroutine[_FirstType, _SecondType, _ValueType_co], - ], + exceptions: Callable[_FuncParams, Awaitable[_ValueType_co]], /, ) -> Callable[_FuncParams, FutureResultE[_ValueType_co]]: ... @@ -1538,33 +1531,20 @@ def future_safe( def future_safe( exceptions: tuple[type[_ExceptionType], ...], ) -> Callable[ - [ - Callable[ - _FuncParams, - Coroutine[_FirstType, _SecondType, _ValueType_co], - ], - ], + [Callable[_FuncParams, Awaitable[_ValueType_co]]], Callable[_FuncParams, FutureResult[_ValueType_co, _ExceptionType]], ]: ... def future_safe( # noqa: WPS212, WPS234, exceptions: ( - Callable[ - _FuncParams, - Coroutine[_FirstType, _SecondType, _ValueType_co], - ] + Callable[_FuncParams, Awaitable[_ValueType_co]] | tuple[type[_ExceptionType], ...] ), ) -> ( Callable[_FuncParams, FutureResultE[_ValueType_co]] | Callable[ - [ - Callable[ - _FuncParams, - Coroutine[_FirstType, _SecondType, _ValueType_co], - ], - ], + [Callable[_FuncParams, Awaitable[_ValueType_co]]], Callable[_FuncParams, FutureResult[_ValueType_co, _ExceptionType]], ] ): @@ -1621,10 +1601,7 @@ def future_safe( # noqa: WPS212, WPS234, """ def _future_safe_factory( # noqa: WPS430 - function: Callable[ - _FuncParams, - Coroutine[_FirstType, _SecondType, _ValueType_co], - ], + function: Callable[_FuncParams, Awaitable[_ValueType_co]], inner_exceptions: tuple[type[_ExceptionType], ...], ) -> Callable[_FuncParams, FutureResult[_ValueType_co, _ExceptionType]]: async def factory( diff --git a/typesafety/test_future/test_future_container/test_future_decorator.yml b/typesafety/test_future/test_future_container/test_future_decorator.yml index fce4d5ac..4ba688ff 100644 --- a/typesafety/test_future/test_future_container/test_future_decorator.yml +++ b/typesafety/test_future/test_future_container/test_future_decorator.yml @@ -22,3 +22,18 @@ ... reveal_type(future(test)) # N: Revealed type is "def (first: int) -> returns.future.Future[str]" + + +- case: future_decorator_with_awaitable_type + disable_cache: false + main: | + from typing import Awaitable, Callable + from returns.future import future + + async def test(first: int) -> int: + return first + + # `future` should accept plain `Awaitable`, not just `Coroutine`. + typed_test: Callable[[int], Awaitable[int]] = test + + reveal_type(future(typed_test)) # N: Revealed type is "def (int) -> returns.future.Future[int]" diff --git a/typesafety/test_future/test_future_result_container/test_future_safe_decorator.yml b/typesafety/test_future/test_future_result_container/test_future_safe_decorator.yml index 28c579ff..23dc5ee3 100644 --- a/typesafety/test_future/test_future_result_container/test_future_safe_decorator.yml +++ b/typesafety/test_future/test_future_result_container/test_future_safe_decorator.yml @@ -55,3 +55,19 @@ return 1 reveal_type(test) # N: Revealed type is "def (first: int, second: str | None =, *, kw: bool =) -> returns.future.FutureResult[int, ValueError]" + + +- case: future_safe_decorator_with_awaitable_type + disable_cache: false + main: | + from typing import Awaitable, Callable + from returns.future import future_safe + + async def test(first: int) -> int: + return first + + # `future_safe` should accept plain `Awaitable`, not just `Coroutine`. + typed_test: Callable[[int], Awaitable[int]] = test + + reveal_type(future_safe(typed_test)) # N: Revealed type is "def (int) -> returns.future.FutureResult[int, Exception]" + reveal_type(future_safe((ValueError,))(typed_test)) # N: Revealed type is "def (int) -> returns.future.FutureResult[int, ValueError]"