Skip to content

Commit ff86555

Browse files
authored
Merge pull request #74 from python-effect/test-goodies
Test helper functions
2 parents b4fa157 + 34a78c6 commit ff86555

File tree

2 files changed

+169
-2
lines changed

2 files changed

+169
-2
lines changed

effect/test_testing.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@
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,
2728
EQDispatcher,
2829
EQFDispatcher,
2930
SequenceDispatcher,
31+
const,
32+
conste,
3033
fail_effect,
34+
intent_func,
35+
nested_sequence,
3136
parallel_sequence,
3237
perform_sequence,
3338
resolve_effect,
@@ -462,3 +467,61 @@ def test_parallel_sequence_must_be_parallel():
462467
with pytest.raises(FoldError) as excinfo:
463468
perform_sequence(seq, p)
464469
assert excinfo.value.wrapped_exception[0] is AssertionError
470+
471+
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+
503+
def test_const():
504+
"""
505+
:func:`const` takes an argument but returns fixed value
506+
"""
507+
assert const(2)(MyIntent("whatever")) == 2
508+
assert const("text")(OtherIntent("else")) == "text"
509+
510+
511+
def test_conste():
512+
"""
513+
:func:`conste` takes an argument but always raises given exception
514+
"""
515+
func = conste(ValueError("boo"))
516+
with pytest.raises(ValueError):
517+
func(MyIntent("yo"))
518+
519+
520+
def test_intent_func():
521+
"""
522+
:func:`intent_func` returns function that returns Effect of tuple of passed arg
523+
and its args.
524+
"""
525+
func = intent_func("myfunc")
526+
assert func(2, 3) == Effect(("myfunc", 2, 3))
527+
assert func("text", 3, None) == Effect(("myfunc", "text", 3, None))

effect/testing.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,26 @@
88

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

1314
import attr
1415

15-
from ._base import Effect, guard, _Box, NoPerformerFoundError
16+
from ._base import Effect, guard, _Box, NoPerformerFoundError, raise_
1617
from ._sync import NotSynchronousError, sync_perform, sync_performer
1718
from ._intents import Constant, Error, Func, ParallelEffects, base_dispatcher
1819

1920
import 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

Comments
 (0)