Skip to content

Commit 3b29f7a

Browse files
committed
Merge pull request #70 from python-effect/parallel-seq
parallel_sequence helper
2 parents 12ff4dd + 170abaa commit 3b29f7a

File tree

5 files changed

+137
-14
lines changed

5 files changed

+137
-14
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
lint:
2-
flake8 --ignore=E131,E731,W503 --max-line-length=100 effect/
2+
flake8 --ignore=E131,E301,E731,W503,E701,E704 --max-line-length=100 effect/
33

44
build-dist:
55
rm -rf dist

README.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ A very quick example of using Effects:
4545
.. code:: python
4646
4747
from __future__ import print_function
48-
from effect import perform, sync_performer, Effect, TypeDispatcher
48+
from effect import sync_perform, sync_performer, Effect, TypeDispatcher
4949
5050
class ReadLine(object):
5151
def __init__(self, prompt):
@@ -65,14 +65,14 @@ A very quick example of using Effects:
6565
error=lambda e: print("sorry, there was an error. {}".format(e)))
6666
6767
dispatcher = TypeDispatcher({ReadLine: perform_read_line})
68-
perform(dispatcher, effect)
68+
sync_perform(dispatcher, effect)
6969
7070
if __name__ == '__main__':
7171
main()
7272
7373
7474
``Effect`` takes what we call an ``intent``, which is any object. The
75-
``dispatcher`` argument to ``perform`` must have a ``performer`` function
75+
``dispatcher`` argument to ``sync_perform`` must have a ``performer`` function
7676
for your intent.
7777

7878
This has a number of advantages. First, your unit tests for ``get_user_name``

docs/source/intro.rst

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,19 @@ A (sometimes) nicer syntax is provided for adding callbacks, with the
6969
yield Effect(Print("Hello,", name))
7070
7171
Finally, to actually perform these effects, they can be passed to
72-
:func:`effect.perform`, along with a dispatcher which looks up the performer
73-
based on the intent.
72+
:func:`effect.sync_perform`, along with a dispatcher which looks up the
73+
performer based on the intent.
7474

7575
.. code:: python
7676
77+
from effect import sync_perform
78+
7779
def main():
7880
eff = greet()
7981
dispatcher = ComposedDispatcher([
8082
TypeDispatcher({ReadLine: perform_read_line}),
8183
base_dispatcher])
82-
perform(dispatcher, eff)
84+
sync_perform(dispatcher, eff)
8385
8486
This has a number of advantages. First, your unit tests for ``get_user_name``
8587
become simpler. You don't need to mock out or parameterize the ``raw_input``
@@ -115,7 +117,10 @@ A quick tour, with definitions
115117
- Box: An object that has ``succeed`` and ``fail`` methods for providing the
116118
result of an effect (potentially asynchronously). Usually you don't need
117119
to care about this, if you define your performers with
118-
:func:`effect.sync_performer` or :func:`effect.twisted.deferred_performer`.
120+
:func:`effect.sync_performer` or ``txeffect.deferred_performer`` from the
121+
`txeffect`_ package.
122+
123+
.. _`txeffect`: https://pypi.python.org/pypi/txeffect
119124

120125
There's a few main things you need to do to use Effect.
121126

@@ -126,11 +131,12 @@ There's a few main things you need to do to use Effect.
126131
``Effect(HTTPRequest(...))`` and attach callbacks to them with
127132
:func:`Effect.on`.
128133
- As close as possible to the top-level of your application, perform your
129-
effect(s) with :func:`effect.perform`.
130-
- You will need to pass a dispatcher to :func:`effect.perform`. You should create one
131-
by creating a :class:`effect.TypeDispatcher` with your own performers (e.g. for
132-
``HTTPRequest``), and composing it with :obj:`effect.base_dispatcher` (which
133-
has performers for built-in effects) using :class:`effect.ComposedDispatcher`.
134+
effect(s) with :func:`effect.sync_perform`.
135+
- You will need to pass a dispatcher to :func:`effect.sync_perform`. You should
136+
create one by creating a :class:`effect.TypeDispatcher` with your own
137+
performers (e.g. for ``HTTPRequest``), and composing it with
138+
:obj:`effect.base_dispatcher` (which has performers for built-in effects)
139+
using :class:`effect.ComposedDispatcher`.
134140

135141

136142
Callback chains

effect/test_testing.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
raises)
1212

