Skip to content

Commit b86d930

Browse files
committed
Adds .bind_future_result and .bind_future to ReaderFutureResult, refs #274
1 parent a37a047 commit b86d930

File tree

12 files changed

+294
-7
lines changed

12 files changed

+294
-7
lines changed

docs/pages/context.rst

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,78 @@ Use it when you work with impure context-related functions that might fail.
302302
This is basically **the main type** that is going to be used in most apps.
303303

304304

305+
.. _requires_context_future_result:
306+
305307
RequiresContextFutureResult container
306308
-------------------------------------
307309

308310
This container is a combintaion of ``RequiresContext[env, FutureResult[a, b]]``.
309311
Which means that it is a wrapper around impure async function that might fail.
310312

313+
Here's how it should be used:
314+
315+
.. code:: python
316+
317+
from typing import Sequence
318+
319+
import anyio # you wound need to `pip install anyio`
320+
import httpx # you wound need to `pip install httpx`
321+
from typing_extensions import Final, TypedDict
322+
323+
from returns.context import ContextFutureResult, RequiresContextFutureResultE
324+
from returns.functions import tap
325+
from returns.future import FutureResultE, future_safe
326+
from returns.pipeline import managed
327+
328+
_URL: Final = 'https://jsonplaceholder.typicode.com/posts/{0}'
329+
_Post = TypedDict('_Post', {
330+
'id': int,
331+
'userId': int,
332+
'title': str,
333+
'body': str,
334+
})
335+
_TitleOnly = TypedDict('_TitleOnly', {'title': str})
336+
337+
def _close(client: httpx.AsyncClient, _) -> FutureResultE[None]:
338+
return future_safe(client.aclose)()
339+
340+
def _fetch_post(
341+
post_id: int,
342+
) -> RequiresContextFutureResultE[httpx.AsyncClient, _Post]:
343+
return ContextFutureResult[httpx.AsyncClient].ask().bind_future_result(
344+
lambda client: future_safe(client.get)(_URL.format(post_id)),
345+
).map(
346+
lambda response: tap(httpx.Response.raise_for_status)(response),
347+
).map(
348+
lambda response: response.json(),
349+
)
350+
351+
def show_titles(
352+
number_of_posts: int,
353+
) -> RequiresContextFutureResultE[httpx.AsyncClient, Sequence[_TitleOnly]]:
354+
return RequiresContextFutureResultE.from_iterable([
355+
# Notice how easily we compose async and sync functions:
356+
_fetch_post(post_id).map(lambda post: post['title'])
357+
# TODO: try `for post_id in {2, 1, 0}:` to see how errors work
358+
for post_id in range(1, number_of_posts + 1)
359+
])
360+
361+
if __name__ == '__main__':
362+
# Let's fetch 3 titles of posts asynchronously:
363+
managed_httpx = managed(show_titles(3), _close)
364+
future_result = managed_httpx(
365+
FutureResultE.from_value(httpx.AsyncClient(timeout=5)),
366+
)
367+
print(anyio.run(future_result.awaitable))
368+
# <IOResult: <Success: (
369+
# 'sunt aut facere repellat provident occaecati ...',
370+
# 'qui est esse',
371+
# 'ea molestias quasi exercitationem repellat qui ipsa sit aut',
372+
# )>>
373+
374+
This example illustrates the whole point of our actions: writting
375+
sync code that executes asynchronously without any magic at all!
376+
311377
We also added a lot of useful methods for this container,
312378
so you can work easily with it.
313379

@@ -324,6 +390,8 @@ These methods are identical with ``RequiresContextIOResult``:
324390
allows to bind functions that return ``IO`` with just one call
325391
- :meth:`~RequiresContextFutureResult.bind_ioresult`
326392
allows to bind functions that return ``IOResult`` with just one call
393+
- :meth:`~RequiresContextFutureResult.bind_future_result`
394+
allows to bind functions that return ``FutureResult`` with just one call
327395
- :meth:`~RequiresContextFutureResult.bind_context`
328396
allows to bind functions that return ``RequiresContext`` easily
329397
- :meth:`~RequiresContextFutureResult.bind_context_result`

docs/pages/future.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ Let's see how it can be used in a real program:
8585
8686
from typing import Sequence
8787
88-
import anyio
89-
import httpx
88+
import anyio # you would need to `pip install anyio`
89+
import httpx # you would need to `pip install httpx`
9090
from typing_extensions import Final, TypedDict
9191
from returns.future import FutureResultE, future_safe
9292
@@ -134,6 +134,11 @@ What is different?
134134
In this example any error will cancel the whole pipeline
135135
3. We now have ``.map`` method to easily compose sync and async functions
136136

137+
You can see the next example
138+
with :ref:`RequiresContextFutureResult <requires_context_future_result>`
139+
and without a single ``async/await``.
140+
That example illustrates the whole point of our actions: writting
141+
sync code that executes asynchronously without any magic at all.
137142

138143
Aliases
139144
-------

