Skip to content

Commit 7a8feaa

Browse files
committed
Closes #221, closes #206
1 parent 32a5465 commit 7a8feaa

File tree

13 files changed

+211
-59
lines changed

13 files changed

+211
-59
lines changed

.travis.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
language: python
22
dist: xenial
33

4-
matrix:
5-
include:
6-
- python: 3.6
7-
- python: 3.7.3
4+
python:
5+
- 3.6
6+
- 3.7.6
87

98

109
before_install:

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ incremental in minor, bugfixes only are patches.
66
See (0Ver)[https://0ver.org/].
77

88

9-
## 0.13.0
9+
## 0.13.0 WIP
1010

1111
### Features
1212

13-
- Adds `io_squash` to squash several `IO` containers into one container with a tuple inside, currently works with `9` containers max at a time
13+
- **Breaking**: now `@pipeline` requires a container type when created:
14+
`@pipeline(Result)` or `@pipeline(Maybe)`
15+
- Adds `io_squash` to squash several `IO` containers into one container
16+
with a tuple inside, currently works with `9` containers max at a time
17+
- Adds `untap` function which does convert return type to `None`
1418

1519
### Bugfixes
1620

docs/pages/functions.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ For example you sometimes need to ``print()`` values inside your :ref:`pipe`:
8181
assert result == 1
8282
# => True
8383
84+
You can also use ``untap`` function to turn any function
85+
return type to ``None``.
86+
This is also sometimes helpful for a typed function composition.
87+
8488

8589
raise_exception
8690
---------------
@@ -94,7 +98,7 @@ We allow you to do that with ease!
9498
9599
from returns.functions import raise_exception
96100
97-
@pipeline
101+
@pipeline(Result)
98102
def create_account_and_user(username: str) -> ...:
99103
"""
100104
Creates new Account-User pair.

docs/pages/maybe.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _maybe:
2+
13
Maybe
24
=====
35

docs/pages/pipeline.rst

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,18 @@ But, composition with ``pipe`` is limited to two things:
7575
pipeline
7676
--------
7777

78-
What is a ``pipeline``?
78+
What is a ``@pipeline``?
7979
It is a more user-friendly syntax to work with containers
8080
that support both async and regular functions.
8181

82+
``@pipeline`` decorator allows you to ``.unwrap`` container values
83+
from containers and work with them
84+
as with regular values (which they are in this context).
85+
86+
It is something like ``do-notation`` if you wish.
87+
88+
Works with both :ref:`Maybe <maybe>` and :ref:`Result <result>` container.
89+
8290
Consider this task.
8391
We were asked to create a method
8492
that will connect together a simple pipeline of three steps:
@@ -168,7 +176,7 @@ Let's see an example.
168176

169177
.. code:: python
170178
171-
@pipeline
179+
@pipeline(Result)
172180
def create_account_and_user(
173181
username: str,
174182
email: str,
@@ -223,7 +231,7 @@ And the returning value will be different.
223231
from returns.result import Result
224232
from returns.pipeline import pipeline
225233
226-
@pipeline
234+
@pipeline(Result)
227235
def example() -> Result[int, str]:
228236
other: Result[int, Exception]
229237
new_value = other.unwrap() + 1 # hidden boom!

docs/pages/result.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _result:
2+
13
Result
24
======
35

returns/functions.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,35 @@ def decorator(argument_to_return: _FirstType) -> _FirstType:
9393
return decorator
9494

9595

96+
def untap(
97+
function: Callable[[_FirstType], Any],
98+
) -> Callable[[_FirstType], None]:
99+
"""
100+
Allows to apply some function and always return ``None`` as a result.
101+
102+
Is usefull for composing functions that do some side effects
103+
and return some nosense.
104+
105+
Is the kind of a reverse of the ``tap`` function.
106+
107+
.. code:: python
108+
109+
>>> def strange_log(arg: int) -> int:
110+
... print(arg)
111+
... return arg
112+
>>> untap(strange_log)(2)
113+
2
114+
>>> untap(tap(lambda _: 1))(2)
115+
116+
See also:
117+
- https://github.com/dry-python/returns/issues/145
118+
119+
"""
120+
def decorator(argument_to_return: _FirstType) -> None:
121+
function(argument_to_return)
122+
return decorator
123+
124+
96125
def raise_exception(exception: Exception) -> NoReturn:
97126
"""
98127
Helper function to raise exceptions as a function.

returns/pipeline.py

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from functools import wraps
44
from inspect import iscoroutinefunction
5-
from typing import Callable, Coroutine, TypeVar, Union, overload
5+
from typing import Callable, Coroutine, Type, TypeVar, Union, overload
6+
7+
from typing_extensions import Protocol
68

79
from returns._generated.pipe import _pipe as pipe # noqa: F401, WPS436
810
from returns.maybe import Maybe
@@ -17,13 +19,22 @@
1719
_SecondType = TypeVar('_SecondType')
1820

1921
# Hacks for functions:
20-
_ReturnsResultType = TypeVar(
21-
'_ReturnsResultType',
22-
bound=Callable[..., _Unwrapable],
22+
_ReturnResultType = TypeVar(
23+
'_ReturnResultType',
24+
bound=Callable[..., Result],
2325
)
24-
_AsyncReturnsResultType = TypeVar(
25-
'_AsyncReturnsResultType',
26-
bound=Callable[..., Coroutine[_FirstType, _SecondType, _Unwrapable]],
26+
_AsyncReturnResultType = TypeVar(
27+
'_AsyncReturnResultType',
28+
bound=Callable[..., Coroutine[_FirstType, _SecondType, Result]],
29+
)
30+
31+
_ReturnMaybeType = TypeVar(
32+
'_ReturnMaybeType',
33+
bound=Callable[..., Maybe],
34+
)
35+
_AsyncReturnMaybeType = TypeVar(
36+
'_AsyncReturnMaybeType',
37+
bound=Callable[..., Coroutine[_FirstType, _SecondType, Maybe]],
2738
)
2839

2940

@@ -56,36 +67,83 @@ def is_successful(container: _Unwrapable) -> bool:
5667
return True
5768

5869

70+
class _PipelineResultProtocol(Protocol):
71+
@overload
72+
def __call__(self, function: _ReturnResultType) -> _ReturnResultType:
73+
"""Sync pipeline case for ``Result`` container."""
74+
75+
@overload # noqa: F811
76+
def __call__(
77+
self, function: _AsyncReturnResultType,
78+
) -> _AsyncReturnResultType:
79+
"""Async pipeline case for ``Result`` container."""
80+
81+
82+
class _PipelineMaybeProtocol(Protocol):
83+
@overload
84+
def __call__(self, function: _ReturnMaybeType) -> _ReturnMaybeType:
85+
"""Sync pipeline case for ``Maybe`` container."""
86+
87+
@overload # noqa: F811
88+
def __call__(
89+
self, function: _AsyncReturnMaybeType,
90+
) -> _AsyncReturnMaybeType:
91+
"""Async pipeline case for ``Maybe`` container."""
92+
93+
5994
@overload
6095
def pipeline(
61-
function: _AsyncReturnsResultType,
62-
) -> _AsyncReturnsResultType:
63-
"""Case for async functions."""
96+
container_type: Type[Result],
97+
) -> _PipelineResultProtocol:
98+
"""Pipeline case for ``Result`` container."""
6499

65100

66101
@overload
67-
def pipeline(function: _ReturnsResultType) -> _ReturnsResultType:
68-
"""Case for regular functions."""
102+
def pipeline(
103+
container_type: Type[Maybe],
104+
) -> _PipelineMaybeProtocol:
105+
"""Pipeline case for ``Maybe`` container."""
69106

70107

71-
def pipeline(function): # noqa: C901
108+
def pipeline(container_type): # noqa: C901, WPS212
72109
"""
73110
Decorator to enable ``do-notation`` context.
74111
75112
Should be used for series of computations that rely on ``.unwrap`` method.
76-
77113
Supports both async and regular functions.
114+
115+
Works with both ``Maybe`` and ``Result`` containers.
116+
117+
Example:
118+
.. code:: python
119+
120+
>>> from typing import Optional
121+
>>> @pipeline(Maybe)
122+
... def test(one: Optional[int], two: Optional[int]) -> Maybe[int]:
123+
... first = Maybe.new(one).unwrap()
124+
... second = Maybe.new(two).unwrap()
125+
... return Maybe.new(first + second)
126+
...
127+
>>> str(test(1, 2))
128+
'<Some: 3>'
129+
>>> str(test(2, None))
130+
'<Nothing>'
131+
132+
Make sure to supply the correct container type when creating a pipeline.
133+
78134
"""
79-
if iscoroutinefunction(function):
80-
async def decorator(*args, **kwargs): # noqa: WPS430
81-
try:
82-
return await function(*args, **kwargs)
83-
except UnwrapFailedError as exc:
84-
return exc.halted_container
85-
else:
86-
def decorator(*args, **kwargs): # noqa: WPS430
87-
try:
88-
return function(*args, **kwargs)
89-
except UnwrapFailedError as exc:
90-
return exc.halted_container
91-
return wraps(function)(decorator)
135+
def factory(function):
136+
if iscoroutinefunction(function):
137+
async def decorator(*args, **kwargs): # noqa: WPS430
138+
try:
139+
return await function(*args, **kwargs)
140+
except UnwrapFailedError as exc:
141+
return exc.halted_container
142+
else:
143+
def decorator(*args, **kwargs): # noqa: WPS430
144+
try:
145+
return function(*args, **kwargs)
146+
except UnwrapFailedError as exc:
147+
return exc.halted_container
148+
return wraps(function)(decorator)
149+
return factory

tests/test_pipeline/test_maybe_pipeline.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
from returns.pipeline import pipeline
77

88

9-
@pipeline
9+
@pipeline(Maybe)
1010
def _maybe_pipeline(number: int) -> Maybe[int]:
1111
first: int = Some(number).unwrap() if number else Nothing.unwrap()
1212
return Some(first + number)
1313

1414

15-
@pipeline
15+
@pipeline(Maybe)
1616
async def _async_maybe_pipeline(number: int) -> Maybe[int]:
1717
first: int = Some(number).unwrap() if number else Nothing.unwrap()
1818
return Some(first + number)

tests/test_pipeline/test_result_pipeline.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ def _sum(first: int, second: float) -> float:
1616
return first + second
1717

1818

19-
@pipeline
19+
@pipeline(Result)
2020
def _result_pipeline(number: int) -> Result[float, Exception]:
2121
divided = _divide(number).unwrap()
2222
return _sum(number, divided)
2323

2424

25-
@pipeline
25+
@pipeline(Result)
2626
async def _result_async_pipeline(number: int) -> Result[float, Exception]:
2727
divided = _divide(number).unwrap()
2828
return _sum(number, divided)

typesafety/test_functions/test_tap.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@
77
return float(num)
88
99
reveal_type(tap(first)) # N: Revealed type is 'def (builtins.int*) -> builtins.int*'
10+
11+
12+
- case: untap_single_function
13+
disable_cache: true
14+
main: |
15+
from returns.functions import untap
16+
17+
def first(num: int) -> float:
18+
return float(num)
19+
20+
reveal_type(untap(first)) # N: Revealed type is 'def (builtins.int*)'
21+
reveal_type(untap(first)(1)) # N: Revealed type is 'None'
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
- case: maybe_pipeline
2+
disable_cache: true
3+
main: |
4+
from returns.pipeline import pipeline
5+
from returns.maybe import Maybe
6+
7+
@pipeline(Maybe)
8+
def returns_maybe(arg: str) -> Maybe[str]:
9+
...
10+
11+
reveal_type(returns_maybe("a")) # N: Revealed type is 'returns.maybe.Maybe[builtins.str]'
12+
13+
14+
- case: maybe_async_pipeline
15+
disable_cache: true
16+
main: |
17+
from returns.pipeline import pipeline
18+
from returns.maybe import Maybe
19+
20+
@pipeline(Maybe)
21+
async def returns_maybe(arg: str) -> Maybe[str]:
22+
...
23+
24+
reveal_type(returns_maybe("a")) # N: Revealed type is 'typing.Coroutine[Any, Any, returns.maybe.Maybe[builtins.str]]'
25+
26+
27+
- case: maybe_wrong_pipeline
28+
disable_cache: true
29+
main: |
30+
from returns.pipeline import pipeline
31+
from returns.maybe import Maybe
32+
from returns.result import Result
33+
34+
@pipeline(Result)
35+
def returns_maybe(arg: str) -> Maybe[int]:
36+
...
37+
38+
reveal_type(returns_maybe("a"))
39+
40+
out: |
41+
main:5: error: Value of type variable "_ReturnResultType" of "__call__" of "_PipelineResultProtocol" cannot be "Callable[[str], Maybe[int]]"
42+
main:9: note: Revealed type is 'returns.maybe.Maybe[builtins.int]'

0 commit comments

Comments
 (0)