diff --git a/README.rst b/README.rst index ba7921e..575245b 100644 --- a/README.rst +++ b/README.rst @@ -113,10 +113,12 @@ but now has contributions from the following people: .. _`Christopher Armstrong`: https://github.com/radix +- `cyli`_ - `lvh`_ - `Manish Tomar`_ - `Tom Prince`_ +.. _`cyli`: https://github.com/cyli .. _`lvh`: https://github.com/lvh .. _`Manish Tomar`: https://github.com/manishtomar .. _`Tom Prince`: https://github.com/tomprince diff --git a/effect/test_testing.py b/effect/test_testing.py index a7a878a..d6c01c1 100644 --- a/effect/test_testing.py +++ b/effect/test_testing.py @@ -18,6 +18,7 @@ ESFunc, EQDispatcher, EQFDispatcher, + SequenceDispatcher, fail_effect, resolve_effect, resolve_stubs) @@ -276,3 +277,41 @@ def test_perform(self): """When an intent matches, performing it returns the canned result.""" d = EQFDispatcher([('hello', lambda i: (i, 'there'))]) self.assertEqual(sync_perform(d, Effect('hello')), ('hello', 'there')) + + +class SequenceDispatcherTests(TestCase): + """Tests for :obj:`SequenceDispatcher`.""" + + def test_mismatch(self): + """ + When an intent isn't expected, a None is returned. + """ + d = SequenceDispatcher([('foo', lambda i: 1 / 0)]) + self.assertEqual(d('hello'), None) + + def test_success(self): + """ + Each intent is performed in sequence with the provided functions, as + long as the intents match. + """ + d = SequenceDispatcher([ + ('foo', lambda i: ('performfoo', i)), + ('bar', lambda i: ('performbar', i)), + ]) + eff = Effect('foo').on(lambda r: Effect('bar').on(lambda r2: (r, r2))) + self.assertEqual( + sync_perform(d, eff), + (('performfoo', 'foo'), ('performbar', 'bar'))) + + def test_ran_out(self): + """When there are no more items left, None is returned.""" + d = SequenceDispatcher([]) + self.assertEqual(d('foo'), None) + + def test_out_of_order(self): + """Order of items in the sequence matters.""" + d = SequenceDispatcher([ + ('bar', lambda i: ('performbar', i)), + ('foo', lambda i: ('performfoo', i)), + ]) + self.assertEqual(d('foo'), None) diff --git a/effect/testing.py b/effect/testing.py index f4ab5c6..5cb08d1 100644 --- a/effect/testing.py +++ b/effect/testing.py @@ -243,3 +243,34 @@ def __call__(self, intent): for k, v in self.mapping: if k == intent: return sync_performer(lambda d, i: v(i)) + + +class SequenceDispatcher(object): + """ + A dispatcher which steps through a sequence of (intent, func) tuples and + runs ``func`` to perform intents in strict sequence. + + So, if you expect to first perform an intent like ``MyIntent('a')`` and + then an intent like ``OtherIntent('b')``, you can create a dispatcher like + this:: + + SequenceDispatcher([ + (MyIntent('a'), lambda i: 'my-intent-result'), + (OtherIntent('b'), lambda i: 'other-intent-result') + ]) + + :obj:`None` is returned if the next intent in the sequence is not equal to + the intent being performed, or if there are no more items left in the + sequence. + """ + def __init__(self, sequence): + """:param list sequence: Sequence of (intent, fn).""" + self.sequence = sequence + + def __call__(self, intent): + if len(self.sequence) == 0: + return + exp_intent, func = self.sequence[0] + if intent == exp_intent: + self.sequence = self.sequence[1:] + return sync_performer(lambda d, i: func(i))