Skip to content

Commit 8b9fbbd

Browse files
committed
add nested_sequence function
and some doc changes
1 parent 23ea45f commit 8b9fbbd

File tree

2 files changed

+79
-3
lines changed

2 files changed

+79
-3
lines changed

effect/test_testing.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .do import do, do_return
2222
from .fold import FoldError, sequence
2323
from .testing import (
24+
_ANY,
2425
ESConstant,
2526
ESError,
2627
ESFunc,
@@ -31,6 +32,7 @@
3132
conste,
3233
fail_effect,
3334
intent_func,
35+
nested_sequence,
3436
parallel_sequence,
3537
perform_sequence,
3638
resolve_effect,
@@ -467,6 +469,37 @@ def test_parallel_sequence_must_be_parallel():
467469
assert excinfo.value.wrapped_exception[0] is AssertionError
468470

469471

472+
def test_nested_sequence():
473+
"""
474+
:func:`nested_sequence` returns sequence performer function for an intent
475+
that wraps an effect.
476+
"""
477+
478+
@attr.s
479+
class WrappedIntent(object):
480+
effect = attr.ib()
481+
value = attr.ib()
482+
483+
@do
484+
def internal():
485+
yield Effect(1)
486+
yield Effect(2)
487+
yield do_return("wrap")
488+
489+
@do
490+
def code_under_test():
491+
r = yield Effect(WrappedIntent(internal(), "field"))
492+
r2 = yield Effect(MyIntent("a"))
493+
yield do_return((r, r2))
494+
495+
seq = [
496+
(WrappedIntent(_ANY, "field"), nested_sequence([(1, const("r1")), (2, const("r2"))])),
497+
(MyIntent("a"), const("result2"))
498+
]
499+
eff = code_under_test()
500+
assert perform_sequence(seq, eff) == ('wrap', 'result2')
501+
502+
470503
def test_const():
471504
"""
472505
:func:`const` takes an argument but returns fixed value

effect/testing.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from contextlib import contextmanager
1010
from functools import partial
11+
from operator import attrgetter
1112
import sys
1213

1314
import attr
@@ -20,8 +21,13 @@
2021

2122
__all__ = [
2223
'perform_sequence',
24+
'parallel_sequence',
25+
'nested_sequence',
2326
'SequenceDispatcher',
2427
'noop',
28+
'const',
29+
'conste',
30+
'intent_func',
2531
'resolve_effect',
2632
'fail_effect',
2733
'EQDispatcher',
@@ -30,9 +36,6 @@
3036
'ESConstant', 'ESError', 'ESFunc',
3137
'resolve_stubs',
3238
'resolve_stub',
33-
'const',
34-
'conste',
35-
'intent_func',
3639
]
3740

3841

@@ -86,6 +89,7 @@ def test_code():
8689
:param fallback_dispatcher: A dispatcher to use for intents that aren't
8790
found in the sequence. if None is provided, ``base_dispatcher`` is
8891
used.
92+
:return: Result of performed sequence
8993
"""
9094
def fmt_log():
9195
next_item = ''
@@ -162,6 +166,8 @@ def test_code():
162166
what :func:`perform_sequence` accepts.
163167
:param fallback_dispatcher: an optional dispatcher to compose onto the
164168
sequence dispatcher.
169+
:return: (intent, performer) tuple as expected by :func:`perform_sequence`
170+
where intent is ParallelEffects object
165171
"""
166172
perf = partial(perform_sequence, fallback_dispatcher=fallback_dispatcher)
167173

@@ -467,6 +473,43 @@ def consume(self):
467473
[x[0] for x in self.sequence]))
468474

469475

476+
def nested_sequence(seq, get_effect=attrgetter('effect'),
477+
fallback_dispatcher=base_dispatcher):
478+
"""
479+
Return a function of Intent -> a that performs an effect retrieved from the
480+
intent (by accessing its `effect` attribute, by default) with the given
481+
intent-sequence.
482+
483+
A demonstration is best::
484+
485+
SequenceDispatcher([
486+
(BoundFields(effect=mock.ANY, fields={...}),
487+
nested_sequence([(SomeIntent(), perform_some_intent)]))
488+
])
489+
490+
The point is that sometimes you have an intent that wraps another effect,
491+
and you want to ensure that the nested effects follow some sequence in the
492+
context of that wrapper intent.
493+
494+
``get_effect`` defaults to ``attrgetter('effect')``, so you can override it if
495+
your intent stores its nested effect in a different attribute. Or, more
496+
interestingly, if it's something other than a single effect, e.g. for
497+
ParallelEffects see the :func:`parallel_sequence` function.
498+
499+
:param list seq: sequence of intents like :obj:`SequenceDispatcher` takes
500+
:param get_effect: callable to get the inner effect from the wrapper
501+
intent.
502+
:param fallback_dispatcher: an optional dispatcher to compose onto the
503+
sequence dispatcher.
504+
:return: ``callable`` that can be used as performer of a wrapped intent
505+
"""
506+
def performer(intent):
507+
effect = get_effect(intent)
508+
return perform_sequence(seq, effect, fallback_dispatcher=fallback_dispatcher)
509+
510+
return performer
511+
512+
470513
def noop(intent):
471514
"""
472515
Return None. This is just a handy way to make your intent sequences (as

0 commit comments

Comments
 (0)