1313
from . import (
14+
ComposedDispatcher,
1415
Constant,
1516
Effect,
1617
base_dispatcher,
1718
parallel,
18-
sync_perform)
19+
sync_perform,
20+
sync_performer)
1921
from .do import do, do_return
22+
from .fold import FoldError, sequence
2023
from .testing import (
2124
ESConstant,
2225
ESError,
@@ -25,6 +28,7 @@
2528
EQFDispatcher,
2629
SequenceDispatcher,
2730
fail_effect,
31+
parallel_sequence,
2832
perform_sequence,
2933
resolve_effect,
3034
resolve_stubs)
@@ -403,3 +407,58 @@ def code_under_test():
403407
expected = ("sequence: MyIntent(val='a')\n"
404408
"NOT FOUND: OtherIntent(val='b')")
405409
assert expected in str(exc.value)
410+
411+
412+
def test_parallel_sequence():
413+
"""
414+
Ensures that all parallel effects are found in the given intents, in
415+
order, and returns the results associated with those intents.
416+
"""
417+
seq = [
418+
parallel_sequence([
419+
[(1, lambda i: "one!")],
420+
[(2, lambda i: "two!")],
421+
[(3, lambda i: "three!")],
422+
])
423+
]
424+
p = parallel([Effect(1), Effect(2), Effect(3)])
425+
assert perform_sequence(seq, p) == ['one!', 'two!', 'three!']
426+
427+
428+
def test_parallel_sequence_fallback():
429+
"""
430+
Accepts a ``fallback`` dispatcher that will be used when the sequence
431+
doesn't contain an intent.
432+
"""
433+
def dispatch_2(intent):
434+
if intent == 2:
435+
return sync_performer(lambda d, i: "two!")
436+
fallback = ComposedDispatcher([dispatch_2, base_dispatcher])
437+
seq = [
438+
parallel_sequence([
439+
[(1, lambda i: 'one!')],
440+
[], # only implicit effects in this slot
441+
[(3, lambda i: 'three!')],
442+
],
443+
fallback_dispatcher=fallback),
444+
]
445+
p = parallel([Effect(1), Effect(2), Effect(3)])
446+
assert perform_sequence(seq, p) == ['one!', 'two!', 'three!']
447+
448+
449+
def test_parallel_sequence_must_be_parallel():
450+
"""
451+
If the sequences aren't run in parallel, the parallel_sequence won't
452+
match and a FoldError of NoPerformerFoundError will be raised.
453+
"""
454+
seq = [
455+
parallel_sequence([
456+
[(1, lambda i: "one!")],
457+
[(2, lambda i: "two!")],
458+
[(3, lambda i: "three!")],
459+
])
460+
]
461+
p = sequence([Effect(1), Effect(2), Effect(3)])
462+
with pytest.raises(FoldError) as excinfo:
463+
perform_sequence(seq, p)
464+
assert excinfo.value.wrapped_exception[0] is AssertionError

effect/testing.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,64 @@ def dispatcher(intent):
107107
return sync_perform(dispatcher, eff)
108108

109109

110+
@object.__new__
111+
class _ANY(object):
112+
def __eq__(self, o): return True
113+
def __ne__(self, o): return False
114+
115+
116+
def parallel_sequence(parallel_seqs, fallback_dispatcher=None):
117+
"""
118+
Convenience for expecting a ParallelEffects in an expected intent sequence,
119+
as required by :func:`perform_sequence` or :obj:`SequenceDispatcher`.
120+
121+
This lets you verify that intents are performed in parallel in the
122+
context of :func:`perform_sequence`. It returns a two-tuple as expected by
123+
that function, so you can use it like this::
124+
125+
@do
126+
def code_under_test():
127+
r = yield Effect(SerialIntent('serial'))
128+
r2 = yield parallel([Effect(MyIntent('a')),
129+
Effect(OtherIntent('b'))])
130+
yield do_return((r, r2))
131+
132+
def test_code():
133+
seq = [
134+
(SerialIntent('serial'), lambda i: 'result1'),
135+
nested_parallel([
136+
[(MyIntent('a'), lambda i: 'a result')],
137+
[(OtherIntent('b'), lambda i: 'b result')]
138+
]),
139+
]
140+
eff = code_under_test()
141+
assert perform_sequence(seq, eff) == ('result1', 'result2')
142+
143+
144+
The argument is expected to be a list of intent sequences, one for each
145+
parallel effect expected. Each sequence will be performed with
146+
:func:`perform_sequence` and the respective effect that's being run in
147+
parallel. The order of the sequences must match that of the order of
148+
parallel effects.
149+
150+
:param parallel_seqs: list of lists of (intent, performer), like
151+
what :func:`perform_sequence` accepts.
152+
:param fallback_dispatcher: an optional dispatcher to compose onto the
153+
sequence dispatcher.
154+
"""
155+
perf = partial(perform_sequence, fallback_dispatcher=fallback_dispatcher)
156+
def performer(intent):
157+
if len(intent.effects) != len(parallel_seqs):
158+
raise AssertionError(
159+
"Need one list in parallel_seqs per parallel effect. "
160+
"Got %s effects and %s seqs.\n"
161+
"Effects: %s\n"
162+
"parallel_seqs: %s" % (len(intent.effects), len(parallel_seqs),
163+
intent.effects, parallel_seqs))
164+
return list(map(perf, parallel_seqs, intent.effects))
165+
return (ParallelEffects(effects=_ANY), performer)
166+
167+
110168
@attr.s
111169
class Stub(object):
112170
"""

0 commit comments

Comments
 (0)