Skip to content

Commit adea4b5

Browse files
committed
Closes #113, closes #147
1 parent 631ca78 commit adea4b5

File tree

4 files changed

+112
-113
lines changed

4 files changed

+112
-113
lines changed

README.md

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -149,26 +149,22 @@ from returns.result import Result, safe
149149
from returns.pipeline import pipe
150150
from returns.functions import box
151151

152-
class FetchUserProfile(object):
153-
"""Single responsibility callable object that fetches user profile."""
154-
155-
def __call__(self, user_id: int) -> Result['UserProfile', Exception]:
156-
"""Fetches `UserProfile` TypedDict from foreign API."""
157-
return pipe(
158-
user_id,
159-
self._make_request,
160-
box(self._parse_json),
161-
)
162-
163-
@safe
164-
def _make_request(self, user_id: int) -> requests.Response:
165-
response = requests.get('/api/users/{0}'.format(user_id))
166-
response.raise_for_status()
167-
return response
168-
169-
@safe
170-
def _parse_json(self, response: requests.Response) -> 'UserProfile':
171-
return response.json()
152+
def fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:
153+
"""Fetches `UserProfile` TypedDict from foreign API."""
154+
return pipe(
155+
self._make_request,
156+
box(self._parse_json),
157+
)(user_id)
158+
159+
@safe
160+
def _make_request(user_id: int) -> requests.Response:
161+
response = requests.get('/api/users/{0}'.format(user_id))
162+
response.raise_for_status()
163+
return response
164+
165+
@safe
166+
def _parse_json(response: requests.Response) -> 'UserProfile':
167+
return response.json()
172168
```
173169

174170
Now we have a clean and a safe and declarative way
@@ -230,29 +226,25 @@ from returns.result import Result, safe
230226
from returns.pipeline import pipe
231227
from returns.functions import box
232228

233-
class FetchUserProfile(object):
234-
"""Single responsibility callable object that fetches user profile."""
235-
236-
def __call__(self, user_id: int) -> IO[Result['UserProfile', Exception]]:
237-
"""Fetches `UserProfile` TypedDict from foreign API."""
238-
return pipe(
239-
user_id,
240-
self._make_request,
241-
# after box: def (Result) -> Result
242-
# after IO.lift: def (IO[Result]) -> IO[Result]
243-
IO.lift(box(self._parse_json)),
244-
)
245-
246-
@impure
247-
@safe
248-
def _make_request(self, user_id: int) -> requests.Response:
249-
response = requests.get('/api/users/{0}'.format(user_id))
250-
response.raise_for_status()
251-
return response
252-
253-
@safe
254-
def _parse_json(self, response: requests.Response) -> 'UserProfile':
255-
return response.json()
229+
def fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:
230+
"""Fetches `UserProfile` TypedDict from foreign API."""
231+
return pipe(
232+
self._make_request,
233+
# after box: def (Result) -> Result
234+
# after IO.lift: def (IO[Result]) -> IO[Result]
235+
IO.lift(box(self._parse_json)),
236+
)(user_id)
237+
238+
@impure
239+
@safe
240+
def _make_request(user_id: int) -> requests.Response:
241+
response = requests.get('/api/users/{0}'.format(user_id))
242+
response.raise_for_status()
243+
return response
244+
245+
@safe
246+
def _parse_json(response: requests.Response) -> 'UserProfile':
247+
return response.json()
256248
```
257249

258250
Now we have explicit markers where the `IO` did happen

docs/pages/functions.rst

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,27 +81,26 @@ We allow you to do that with ease!
8181
8282
from returns.functions import raise_exception
8383
84-
class CreateAccountAndUser(object):
85-
"""Creates new Account-User pair."""
86-
87-
@pipeline
88-
def __call__(self, username: str) -> ...:
89-
"""Imagine, that you need to reraise ValidationErrors due to API."""
90-
return self._validate_user(
91-
username,
92-
).alt(
93-
# What happens here is interesting, since you do not let your
94-
# unwrap to fail with UnwrapFailedError, but instead
95-
# allows you to reraise a wrapped exception.
96-
# In this case `ValidationError()` will be thrown
97-
# before `UnwrapFailedError`
98-
raise_exception,
99-
)
100-
101-
def _validate_user(
102-
self, username: str,
103-
) -> Result['User', ValidationError]:
104-
...
84+
@pipeline
85+
def create_account_and_user(username: str) -> ...:
86+
"""
87+
Creates new Account-User pair.
88+
89+
Imagine, that you need to reraise ValidationErrors due to existing API.
90+
"""
91+
return _validate_user(
92+
username,
93+
).alt(
94+
# What happens here is interesting, since you do not let your
95+
# unwrap to fail with UnwrapFailedError, but instead
96+
# allows you to reraise a wrapped exception.
97+
# In this case `ValidationError()` will be thrown
98+
# before `UnwrapFailedError`
99+
raise_exception,
100+
)
101+
102+
def _validate_user(username: str) -> Result['User', ValidationError]:
103+
...
105104
106105
Use this with caution. We try to remove exceptions from our code base.
107106
Original proposal is `here <https://github.com/dry-python/returns/issues/56>`_.

