Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exceptions to @future_safe; deprecate FutureResultE #1880

Merged
merged 11 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ See [0Ver](https://0ver.org/).

### Features

- Add picky exceptions to `future_safe` decorator like `safe` has.
- Improve inference of `ResultLike` objects when exception catching
decorator is applied with explicit exception types
- Add picky exceptions to `impure_safe` decorator like `safe` has. Issue #1543


## Deprecated

- FutureResultE from future


## 0.23.0

### Features
Expand Down
12 changes: 0 additions & 12 deletions docs/pages/future.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,6 @@ and without a single ``async/await``.
That example illustrates the whole point of our actions: writing
sync code that executes asynchronously without any magic at all.

Aliases
-------

There are several useful aliases for ``FutureResult`` type
with some common values:

- :attr:`returns.future.FutureResultE` is an alias
for ``FutureResult[... Exception]``,
just use it when you want to work with ``FutureResult`` containers
that use exceptions as error type.
It is named ``FutureResultE`` because it is ``FutureResultException``
and ``FutureResultError`` at the same time.


Decorators
Expand Down
197 changes: 86 additions & 111 deletions poetry.lock

Large diffs are not rendered by default.

117 changes: 99 additions & 18 deletions returns/future.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
Callable,
Coroutine,
Generator,
Tuple,
Type,
TypeVar,
Union,
final,
overload,
)

from typing_extensions import ParamSpec
Expand Down Expand Up @@ -1450,20 +1454,61 @@ def FutureFailure( # noqa: N802
return FutureResult.from_failure(inner_value)


# Aliases:

#: Alias for a popular case when ``Result`` has ``Exception`` as error type.
# Deprecated
FutureResultE = FutureResult[_ValueType, Exception]


_ExceptionType = TypeVar('_ExceptionType', bound=Exception)


# Decorators:

@overload
def future_safe(
function: Callable[
exceptions: Callable[
_FuncParams,
Coroutine[_FirstType, _SecondType, _ValueType],
],
) -> Callable[_FuncParams, FutureResultE[_ValueType]]:
/,
) -> Callable[_FuncParams, FutureResult[_ValueType, Exception]]:
"""Decorator to convert exception-throwing for any kind of Exception."""


@overload
def future_safe(
exceptions: Tuple[Type[_ExceptionType], ...],
) -> Callable[
[
Callable[
_FuncParams,
Coroutine[_FirstType, _SecondType, _ValueType],
],
],
Callable[_FuncParams, FutureResult[_ValueType, _ExceptionType]],
]:
"""Decorator to convert exception-throwing just for a set of Exceptions."""


def future_safe( # noqa: C901, WPS212, WPS234,
exceptions: Union[
Callable[
_FuncParams,
Coroutine[_FirstType, _SecondType, _ValueType],
],
Tuple[Type[_ExceptionType], ...],
],
) -> Union[
Callable[_FuncParams, FutureResult[_ValueType, Exception]],
Callable[
[
Callable[
_FuncParams,
Coroutine[_FirstType, _SecondType, _ValueType],
],
],
Callable[_FuncParams, FutureResult[_ValueType, _ExceptionType]],
],
]:
"""
Decorator to convert exception-throwing coroutine to ``FutureResult``.

Expand Down Expand Up @@ -1491,20 +1536,56 @@ def future_safe(
... IOFailure,
... )

You can also use it with explicit exception types as the first argument:

.. code:: python

>>> from returns.future import future_safe
>>> from returns.io import IOFailure, IOSuccess

>>> @future_safe(exceptions=(ZeroDivisionError,))
... async def might_raise(arg: int) -> float:
... return 1 / arg

>>> assert anyio.run(might_raise(2).awaitable) == IOSuccess(0.5)
>>> assert isinstance(
... anyio.run(might_raise(0).awaitable),
... IOFailure,
... )

In this case, only exceptions that are explicitly
listed are going to be caught.

Similar to :func:`returns.io.impure_safe` and :func:`returns.result.safe`
decorators, but works with ``async`` functions.

"""
async def factory(
*args: _FuncParams.args,
**kwargs: _FuncParams.kwargs,
) -> Result[_ValueType, Exception]:
try:
return Success(await function(*args, **kwargs))
except Exception as exc:
return Failure(exc)

@wraps(function)
def decorator(*args, **kwargs):
return FutureResult(factory(*args, **kwargs))
return decorator
def _future_safe_factory( # noqa: WPS430
function: Callable[
_FuncParams,
Coroutine[_FirstType, _SecondType, _ValueType],
],
inner_exceptions: Tuple[Type[_ExceptionType], ...],
) -> Callable[_FuncParams, FutureResult[_ValueType, _ExceptionType]]:
async def factory(
*args: _FuncParams.args,
**kwargs: _FuncParams.kwargs,
) -> Result[_ValueType, _ExceptionType]:
try:
return Success(await function(*args, **kwargs))
except inner_exceptions as exc:
return Failure(exc)

@wraps(function)
def decorator(
*args: _FuncParams.args,
**kwargs: _FuncParams.kwargs,
) -> FutureResult[_ValueType, _ExceptionType]:
return FutureResult(factory(*args, **kwargs))
return decorator
if isinstance(exceptions, tuple):
return lambda function: _future_safe_factory(function, exceptions)
return _future_safe_factory(
exceptions,
(Exception,), # type: ignore[arg-type]
)
6 changes: 3 additions & 3 deletions tests/test_examples/test_context/test_reader_future_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from returns.context import RequiresContextFutureResultE
from returns.functions import tap
from returns.future import FutureResultE, future_safe
from returns.future import FutureResult, future_safe
from returns.iterables import Fold
from returns.pipeline import managed
from returns.result import ResultE, safe
Expand All @@ -24,7 +24,7 @@ class _Post(TypedDict):
def _close(
client: httpx.AsyncClient,
raw_value: ResultE[Sequence[str]],
) -> FutureResultE[None]:
) -> FutureResult[None, Exception]:
return future_safe(client.aclose)()


