-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy path_base.py
More file actions
185 lines (140 loc) · 6.07 KB
/
Copy path_base.py
File metadata and controls
185 lines (140 loc) · 6.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# -*- test-case-name: effect.test_base -*-
from functools import partial
import attr
from ._continuation import trampoline
@attr.s
class Effect(object):
"""
Take an object that describes a desired effect (called an "Intent"), and
allow binding callbacks to be called with the result of the effect.
Effects can be performed with :func:`perform`.
:param intent: The intent to be performed.
"""
intent = attr.ib()
callbacks = attr.ib(default=attr.Factory(list))
def on(self, success=None, error=None):
"""
Return a new Effect with the given success and/or error callbacks
bound.
The result of the Effect will be passed to the first callback. Any
callbacks added afterwards will receive the result of the previous
callback. Normal return values are passed on to the next ``success``
callback, and exceptions are passed to the next ``error`` callback.
If a callback returns an :obj:`Effect`, the result of that
:obj:`Effect` will be passed to the next callback.
"""
return Effect(self.intent, callbacks=self.callbacks + [(success, error)])
class _Box(object):
"""
An object into which an effect dispatcher can place a result.
"""
def __init__(self, cont):
"""
:param callable cont: Called with (bool is_error, result)
"""
self._cont = cont
def succeed(self, result):
"""
Indicate that the effect has succeeded, and the result is available.
"""
self._cont((False, result))
def fail(self, result):
"""
Indicate that the effect has failed. result must be an exception.
"""
self._cont((True, result))
def guard(f, *args, **kwargs):
"""
Run a function.
Return (is_error, result), where ``is_error`` is a boolean indicating whether
it raised an exception. In that case, ``result`` will be an exception.
"""
try:
return (False, f(*args, **kwargs))
except Exception as e:
return (True, e)
class NoPerformerFoundError(Exception):
"""Raised when a performer for an intent couldn't be found."""
def perform(dispatcher, effect):
"""
Perform an effect and invoke callbacks bound to it. You probably don't want
to use this. Instead, use :func:`sync_perform` (or, if you're using
Twisted, see the `txeffect`_ library).
The dispatcher will be called with the intent, and is expected to return a
performer (another callable). See :obj:`TypeDispatcher` and
:obj:`ComposedDispatcher` for some implementations of dispatchers, and
:obj:`effect.base_dispatcher` for a dispatcher supporting basic intents
like :obj:`Constant` et al.
The performer will often be decorated with :func:`sync_performer` or the
``deferred_performer`` from `txeffect`_ and will be invoked with the
dispatcher [#dispatcher]_ and the intent, and should perform the desired
effect. [#box]_ The performer should return the result of the effect, or
raise an exception, and the result will be passed on to the first callback,
then the result of the first callback will be passed to the next callback,
and so on.
.. _`txeffect`: https://warehouse.python.org/project/txeffect
Both performers and callbacks may return regular values, raise exceptions,
or return another Effect, which will be recursively performed, such that
the result of the returned Effect becomes the result passed to the next
callback. In the case of exceptions, the next error-callback will be called
with the exception instance.
:returns: None
.. [#dispatcher] The dispatcher is passed because some performers need to
make recursive calls to :func:`perform`, because they need to perform
other effects (see :func:`parallel` and
:func:`.parallel_async.perform_parallel_async` for an example of this).
.. [#box] Without using one of those decorators, the performer is actually
passed three arguments, not two: the dispatcher, the intent, and a
"box". The box is an object that lets the performer provide the result,
optionally asynchronously. To provide the result, use
``box.succeed(result)`` or ``box.fail(exc)``, where ``exc`` is
an exception. Decorators like :func:`sync_performer` simply abstract this away.
"""
def _run_callbacks(bouncer, chain, result):
is_error, value = result
if type(value) is Effect:
bouncer.bounce(
_perform, Effect(value.intent, callbacks=value.callbacks + chain)
)
return
if not chain:
return
cb = chain[0][is_error]
if cb is not None:
result = guard(cb, value)
chain = chain[1:]
bouncer.bounce(_run_callbacks, chain, result)
def _perform(bouncer, effect):
try:
performer = dispatcher(effect.intent)
if performer is None:
raise NoPerformerFoundError(effect.intent)
else:
performer(
dispatcher,
effect.intent,
_Box(partial(bouncer.bounce, _run_callbacks, effect.callbacks)),
)
except Exception as e:
_run_callbacks(bouncer, effect.callbacks, (True, e))
trampoline(_perform, effect)
def catch(exc_type, callable):
"""
A helper for handling errors of a specific type::
eff.on(error=catch(SpecificException,
lambda exc: "got an error!"))
If any exception other than a ``SpecificException`` is thrown, it will be
ignored by this handler and propagate further down the chain of callbacks.
"""
def catcher(error):
if isinstance(error, exc_type):
return callable(error)
raise error
return catcher
def raise_(exception):
"""Simple convenience function to allow raising exceptions as an expression,
useful in lambdas.
:param exception: An exception *instance* (not an exception type).
``raise_(exc)`` is the same as ``raise exc``.
"""
raise exception