Skip to content

Commit 481bfbb

Browse files
committed
add perform_sequence, a MUCH nicer way to use SequenceDispatcher.
1 parent c39addf commit 481bfbb

File tree

2 files changed

+138
-22
lines changed

2 files changed

+138
-22
lines changed

effect/test_testing.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
Tests for the effect.testing module.
33
"""
44

5+
import attr
6+
7+
import pytest
8+
59
from testtools import TestCase
610
from testtools.matchers import (MatchesListwise, Equals, MatchesException,
711
raises)
@@ -12,6 +16,7 @@
1216
base_dispatcher,
1317
parallel,
1418
sync_perform)
19+
from .do import do, do_return
1520
from .testing import (
1621
ESConstant,
1722
ESError,
@@ -20,6 +25,7 @@
2025
EQFDispatcher,
2126
SequenceDispatcher,
2227
fail_effect,
28+
perform_sequence,
2329
resolve_effect,
2430
resolve_stubs)
2531

@@ -324,7 +330,7 @@ def test_consumed(self):
324330
def test_consumed_honors_changes(self):
325331
"""
326332
`consumed` returns True if there are no more elements after performing
327-
some..
333+
some.
328334
"""
329335
d = SequenceDispatcher([('foo', lambda i: 'bar')])
330336
sync_perform(d, Effect('foo'))
@@ -352,3 +358,46 @@ def failer():
352358
pass
353359
e = self.assertRaises(AssertionError, failer)
354360
self.assertEqual(str(e), "Not all intents were performed: ['foo']")
361+
362+
363+
@attr.s
364+
class MyIntent(object):
365+
val = attr.ib()
366+
367+
@attr.s
368+
class OtherIntent(object):
369+
val = attr.ib()
370+
371+
def test_perform_sequence():
372+
"""perform_sequence pretty much acts like SequenceDispatcher by default."""
373+
374+
@do
375+
def code_under_test():
376+
r = yield Effect(MyIntent('a'))
377+
r2 = yield Effect(OtherIntent('b'))
378+
yield do_return((r, r2))
379+
380+
seq = [(MyIntent('a'), lambda i: 'result1'),
381+
(OtherIntent('b'), lambda i: 'result2')]
382+
eff = code_under_test()
383+
assert perform_sequence(seq, eff) == ('result1', 'result2')
384+
385+
386+
def test_perform_sequence_log():
387+
"""
388+
When an intent isn't found, a useful log of intents is included in the
389+
exception message.
390+
"""
391+
@do
392+
def code_under_test():
393+
r = yield Effect(MyIntent('a'))
394+
r2 = yield Effect(OtherIntent('b'))
395+
yield do_return((r, r2))
396+
397+
seq = [(MyIntent('a'), lambda i: 'result1')]
398+
with pytest.raises(AssertionError) as exc:
399+
perform_sequence(seq, code_under_test())
400+
401+
expected = ("sequence: MyIntent(val='a')\n"
402+
"NOT FOUND: OtherIntent(val='b')")
403+
assert expected in str(exc.value)

effect/testing.py

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""
22
Various functions and dispatchers for testing effects.
33
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`.
65
"""
76

87
from __future__ import print_function
@@ -14,12 +13,13 @@
1413
import attr
1514

1615
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
1918

2019
import six
2120

2221
__all__ = [
22+
'perform_sequence',
2323
'SequenceDispatcher',
2424
'EQDispatcher',
2525
'EQFDispatcher',
@@ -32,10 +32,86 @@
3232
]
3333

3434

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+
35111
@attr.s
36112
class Stub(object):
37113
"""
38-
DEPRECATED in favor of using :obj:`SequenceDispatcher`.
114+
DEPRECATED in favor of using :func:`perform_sequence`.
39115
40116
41117
An intent which wraps another intent, to flag that the intent should
@@ -67,7 +143,7 @@ def resolve_effect(effect, result, is_error=False):
67143
Supply a result for an effect, allowing its callbacks to run.
68144
69145
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.
71147
72148
The return value of the last callback is returned, unless any callback
73149
returns another Effect, in which case an Effect representing that
@@ -128,7 +204,7 @@ def fail_effect(effect, exception):
128204

129205
def resolve_stub(dispatcher, effect):
130206
"""
131-
DEPRECATED in favor of obj:`SequenceDispatcher`.
207+
DEPRECATED in favor of :func:`perform_sequence`.
132208
133209
Automatically perform an effect, if its intent is a :obj:`Stub`.
134210
@@ -161,7 +237,7 @@ def resolve_stub(dispatcher, effect):
161237

162238
def resolve_stubs(dispatcher, effect):
163239
"""
164-
DEPRECATED in favor of obj:`SequenceDispatcher`.
240+
DEPRECATED in favor of using :func:`perform_sequence`.
165241
166242
Successively performs effects with resolve_stub until a non-Effect value,
167243
or an Effect with a non-stub intent is returned, and return that value.
@@ -198,7 +274,7 @@ class EQDispatcher(object):
198274
This dispatcher looks up intents by equality and performs them by returning
199275
an associated constant value.
200276
201-
This is sometimes useful, but :obj:`SequenceDispatcher` should be
277+
This is sometimes useful, but :func:`perform_sequence` should be
202278
preferred, since it constrains the order of effects, which is usually
203279
important.
204280
@@ -239,7 +315,7 @@ class EQFDispatcher(object):
239315
This dispatcher looks up intents by equality and performs them by invoking
240316
an associated function.
241317
242-
This is sometimes useful, but :obj:`SequenceDispatcher` should be
318+
This is sometimes useful, but :func:`perform_sequence` should be
243319
preferred, since it constrains the order of effects, which is usually
244320
important.
245321
@@ -281,17 +357,8 @@ class SequenceDispatcher(object):
281357
A dispatcher which steps through a sequence of (intent, func) tuples and
282358
runs ``func`` to perform intents in strict sequence.
283359
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.
295362
296363
It's important to use `with sequence.consume():` to ensure that all of the
297364
intents are performed. Otherwise, if your code has a bug that causes it to

0 commit comments

Comments
 (0)