Expand Down Expand Up @@ -65,7 +65,7 @@ def factory(post: _Post) -> str:
# because we want to highlight `managed` in this example:
managed_httpx = managed(_show_titles(3), _close)
future_result = managed_httpx(
FutureResultE.from_value(httpx.AsyncClient(timeout=5)),
FutureResult.from_value(httpx.AsyncClient(timeout=5)),
)
print(anyio.run(future_result.awaitable)) # noqa: WPS421
# <IOResult: <Success: (
Expand Down
6 changes: 4 additions & 2 deletions tests/test_examples/test_future/test_future_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import httpx # you would need to `pip install httpx`
from typing_extensions import TypedDict

from returns.future import FutureResultE, future_safe
from returns.future import FutureResult, future_safe
from returns.io import IOResultE
from returns.iterables import Fold

Expand All @@ -27,7 +27,9 @@ async def _fetch_post(post_id: int) -> _Post:
return cast(_Post, response.json()) # or validate the response


def _show_titles(number_of_posts: int) -> Sequence[FutureResultE[str]]:
def _show_titles(number_of_posts: int) -> Sequence[
FutureResult[str, Exception]
]:
def factory(post: _Post) -> str:
return post['title']

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Union

import pytest

from returns.future import FutureResult, future_safe
Expand All @@ -9,6 +11,17 @@ async def _coro(arg: int) -> float:
return 1 / arg


@future_safe(exceptions=(ZeroDivisionError,))
async def _coro_two(arg: int) -> float:
return 1 / arg


@future_safe((ZeroDivisionError,))
async def _coro_three(arg: Union[int, str]) -> float:
assert isinstance(arg, int)
return 1 / arg


@pytest.mark.anyio
async def test_future_safe_decorator():
"""Ensure that coroutine marked with ``@future_safe``."""
Expand All @@ -25,3 +38,22 @@ async def test_future_safe_decorator_failure():

assert isinstance(future_instance, FutureResult)
assert isinstance(await future_instance, IOFailure)


@pytest.mark.anyio
async def test_future_safe_decorator_w_expected_error(subtests):
"""Ensure that coroutine marked with ``@future_safe``."""
expected = '<IOResult: <Failure: division by zero>>'

for future_instance in (_coro_two(0), _coro_three(0)):
with subtests.test(future_instance=future_instance):
assert isinstance(future_instance, FutureResult)
inner_result = await future_instance
assert str(inner_result) == expected


@pytest.mark.anyio
@pytest.mark.xfail(raises=AssertionError)
async def test_future_safe_decorator_w_unexpected_error():
"""Ensure that coroutine marked with ``@future_safe``."""
await _coro_three('0')
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,33 @@
return 1

reveal_type(future_safe(test)) # N: Revealed type is "def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.future.FutureResult[builtins.int, builtins.Exception]"


- case: future_safe_decorator_with_pos_params
disable_cache: false
main: |
from typing import Optional
from returns.future import future_safe

@future_safe((ValueError,))
async def test(
first: int, second: Optional[str] = None, *, kw: bool = True,
) -> int:
return 1

reveal_type(test) # N: Revealed type is "def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.future.FutureResult[builtins.int, builtins.ValueError]"


- case: future_safe_decorator_with_named_params
disable_cache: false
main: |
from typing import Optional
from returns.future import future_safe

@future_safe(exceptions=(ValueError,))
async def test(
first: int, second: Optional[str] = None, *, kw: bool = True,
) -> int:
return 1

reveal_type(test) # N: Revealed type is "def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.future.FutureResult[builtins.int, builtins.ValueError]"