Skip to content

Commit bc6e130

Browse files
committed
Add error-handling support to fold_effect
1 parent a935029 commit bc6e130

2 files changed

Lines changed: 57 additions & 6 deletions

File tree

effect/fold.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
1+
import traceback
12
from functools import reduce
23

34
from effect import Constant, Effect
45

56

7+
class FoldError(Exception):
8+
"""
9+
Raised when one of the Effects passed to :func:`fold_effect` fails.
10+
11+
:ivar accumulator: The data accumulated so far, before the failing Effect.
12+
:ivar wrapped_exception: The exc_info tuple representing the original
13+
exception raised by the failing Effect.
14+
"""
15+
def __init__(self, accumulator, wrapped_exception):
16+
self.accumulator = accumulator
17+
self.wrapped_exception = wrapped_exception
18+
19+
def __str__(self):
20+
tb_lines = traceback.format_tb(self.wrapped_exception[2])
21+
tb = ''.join(tb_lines)
22+
return "FoldError(%r, %r) -> ORIGINAL TRACEBACK FOLLOWS\n%s" % (
23+
self.accumulator, self.wrapped_exception, tb)
24+
25+
626
def fold_effect(f, initial, effects):
727
"""
828
Fold over effects.
@@ -18,7 +38,11 @@ def fold_effect(f, initial, effects):
1838
:param effects: sequence of Effects.
1939
"""
2040

41+
def failed(acc, e):
42+
raise FoldError(acc, e)
43+
2144
def folder(acc, element):
22-
return acc.on(lambda r: element.on(lambda r2: f(r, r2)))
45+
return acc.on(lambda r: element.on(lambda r2: f(r, r2),
46+
error=lambda e: failed(r, e)))
2347

2448
return reduce(folder, effects, Effect(Constant(initial)))

effect/test_fold.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11

22
import operator
33

4-
from effect import ComposedDispatcher, Effect, base_dispatcher, sync_perform
5-
from effect.fold import fold_effect
4+
from pytest import raises
5+
6+
from effect import (
7+
ComposedDispatcher, Effect, Error,
8+
base_dispatcher, sync_perform)
9+
from effect.fold import FoldError, fold_effect
610
from effect.testing import SequenceDispatcher
711

812

13+
def _disp(dispatcher):
14+
return ComposedDispatcher([dispatcher, base_dispatcher])
15+
16+
917
def test_fold_effect():
1018
"""Behaves like foldM."""
1119
effs = [Effect('a'), Effect('b'), Effect('c')]
@@ -18,9 +26,7 @@ def test_fold_effect():
1826
eff = fold_effect(operator.add, 'Nil', effs)
1927

2028
with dispatcher.consume():
21-
result = sync_perform(
22-
ComposedDispatcher([dispatcher, base_dispatcher]),
23-
eff)
29+
result = sync_perform(_disp(dispatcher), eff)
2430
assert result == 'NilEiBeeCee'
2531

2632

@@ -31,3 +37,24 @@ def test_fold_effect_empty():
3137
eff = fold_effect(operator.add, 0, [])
3238
result = sync_perform(base_dispatcher, eff)
3339
assert result == 0
40+
41+
42+
def test_fold_effect_errors():
43+
"""
44+
When one of the effects in the folding list fails, a FoldError is raised
45+
with the accumulator so far.
46+
"""
47+
effs = [Effect('a'), Effect(Error(ZeroDivisionError('foo'))), Effect('c')]
48+
49+
dispatcher = SequenceDispatcher([
50+
('a', lambda i: 'Ei'),
51+
])
52+
53+
eff = fold_effect(operator.add, 'Nil', effs)
54+
55+
with dispatcher.consume():
56+
with raises(FoldError) as excinfo:
57+
sync_perform(_disp(dispatcher), eff)
58+
assert excinfo.value.accumulator == 'NilEi'
59+
assert excinfo.value.wrapped_exception[0] is ZeroDivisionError
60+
assert str(excinfo.value.wrapped_exception[1]) == 'foo'

0 commit comments

Comments
 (0)