Skip to content

Commit

Permalink
Merge pull request #70 from python-effect/parallel-seq
Browse files Browse the repository at this point in the history
parallel_sequence helper
  • Loading branch information
radix committed Aug 25, 2015
2 parents 12ff4dd + 170abaa commit 3b29f7a
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
lint:
flake8 --ignore=E131,E731,W503 --max-line-length=100 effect/
flake8 --ignore=E131,E301,E731,W503,E701,E704 --max-line-length=100 effect/

build-dist:
rm -rf dist
Expand Down
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ A very quick example of using Effects:
.. code:: python
from __future__ import print_function
from effect import perform, sync_performer, Effect, TypeDispatcher
from effect import sync_perform, sync_performer, Effect, TypeDispatcher
class ReadLine(object):
def __init__(self, prompt):
Expand All @@ -65,14 +65,14 @@ A very quick example of using Effects:
error=lambda e: print("sorry, there was an error. {}".format(e)))
dispatcher = TypeDispatcher({ReadLine: perform_read_line})
perform(dispatcher, effect)
sync_perform(dispatcher, effect)
if __name__ == '__main__':
main()
``Effect`` takes what we call an ``intent``, which is any object. The
``dispatcher`` argument to ``perform`` must have a ``performer`` function
``dispatcher`` argument to ``sync_perform`` must have a ``performer`` function
for your intent.

This has a number of advantages. First, your unit tests for ``get_user_name``
Expand Down
24 changes: 15 additions & 9 deletions docs/source/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,19 @@ A (sometimes) nicer syntax is provided for adding callbacks, with the
yield Effect(Print("Hello,", name))
Finally, to actually perform these effects, they can be passed to
:func:`effect.perform`, along with a dispatcher which looks up the performer
based on the intent.
:func:`effect.sync_perform`, along with a dispatcher which looks up the
performer based on the intent.

.. code:: python
from effect import sync_perform
def main():
eff = greet()
dispatcher = ComposedDispatcher([
TypeDispatcher({ReadLine: perform_read_line}),
base_dispatcher])
perform(dispatcher, eff)
sync_perform(dispatcher, eff)
This has a number of advantages. First, your unit tests for ``get_user_name``
become simpler. You don't need to mock out or parameterize the ``raw_input``
Expand Down Expand Up @@ -115,7 +117,10 @@ A quick tour, with definitions
- Box: An object that has ``succeed`` and ``fail`` methods for providing the
result of an effect (potentially asynchronously). Usually you don't need
to care about this, if you define your performers with
:func:`effect.sync_performer` or :func:`effect.twisted.deferred_performer`.
:func:`effect.sync_performer` or ``txeffect.deferred_performer`` from the
`txeffect`_ package.

.. _`txeffect`: https://pypi.python.org/pypi/txeffect

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

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


Callback chains
Expand Down
61 changes: 60 additions & 1 deletion effect/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
raises)

from . import (
ComposedDispatcher,
Constant,
Effect,
base_dispatcher,
parallel,
sync_perform)
sync_perform,
sync_performer)
from .do import do, do_return
from .fold import FoldError, sequence
from .testing import (
ESConstant,
ESError,
Expand All @@ -25,6 +28,7 @@
EQFDispatcher,
SequenceDispatcher,
fail_effect,
parallel_sequence,
perform_sequence,
resolve_effect,
resolve_stubs)
Expand Down Expand Up @@ -403,3 +407,58 @@ def code_under_test():
expected = ("sequence: MyIntent(val='a')\n"
"NOT FOUND: OtherIntent(val='b')")
assert expected in str(exc.value)


def test_parallel_sequence():
"""
Ensures that all parallel effects are found in the given intents, in
order, and returns the results associated with those intents.
"""
seq = [
parallel_sequence([
[(1, lambda i: "one!")],
[(2, lambda i: "two!")],
[(3, lambda i: "three!")],
])
]
p = parallel([Effect(1), Effect(2), Effect(3)])
assert perform_sequence(seq, p) == ['one!', 'two!', 'three!']


def test_parallel_sequence_fallback():
"""
Accepts a ``fallback`` dispatcher that will be used when the sequence
doesn't contain an intent.
"""
def dispatch_2(intent):
if intent == 2:
return sync_performer(lambda d, i: "two!")
fallback = ComposedDispatcher([dispatch_2, base_dispatcher])
seq = [
parallel_sequence([
[(1, lambda i: 'one!')],
[], # only implicit effects in this slot
[(3, lambda i: 'three!')],
],
fallback_dispatcher=fallback),
]
p = parallel([Effect(1), Effect(2), Effect(3)])
assert perform_sequence(seq, p) == ['one!', 'two!', 'three!']


def test_parallel_sequence_must_be_parallel():
"""
If the sequences aren't run in parallel, the parallel_sequence won't
match and a FoldError of NoPerformerFoundError will be raised.
"""
seq = [
parallel_sequence([
[(1, lambda i: "one!")],
[(2, lambda i: "two!")],
[(3, lambda i: "three!")],
])
]
p = sequence([Effect(1), Effect(2), Effect(3)])
with pytest.raises(FoldError) as excinfo:
perform_sequence(seq, p)
assert excinfo.value.wrapped_exception[0] is AssertionError
58 changes: 58 additions & 0 deletions effect/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,64 @@ def dispatcher(intent):
return sync_perform(dispatcher, eff)


@object.__new__
class _ANY(object):
def __eq__(self, o): return True
def __ne__(self, o): return False


def parallel_sequence(parallel_seqs, fallback_dispatcher=None):
"""
Convenience for expecting a ParallelEffects in an expected intent sequence,
as required by :func:`perform_sequence` or :obj:`SequenceDispatcher`.
This lets you verify that intents are performed in parallel in the
context of :func:`perform_sequence`. It returns a two-tuple as expected by
that function, so you can use it like this::
@do
def code_under_test():
r = yield Effect(SerialIntent('serial'))
r2 = yield parallel([Effect(MyIntent('a')),
Effect(OtherIntent('b'))])
yield do_return((r, r2))
def test_code():
seq = [
(SerialIntent('serial'), lambda i: 'result1'),
nested_parallel([
[(MyIntent('a'), lambda i: 'a result')],
[(OtherIntent('b'), lambda i: 'b result')]
]),
]
eff = code_under_test()
assert perform_sequence(seq, eff) == ('result1', 'result2')
The argument is expected to be a list of intent sequences, one for each
parallel effect expected. Each sequence will be performed with
:func:`perform_sequence` and the respective effect that's being run in
parallel. The order of the sequences must match that of the order of
parallel effects.
:param parallel_seqs: list of lists of (intent, performer), like
what :func:`perform_sequence` accepts.
:param fallback_dispatcher: an optional dispatcher to compose onto the
sequence dispatcher.
"""
perf = partial(perform_sequence, fallback_dispatcher=fallback_dispatcher)
def performer(intent):
if len(intent.effects) != len(parallel_seqs):
raise AssertionError(
"Need one list in parallel_seqs per parallel effect. "
"Got %s effects and %s seqs.\n"
"Effects: %s\n"
"parallel_seqs: %s" % (len(intent.effects), len(parallel_seqs),
intent.effects, parallel_seqs))
return list(map(perf, parallel_seqs, intent.effects))
return (ParallelEffects(effects=_ANY), performer)


@attr.s
class Stub(object):
"""
Expand Down

0 comments on commit 3b29f7a

Please sign in to comment.