returns/_generated/pointfree/bind_future.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ def _bind_future(function):
2626
... container = FutureResult.from_failure(1)
2727
... return await bind_future(example)(container)
2828
29-
3029
>>> assert anyio.run(success) == IOSuccess(0.5)
3130
>>> assert anyio.run(failure) == IOFailure(1)
3231

returns/_generated/pointfree/bind_future.pyi

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
from typing import Callable, TypeVar
1+
from typing import Callable, TypeVar, overload
22

33
from typing_extensions import Protocol
44

55
from returns.future import Future, FutureResult
6+
from returns.context import RequiresContextFutureResult
67

78
_ValueType = TypeVar('_ValueType', contravariant=True)
89
_ErrorType = TypeVar('_ErrorType')
910
_NewValueType = TypeVar('_NewValueType', covariant=True)
11+
_EnvType = TypeVar('_EnvType')
1012

1113

1214
class _BindFuture(Protocol[_ValueType, _NewValueType]):
@@ -19,12 +21,22 @@ class _BindFuture(Protocol[_ValueType, _NewValueType]):
1921
It is also completely removed from typing with the help of the mypy plugin.
2022
"""
2123

24+
@overload
2225
def __call__(
2326
self,
2427
container: FutureResult[_ValueType, _ErrorType],
2528
) -> FutureResult[_NewValueType, _ErrorType]:
2629
...
2730

31+
@overload
32+
def __call__(
33+
self,
34+
container: RequiresContextFutureResult[
35+
_EnvType, _ValueType, _ErrorType,
36+
],
37+
) -> RequiresContextFutureResult[_EnvType, _NewValueType, _ErrorType]:
38+
...
39+
2840

2941
def _bind_future(
3042
function: Callable[[_ValueType], Future[_NewValueType]],
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
def _bind_future_result(function):
2+
"""
3+
Lifts ``FutureResult`` function to be wrapped in other container.
4+
5+
In other words, it modifies the function's
6+
signature from: ``a -> FutureResult[b, c]``
7+
to: ``Container[a, c] -> Container[b, c]``
8+
9+
This is how it should be used:
10+
11+
.. code:: python
12+
13+
>>> import anyio
14+
>>> from returns.future import FutureResultE
15+
>>> from returns.context import ReaderFutureResultE
16+
>>> from returns.io import IOSuccess, IOFailure
17+
>>> from returns.pointfree import bind_future_result
18+
19+
>>> def example(argument: int) -> FutureResultE[float]:
20+
... return FutureResultE.from_value(argument / 2)
21+
22+
>>> assert anyio.run(bind_future_result(example)(
23+
... ReaderFutureResultE.from_value(1),
24+
... ), ReaderFutureResultE.empty) == IOSuccess(0.5)
25+
26+
>>> assert anyio.run(bind_future_result(example)(
27+
... ReaderFutureResultE.from_failure(':('),
28+
... ), ReaderFutureResultE.empty) == IOFailure(':(')
29+
30+
"""
31+
return lambda container: container.bind_future_result(function)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing import Callable, TypeVar
2+
3+
from typing_extensions import Protocol
4+
5+
from returns.context import RequiresContextFutureResult
6+
from returns.future import FutureResult
7+
8+
_ValueType = TypeVar('_ValueType', contravariant=True)
9+
_ErrorType = TypeVar('_ErrorType')
10+
_NewValueType = TypeVar('_NewValueType', covariant=True)
11+
_EnvType = TypeVar('_EnvType')
12+
13+
14+
class _BindFutureResult(Protocol[_ValueType, _NewValueType, _ErrorType]):
15+
"""
16+
Helper class to represent type overloads for ret_type based on a value type.
17+
18+
Contains all containers we have.
19+
20+
It does not exist in runtime.
21+
It is also completely removed from typing with the help of the mypy plugin.
22+
"""
23+
24+
def __call__(
25+
self,
26+
container: RequiresContextFutureResult[
27+
_EnvType, _ValueType, _ErrorType,
28+
],
29+
) -> RequiresContextFutureResult[_EnvType, _NewValueType, _ErrorType]:
30+
...
31+
32+
33+
def _bind_future_result(
34+
function: Callable[[_ValueType], FutureResult[_NewValueType, _ErrorType]],
35+
) -> _BindFutureResult[_ValueType, _NewValueType, _ErrorType]:
36+
...

returns/context/requires_context_future_result.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,79 @@ def bind_ioresult(
449449
lambda deps: self(deps).bind_ioresult(function),
450450
)
451451

452+
def bind_future(
453+
self,
454+
function: Callable[[_ValueType], Future[_NewValueType]],
455+
) -> 'RequiresContextFutureResult[_EnvType, _NewValueType, _ErrorType]':
456+
"""
457+
Binds ``Future`` returning function to the current container.
458+
459+
.. code:: python
460+
461+
>>> import anyio
462+
>>> from returns.context import RequiresContextFutureResult
463+
>>> from returns.future import Future
464+
>>> from returns.io import IOSuccess, IOFailure
465+
466+
>>> def function(num: int) -> Future[int]:
467+
... return Future.from_value(num + 1)
468+
469+
>>> assert anyio.run(
470+
... RequiresContextFutureResult.from_value(1).bind_future(
471+
... function,
472+
... ),
473+
... RequiresContextFutureResult.empty,
474+
... ) == IOSuccess(2)
475+
476+
>>> failed = RequiresContextFutureResult.from_failure(':(')
477+
>>> assert anyio.run(
478+
... failed.bind_future_result(function),
479+
... RequiresContextFutureResult.empty,
480+
... ) == IOFailure(':(')
481+
482+
"""
483+
return RequiresContextFutureResult(
484+
lambda deps: self(deps).bind_future(function),
485+
)
486+
487+
def bind_future_result(
488+
self,
489+
function: Callable[
490+
[_ValueType],
491+
FutureResult[_NewValueType, _ErrorType],
492+
],
493+
) -> 'RequiresContextFutureResult[_EnvType, _NewValueType, _ErrorType]':
494+
"""
495+
Binds ``FutureResult`` returning function to the current container.
496+
497+
.. code:: python
498+
499+
>>> import anyio
500+
>>> from returns.context import RequiresContextFutureResult
501+
>>> from returns.future import FutureResult
502+
>>> from returns.io import IOSuccess, IOFailure
503+
504+
>>> def function(num: int) -> FutureResult[int, str]:
505+
... return FutureResult.from_value(num + 1)
506+
507+
>>> assert anyio.run(
508+
... RequiresContextFutureResult.from_value(1).bind_future_result(
509+
... function,
510+
... ),
511+
... RequiresContextFutureResult.empty,
512+
... ) == IOSuccess(2)
513+
514+
>>> failed = RequiresContextFutureResult.from_failure(':(')
515+
>>> assert anyio.run(
516+
... failed.bind_future_result(function),
517+
... RequiresContextFutureResult.empty,
518+
... ) == IOFailure(':(')
519+
520+
"""
521+
return RequiresContextFutureResult(
522+
lambda deps: self(deps).bind(function),
523+
)
524+
452525
def fix(
453526
self, function: Callable[[_ErrorType], _NewValueType],
454527
) -> 'RequiresContextFutureResult[_EnvType, _NewValueType, _ErrorType]':
@@ -781,14 +854,14 @@ def from_failed_future(
781854
>>> import anyio
782855
>>> from returns.context import RequiresContextFutureResult
783856
>>> from returns.future import Future
784-
>>> from returns.io import IOSuccess
857+
>>> from returns.io import IOFailure
785858
786859
>>> assert anyio.run(
787860
... RequiresContextFutureResult.from_failed_future(
788861
... Future.from_value(1),
789862
... ),
790863
... RequiresContextFutureResult.empty,
791-
... ) == IOSuccess(1)
864+
... ) == IOFailure(1)
792865
793866
"""
794867
return RequiresContextFutureResult(

returns/contrib/mypy/returns_plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
'returns._generated.pointfree.bind_io._bind_io',
5454
'returns._generated.pointfree.bind_ioresult._bind_ioresult',
5555
'returns._generated.pointfree.bind_future._bind_future',
56+
'returns._generated.pointfree.bind_future_result._bind_future_result',
5657
'returns._generated.pointfree.bind_context._bind_context',
5758
'returns._generated.pointfree.bind_context_result._bind_context_result',
5859
'returns._generated.pointfree.bind_awaitable._bind_awaitable',

returns/pointfree.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
_bind_context_result as bind_context_result,
1313
)
1414
from returns._generated.pointfree.bind_future import _bind_future as bind_future
15+
from returns._generated.pointfree.bind_future_result import (
16+
_bind_future_result as bind_future_result,
17+
)
1518
from returns._generated.pointfree.bind_io import _bind_io as bind_io
1619
from returns._generated.pointfree.bind_ioresult import (
1720
_bind_ioresult as bind_ioresult,

typesafety/test_context/test_requires_context_future_result/test_requires_context_future_result.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@
7575
reveal_type(x.bind_io(test)) # N: Revealed type is 'returns.context.requires_context_future_result.RequiresContextFutureResult[builtins.str, builtins.bool*, builtins.float]'
7676
7777
78+
- case: requires_context_futureresult_bind_future_result
79+
disable_cache: true
80+
main: |
81+
from returns.context import RequiresContextFutureResult
82+
from returns.future import FutureResult
83+
84+
x: RequiresContextFutureResult[str, int, float]
85+
86+
def test(param: int) -> FutureResult[bool, float]:
87+
...
88+
89+
reveal_type(x.bind_future_result(test)) # N: Revealed type is 'returns.context.requires_context_future_result.RequiresContextFutureResult[builtins.str, builtins.bool*, builtins.float]'
90+
91+
7892
- case: requires_context_future_result_bind_context
7993
disable_cache: true
8094
main: |

0 commit comments

Comments
 (0)