88
99from contextlib import contextmanager
1010from functools import partial
11+ from operator import attrgetter
1112import sys
1213
1314import attr
1415
15- from ._base import Effect , guard , _Box , NoPerformerFoundError
16+ from ._base import Effect , guard , _Box , NoPerformerFoundError , raise_
1617from ._sync import NotSynchronousError , sync_perform , sync_performer
1718from ._intents import Constant , Error , Func , ParallelEffects , base_dispatcher
1819
1920import six
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' ,
@@ -83,6 +89,7 @@ def test_code():
8389 :param fallback_dispatcher: A dispatcher to use for intents that aren't
8490 found in the sequence. if None is provided, ``base_dispatcher`` is
8591 used.
92+ :return: Result of performed sequence
8693 """
8794 def fmt_log ():
8895 next_item = ''
@@ -159,6 +166,8 @@ def test_code():
159166 what :func:`perform_sequence` accepts.
160167 :param fallback_dispatcher: an optional dispatcher to compose onto the
161168 sequence dispatcher.
169+ :return: (intent, performer) tuple as expected by :func:`perform_sequence`
170+ where intent is ParallelEffects object
162171 """
163172 perf = partial (perform_sequence , fallback_dispatcher = fallback_dispatcher )
164173
@@ -464,9 +473,45 @@ def consume(self):
464473 [x [0 ] for x in self .sequence ]))
465474
466475
467- def noop (intent ):
476+ def nested_sequence (seq , get_effect = attrgetter ('effect' ),
477+ fallback_dispatcher = base_dispatcher ):
468478 """
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
469511
512+
513+ def noop (intent ):
514+ """
470515 Return None. This is just a handy way to make your intent sequences (as
471516 used by :func:`perform_sequence`) more concise when the effects you're
472517 expecting in a test don't return a result (and are instead only performed
@@ -479,3 +524,62 @@ def noop(intent):
479524
480525 """
481526 return None
527+
528+
529+ def const (value ):
530+ """
531+ Return function that takes an argument but always return given `value`.
532+ Useful when creating sequence used by :func:`perform_sequence`. For example,
533+
534+ >>> dt = datetime(1970, 1, 1)
535+ >>> seq = [(Func(datetime.now), const(dt))]
536+
537+ :param value: This will be returned when called by returned function
538+ :return: ``callable`` that takes an arg and always returns ``value``
539+ """
540+ return lambda intent : value
541+
542+
543+ def conste (excp ):
544+ """
545+ Like :func:`const` but takes and exception and returns function that raises
546+ the exception
547+
548+ :param excp: Exception that will be raised
549+ :type: :obj:`Exception`
550+ :return: ``callable`` that will raise given exception
551+ """
552+ return lambda intent : raise_ (excp )
553+
554+
555+ def intent_func (fname ):
556+ """
557+ Return function that returns Effect of tuple of fname and its args. Useful
558+ in writing tests that expect intent based on args. For example, if you are
559+ testing following function::
560+
561+ @do
562+ def code_under_test(arg1, arg2, eff_returning_func=eff_returning_func):
563+ r = yield Effect(MyIntent('a'))
564+ r2 = yield eff_returning_func(arg1, arg2)
565+ yield do_return((r, r2))
566+
567+ you will need to know the intents which ``eff_returning_func`` generates
568+ to test this using :func:`perform_sequence`. You can avoid that by doing::
569+
570+ def test_code():
571+ test_eff_func = intent_func("erf")
572+ seq = [
573+ (MyIntent('a'), const('result1')),
574+ (("erf", 'a1', 'a2'), const('result2'))
575+ ]
576+ eff = code_under_test('a1', 'a2', eff_returning_func=test_eff_func)
577+ assert perform_sequence(seq, eff) == ('result1', 'result2')
578+
579+ Here, the ``seq`` ensures that ``eff_returning_func`` is called with arguments
580+ ``a1`` and ``a2``.
581+
582+ :param str fname: First member of intent tuple returned
583+ :return: ``callable`` with multiple positional arguments
584+ """
585+ return lambda * a : Effect ((fname ,) + a )
0 commit comments