|
1 | 1 | """ |
2 | 2 | Various functions and dispatchers for testing effects. |
3 | 3 |
|
4 | | -Usually the best way to test effects is by using :func:`effect.sync_perform` |
5 | | -with a :obj:`SequenceDispatcher`. |
| 4 | +Usually the best way to test effects is by using :func:`perform_sequence`. |
6 | 5 | """ |
7 | 6 |
|
8 | 7 | from __future__ import print_function |
|
14 | 13 | import attr |
15 | 14 |
|
16 | 15 | from ._base import Effect, guard, _Box, NoPerformerFoundError |
17 | | -from ._sync import NotSynchronousError, sync_performer |
18 | | -from ._intents import Constant, Error, Func, ParallelEffects |
| 16 | +from ._sync import NotSynchronousError, sync_perform, sync_performer |
| 17 | +from ._intents import Constant, Error, Func, ParallelEffects, base_dispatcher |
19 | 18 |
|
20 | 19 | import six |
21 | 20 |
|
22 | 21 | __all__ = [ |
| 22 | + 'perform_sequence', |
23 | 23 | 'SequenceDispatcher', |
24 | 24 | 'EQDispatcher', |
25 | 25 | 'EQFDispatcher', |
|
32 | 32 | ] |
33 | 33 |
|
34 | 34 |
|
| 35 | +def perform_sequence(seq, eff, fallback_dispatcher=None): |
| 36 | + """ |
| 37 | + Perform an Effect by looking up performers for intents in an ordered |
| 38 | + "plan". |
| 39 | +
|
| 40 | + First, an example:: |
| 41 | +
|
| 42 | + @do |
| 43 | + def code_under_test(): |
| 44 | + r = yield Effect(MyIntent('a')) |
| 45 | + r2 = yield Effect(OtherIntent('b')) |
| 46 | + yield do_return((r, r2)) |
| 47 | +
|
| 48 | + def test_code(): |
| 49 | + seq = [ |
| 50 | + (MyIntent('a'), lambda i: 'result1'), |
| 51 | + (OtherIntent('b'), lambda i: 'result2') |
| 52 | + ] |
| 53 | + eff = code_under_test() |
| 54 | + assert perform_sequence(seq, eff) == ('result1', 'result2') |
| 55 | +
|
| 56 | + Every time an intent is to be performed, it is checked against the next |
| 57 | + item in the sequence, and the associated function is used to calculate its |
| 58 | + result. Note that the objects used for intents must provide a meaningful |
| 59 | + ``__eq__`` implementation, since they will be checked for equality. Using |
| 60 | + something like `attrs`_ or `pyrsistent`_'s `PClass`_ is recommended for |
| 61 | + your intents, since they will auto-generate __eq__ and many other methods |
| 62 | + useful for immutable objects. |
| 63 | +
|
| 64 | + .. _`attrs`: https://pypi.python.org/pypi/attrs |
| 65 | + .. _`pyrsistent`: https://pypi.python.org/pypi/pyrsistent |
| 66 | + .. _`PClass`: http://pyrsistent.readthedocs.org/en/latest/api.html#pyrsistent.PClass |
| 67 | +
|
| 68 | + If an intent can't be found in the sequence or the fallback dispatcher, an |
| 69 | + ``AssertionError`` is raised with a log of all intents that were performed |
| 70 | + so far. Each item in the log starts with one of three prefixes: |
| 71 | +
|
| 72 | + - sequence: this intent was found in the sequence |
| 73 | + - fallback: a performer for this intent was provided by the fallback |
| 74 | + dispatcher |
| 75 | + - NOT FOUND: no performer for this intent was found. |
| 76 | +
|
| 77 | + :param list sequence: Sequence of ``(intent, fn)``, where ``fn`` is a |
| 78 | + function that should accept an intent and return a result. |
| 79 | + :param Effect eff: The Effect to perform. |
| 80 | + :param fallback_dispatcher: A dispatcher to use for intents that aren't |
| 81 | + found in the sequence. if None is provided, ``base_dispatcher`` is |
| 82 | + used. |
| 83 | + """ |
| 84 | + def fmt_log(): |
| 85 | + return '{{{\n%s\n}}}' % ( |
| 86 | + '\n'.join(['{}: {}'.format(*x) for x in log]),) |
| 87 | + |
| 88 | + def dispatcher(intent): |
| 89 | + p = sequence(intent) |
| 90 | + if p is not None: |
| 91 | + log.append(("sequence", intent)) |
| 92 | + return p |
| 93 | + p = fallback_dispatcher(intent) |
| 94 | + if p is not None: |
| 95 | + log.append(("fallback", intent)) |
| 96 | + return p |
| 97 | + else: |
| 98 | + log.append(("NOT FOUND", intent)) |
| 99 | + raise AssertionError( |
| 100 | + "Performer not found: {}! Log follows:\n{}".format( |
| 101 | + intent, fmt_log())) |
| 102 | + |
| 103 | + if fallback_dispatcher is None: |
| 104 | + fallback_dispatcher = base_dispatcher |
| 105 | + sequence = SequenceDispatcher(seq) |
| 106 | + log = [] |
| 107 | + with sequence.consume(): |
| 108 | + return sync_perform(dispatcher, eff) |
| 109 | + |
| 110 | + |
35 | 111 | @attr.s |
36 | 112 | class Stub(object): |
37 | 113 | """ |
38 | | - DEPRECATED in favor of using :obj:`SequenceDispatcher`. |
| 114 | + DEPRECATED in favor of using :func:`perform_sequence`. |
39 | 115 |
|
40 | 116 |
|
41 | 117 | An intent which wraps another intent, to flag that the intent should |
@@ -67,7 +143,7 @@ def resolve_effect(effect, result, is_error=False): |
67 | 143 | Supply a result for an effect, allowing its callbacks to run. |
68 | 144 |
|
69 | 145 | Note that is a pretty low-level testing utility; it's much better to use a |
70 | | - higher-level tool like :obj:`SequenceDispatcher` in your tests. |
| 146 | + higher-level tool like :func:`perform_sequence` in your tests. |
71 | 147 |
|
72 | 148 | The return value of the last callback is returned, unless any callback |
73 | 149 | returns another Effect, in which case an Effect representing that |
@@ -128,7 +204,7 @@ def fail_effect(effect, exception): |
128 | 204 |
|
129 | 205 | def resolve_stub(dispatcher, effect): |
130 | 206 | """ |
131 | | - DEPRECATED in favor of obj:`SequenceDispatcher`. |
| 207 | + DEPRECATED in favor of :func:`perform_sequence`. |
132 | 208 |
|
133 | 209 | Automatically perform an effect, if its intent is a :obj:`Stub`. |
134 | 210 |
|
@@ -161,7 +237,7 @@ def resolve_stub(dispatcher, effect): |
161 | 237 |
|
162 | 238 | def resolve_stubs(dispatcher, effect): |
163 | 239 | """ |
164 | | - DEPRECATED in favor of obj:`SequenceDispatcher`. |
| 240 | + DEPRECATED in favor of using :func:`perform_sequence`. |
165 | 241 |
|
166 | 242 | Successively performs effects with resolve_stub until a non-Effect value, |
167 | 243 | or an Effect with a non-stub intent is returned, and return that value. |
@@ -198,7 +274,7 @@ class EQDispatcher(object): |
198 | 274 | This dispatcher looks up intents by equality and performs them by returning |
199 | 275 | an associated constant value. |
200 | 276 |
|
201 | | - This is sometimes useful, but :obj:`SequenceDispatcher` should be |
| 277 | + This is sometimes useful, but :func:`perform_sequence` should be |
202 | 278 | preferred, since it constrains the order of effects, which is usually |
203 | 279 | important. |
204 | 280 |
|
@@ -239,7 +315,7 @@ class EQFDispatcher(object): |
239 | 315 | This dispatcher looks up intents by equality and performs them by invoking |
240 | 316 | an associated function. |
241 | 317 |
|
242 | | - This is sometimes useful, but :obj:`SequenceDispatcher` should be |
| 318 | + This is sometimes useful, but :func:`perform_sequence` should be |
243 | 319 | preferred, since it constrains the order of effects, which is usually |
244 | 320 | important. |
245 | 321 |
|
@@ -281,17 +357,8 @@ class SequenceDispatcher(object): |
281 | 357 | A dispatcher which steps through a sequence of (intent, func) tuples and |
282 | 358 | runs ``func`` to perform intents in strict sequence. |
283 | 359 |
|
284 | | - So, if you expect to first perform an intent like ``MyIntent('a')`` and |
285 | | - then an intent like ``OtherIntent('b')``, you can create and use a |
286 | | - dispatcher like this:: |
287 | | -
|
288 | | - sequence = SequenceDispatcher([ |
289 | | - (MyIntent('a'), lambda i: 'my-intent-result'), |
290 | | - (OtherIntent('b'), lambda i: 'other-intent-result') |
291 | | - ]) |
292 | | -
|
293 | | - with sequence.consume(): |
294 | | - sync_perform(sequence, eff) |
| 360 | + This is the dispatcher used by :func:`perform_sequence`. In general that |
| 361 | + function should be used directly, instead of this dispatcher. |
295 | 362 |
|
296 | 363 | It's important to use `with sequence.consume():` to ensure that all of the |
297 | 364 | intents are performed. Otherwise, if your code has a bug that causes it to |
|
0 commit comments