Skip to content

Commit a82b7f5

Browse files
RomanMIzulinroman matveevsobolevn
authored
Add exceptions to @future_safe; deprecate FutureResultE (#1880)
Co-authored-by: roman matveev <[email protected]> Co-authored-by: sobolevn <[email protected]>
1 parent 9c09096 commit a82b7f5

File tree

8 files changed

+260
-146
lines changed

8 files changed

+260
-146
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ See [0Ver](https://0ver.org/).
1010

1111
### Features
1212

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

1718

19+
## Deprecated
20+
21+
- FutureResultE from future
22+
23+
1824
## 0.23.0
1925

2026
### Features

docs/pages/future.rst

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,6 @@ and without a single ``async/await``.
100100
That example illustrates the whole point of our actions: writing
101101
sync code that executes asynchronously without any magic at all.
102102

103-
Aliases
104-
-------
105-
106-
There are several useful aliases for ``FutureResult`` type
107-
with some common values:
108-
109-
- :attr:`returns.future.FutureResultE` is an alias
110-
for ``FutureResult[... Exception]``,
111-
just use it when you want to work with ``FutureResult`` containers
112-
that use exceptions as error type.
113-
It is named ``FutureResultE`` because it is ``FutureResultException``
114-
and ``FutureResultError`` at the same time.
115103

116104

117105
Decorators

poetry.lock

Lines changed: 86 additions & 111 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

returns/future.py

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
Callable,
88
Coroutine,
99
Generator,
10+
Tuple,
11+
Type,
1012
TypeVar,
13+
Union,
1114
final,
15+
overload,
1216
)
1317

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

14521456

1453-
# Aliases:
1454-
1455-
#: Alias for a popular case when ``Result`` has ``Exception`` as error type.
1457+
# Deprecated
14561458
FutureResultE = FutureResult[_ValueType, Exception]
14571459

14581460

1461+
_ExceptionType = TypeVar('_ExceptionType', bound=Exception)
1462+
1463+
14591464
# Decorators:
14601465

1466+
@overload
14611467
def future_safe(
1462-
function: Callable[
1468+
exceptions: Callable[
14631469
_FuncParams,
14641470
Coroutine[_FirstType, _SecondType, _ValueType],
14651471
],
1466-
) -> Callable[_FuncParams, FutureResultE[_ValueType]]:
1472+
/,
1473+
) -> Callable[_FuncParams, FutureResult[_ValueType, Exception]]:
1474+
"""Decorator to convert exception-throwing for any kind of Exception."""
1475+
1476+
1477+
@overload
1478+
def future_safe(
1479+
exceptions: Tuple[Type[_ExceptionType], ...],
1480+
) -> Callable[
1481+
[
1482+
Callable[
1483+
_FuncParams,
1484+
Coroutine[_FirstType, _SecondType, _ValueType],
1485+
],
1486+
],
1487+
Callable[_FuncParams, FutureResult[_ValueType, _ExceptionType]],
1488+
]:
1489+
"""Decorator to convert exception-throwing just for a set of Exceptions."""
1490+
1491+
1492+
def future_safe( # noqa: C901, WPS212, WPS234,
1493+
exceptions: Union[
1494+
Callable[
1495+
_FuncParams,
1496+
Coroutine[_FirstType, _SecondType, _ValueType],
1497+
],
1498+
Tuple[Type[_ExceptionType], ...],
1499+
],
1500+
) -> Union[
1501+
Callable[_FuncParams, FutureResult[_ValueType, Exception]],
1502+
Callable[
1503+
[
1504+
Callable[
1505+
_FuncParams,
1506+
Coroutine[_FirstType, _SecondType, _ValueType],
1507+
],
1508+
],
1509+
Callable[_FuncParams, FutureResult[_ValueType, _ExceptionType]],
1510+
],
1511+
]:
14671512
"""
14681513
Decorator to convert exception-throwing coroutine to ``FutureResult``.
14691514
@@ -1491,20 +1536,56 @@ def future_safe(
14911536
... IOFailure,
14921537
... )
14931538
1539+
You can also use it with explicit exception types as the first argument:
1540+
1541+
.. code:: python
1542+
1543+
>>> from returns.future import future_safe
1544+
>>> from returns.io import IOFailure, IOSuccess
1545+
1546+
>>> @future_safe(exceptions=(ZeroDivisionError,))
1547+
... async def might_raise(arg: int) -> float:
1548+
... return 1 / arg
1549+
1550+
>>> assert anyio.run(might_raise(2).awaitable) == IOSuccess(0.5)
1551+
>>> assert isinstance(
1552+
... anyio.run(might_raise(0).awaitable),
1553+
... IOFailure,
1554+
... )
1555+
1556+
In this case, only exceptions that are explicitly
1557+
listed are going to be caught.
1558+
14941559
Similar to :func:`returns.io.impure_safe` and :func:`returns.result.safe`
14951560
decorators, but works with ``async`` functions.
14961561
14971562
"""
1498-
async def factory(
1499-
*args: _FuncParams.args,
1500-
**kwargs: _FuncParams.kwargs,
1501-
) -> Result[_ValueType, Exception]:
1502-
try:
1503-
return Success(await function(*args, **kwargs))
1504-
except Exception as exc:
1505-
return Failure(exc)
1506-
1507-
@wraps(function)
1508-
def decorator(*args, **kwargs):
1509-
return FutureResult(factory(*args, **kwargs))
1510-
return decorator
1563+
def _future_safe_factory( # noqa: WPS430
1564+
function: Callable[
1565+
_FuncParams,
1566+
Coroutine[_FirstType, _SecondType, _ValueType],
1567+
],
1568+
inner_exceptions: Tuple[Type[_ExceptionType], ...],
1569+
) -> Callable[_FuncParams, FutureResult[_ValueType, _ExceptionType]]:
1570+
async def factory(
1571+
*args: _FuncParams.args,
1572+
**kwargs: _FuncParams.kwargs,
1573+
) -> Result[_ValueType, _ExceptionType]:
1574+
try:
1575+
return Success(await function(*args, **kwargs))
1576+
except inner_exceptions as exc:
1577+
return Failure(exc)
1578+
1579+
@wraps(function)
1580+
def decorator(
1581+
*args: _FuncParams.args,
1582+
**kwargs: _FuncParams.kwargs,
1583+
) -> FutureResult[_ValueType, _ExceptionType]:
1584+
return FutureResult(factory(*args, **kwargs))
1585+
return decorator
1586+
if isinstance(exceptions, tuple):
1587+
return lambda function: _future_safe_factory(function, exceptions)
1588+
return _future_safe_factory(
1589+
exceptions,
1590+
(Exception,), # type: ignore[arg-type]
1591+
)

tests/test_examples/test_context/test_reader_future_result.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from returns.context import RequiresContextFutureResultE
88
from returns.functions import tap
9-
from returns.future import FutureResultE, future_safe
9+
from returns.future import FutureResult, future_safe
1010
from returns.iterables import Fold
1111
from returns.pipeline import managed
1212
from returns.result import ResultE, safe
@@ -24,7 +24,7 @@ class _Post(TypedDict):
2424
def _close(
2525
client: httpx.AsyncClient,
2626
raw_value: ResultE[Sequence[str]],
27-
) -> FutureResultE[None]:
27+
) -> FutureResult[None, Exception]:
2828
return future_safe(client.aclose)()
2929