docs/pages/pipeline.rst

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -98,29 +98,39 @@ Here's the code to illustrate the task.
9898

9999
.. code:: python
100100
101-
from returns.result import Result, Success, Failure, pipeline
102-
103-
class CreateAccountAndUser(object):
104-
"""Creates new Account-User pair."""
105-
106-
# TODO: we need to create a pipeline of these methods somehow...
107-
108-
# Protected methods
109-
110-
def _validate_user(
111-
self, username: str, email: str,
112-
) -> Result['UserSchema', str]:
113-
"""Returns an UserSchema for valid input, otherwise a Failure."""
114-
115-
def _create_account(
116-
self, user_schema: 'UserSchema',
117-
) -> Result['Account', str]:
118-
"""Creates an Account for valid UserSchema's. Or returns a Failure."""
101+
from returns.result import Result, Success, Failure, safe
102+
from returns.pipeline import pipeline
119103
120-
def _create_user(
121-
self, account: 'Account',
122-
) -> Result['User', str]:
123-
"""Create an User instance. If user already exists returns Failure."""
104+
def create_account_and_user(
105+
username: str,
106+
email: str,
107+
) -> Result['User', str]:
108+
# TODO: we need to create a pipeline of these functions somehow...
109+
110+
# Protected functions:
111+
112+
def _validate_user(
113+
username: str, email: str,
114+
) -> Result['UserSchema', Exception]:
115+
"""Returns an UserSchema for valid input, otherwise a Failure."""
116+
if username and '@' in email:
117+
return Success({'username': username, 'email': email})
118+
return Failure(ValueError('Not valid!'))
119+
120+
def _create_account(
121+
user_schema: 'UserSchema',
122+
) -> Result['Account', Exception]:
123+
"""Creates an Account for valid UserSchema's. Or returns a Failure."""
124+
return safe(Accounts.save)(user_schema)
125+
126+
def _create_user(
127+
account: 'Account',
128+
) -> Result['User', Exception]:
129+
"""Create an User instance. If user already exists returns Failure."""
130+
return safe(User.objects.create)(
131+
username=account.username,
132+
account=account,
133+
)
124134
125135
Using bind technique
126136
~~~~~~~~~~~~~~~~~~~~
@@ -129,19 +139,19 @@ We can implement this feature using a traditional ``bind`` method.
129139

130140
.. code:: python
131141
132-
class CreateAccountAndUser(object):
133-
"""Creates new Account-User pair."""
134-
135-
def __call__(self, username: str, email: str) -> Result['User', str]:
136-
"""Can return a Success(user) or Failure(str_reason)."""
137-
return self._validate_user(username, email).bind(
138-
self._create_account,
139-
).bind(
140-
self._create_user,
141-
)
142+
def create_account_and_user(
143+
username: str,
144+
email: str,
145+
) -> Result['User', Exception]:
146+
"""Can return a Success(user) or Failure(exception)."""
147+
return _validate_user(username, email).bind(
148+
_create_account,
149+
).bind(
150+
_create_user,
151+
)
142152
143-
# Protected methods
144-
# ...
153+
# Protected functions:
154+
# ...
145155
146156
And this will work without any problems.
147157
But, is it easy to read a code like this? **No**, it is not.
@@ -158,18 +168,18 @@ Let's see an example.
158168

159169
.. code:: python
160170
161-
class CreateAccountAndUser(object):
162-
"""Creates new Account-User pair."""
163-
164-
@pipeline
165-
def __call__(self, username: str, email: str) -> Result['User', str]:
166-
"""Can return a Success(user) or Failure(str_reason)."""
167-
user_schema = self._validate_user(username, email).unwrap()
168-
account = self._create_account(user_schema).unwrap()
169-
return self._create_user(account)
170-
171-
# Protected methods
172-
# ...
171+
@pipeline
172+
def create_account_and_user(
173+
username: str,
174+
email: str,
175+
) -> Result['User', Exception]:
176+
"""Can return a Success(user) or Failure(exception)."""
177+
user_schema = self._validate_user(username, email).unwrap()
178+
account = self._create_account(user_schema).unwrap()
179+
return self._create_user(account)
180+
181+
# Protected functions:
182+
# ...
173183
174184
Let's see how this new ``.unwrap()`` method works:
175185

returns/generated/pipe.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,4 @@ def _pipe(*functions):
2525
- https://github.com/gcanti/fp-ts/blob/master/src/pipeable.ts
2626
2727
"""
28-
def decorator(initial):
29-
return reduce(compose, functions)(initial)
30-
return decorator
28+
return lambda initial: reduce(compose, functions)(initial)

0 commit comments

Comments
 (0)