3030

@@ -65,7 +65,7 @@ def factory(post: _Post) -> str:
6565
# because we want to highlight `managed` in this example:
6666
managed_httpx = managed(_show_titles(3), _close)
6767
future_result = managed_httpx(
68-
FutureResultE.from_value(httpx.AsyncClient(timeout=5)),
68+
FutureResult.from_value(httpx.AsyncClient(timeout=5)),
6969
)
7070
print(anyio.run(future_result.awaitable)) # noqa: WPS421
7171
# <IOResult: <Success: (

tests/test_examples/test_future/test_future_result.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import httpx # you would need to `pip install httpx`
55
from typing_extensions import TypedDict
66

7-
from returns.future import FutureResultE, future_safe
7+
from returns.future import FutureResult, future_safe
88
from returns.io import IOResultE
99
from returns.iterables import Fold
1010

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

2929

30-
def _show_titles(number_of_posts: int) -> Sequence[FutureResultE[str]]:
30+
def _show_titles(number_of_posts: int) -> Sequence[
31+
FutureResult[str, Exception]
32+
]:
3133
def factory(post: _Post) -> str:
3234
return post['title']
3335

tests/test_future/test_future_result/test_future_result_decorator.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Union
2+
13
import pytest
24

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

1113

14+
@future_safe(exceptions=(ZeroDivisionError,))
15+
async def _coro_two(arg: int) -> float:
16+
return 1 / arg
17+
18+
19+
@future_safe((ZeroDivisionError,))
20+
async def _coro_three(arg: Union[int, str]) -> float:
21+
assert isinstance(arg, int)
22+
return 1 / arg
23+
24+
1225
@pytest.mark.anyio
1326
async def test_future_safe_decorator():
1427
"""Ensure that coroutine marked with ``@future_safe``."""
@@ -25,3 +38,22 @@ async def test_future_safe_decorator_failure():
2538

2639
assert isinstance(future_instance, FutureResult)
2740
assert isinstance(await future_instance, IOFailure)
41+
42+
43+
@pytest.mark.anyio
44+
async def test_future_safe_decorator_w_expected_error(subtests):
45+
"""Ensure that coroutine marked with ``@future_safe``."""
46+
expected = '<IOResult: <Failure: division by zero>>'
47+
48+
for future_instance in (_coro_two(0), _coro_three(0)):
49+
with subtests.test(future_instance=future_instance):
50+
assert isinstance(future_instance, FutureResult)
51+
inner_result = await future_instance
52+
assert str(inner_result) == expected
53+
54+
55+
@pytest.mark.anyio
56+
@pytest.mark.xfail(raises=AssertionError)
57+
async def test_future_safe_decorator_w_unexpected_error():
58+
"""Ensure that coroutine marked with ``@future_safe``."""
59+
await _coro_three('0')

typesafety/test_future/test_future_result_container/test_future_safe_decorator.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,33 @@
2525
return 1
2626
2727
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]"
28+
29+
30+
- case: future_safe_decorator_with_pos_params
31+
disable_cache: false
32+
main: |
33+
from typing import Optional
34+
from returns.future import future_safe
35+
36+
@future_safe((ValueError,))
37+
async def test(
38+
first: int, second: Optional[str] = None, *, kw: bool = True,
39+
) -> int:
40+
return 1
41+
42+
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]"
43+
44+
45+
- case: future_safe_decorator_with_named_params
46+
disable_cache: false
47+
main: |
48+
from typing import Optional
49+
from returns.future import future_safe
50+
51+
@future_safe(exceptions=(ValueError,))
52+
async def test(
53+
first: int, second: Optional[str] = None, *, kw: bool = True,
54+
) -> int:
55+
return 1
56+
57+
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]"

0 commit comments

Comments
 (0)