Skip to content

Commit c9cbb75

Browse files
committed
Merge branch 'master' into eref
2 parents 5f361cb + a950a44 commit c9cbb75

5 files changed

Lines changed: 154 additions & 4 deletions

File tree

effect/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from __future__ import absolute_import
1111

12-
from ._base import Effect, perform, NoPerformerFoundError
12+
from ._base import Effect, perform, NoPerformerFoundError, catch
1313
from ._sync import (
1414
NotSynchronousError,
1515
sync_perform,
@@ -28,4 +28,5 @@
2828
"Constant", "Error", "FirstError", "Func",
2929
"base_dispatcher",
3030
"TypeDispatcher", "ComposedDispatcher",
31+
"catch",
3132
]

effect/_base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from characteristic import attributes
99

10+
import six
11+
1012
from ._continuation import trampoline
1113

1214

@@ -149,3 +151,20 @@ def _perform(bouncer, effect):
149151
_run_callbacks(bouncer, effect.callbacks, (True, e))
150152

151153
trampoline(_perform, effect)
154+
155+
156+
def catch(exc_type, callable):
157+
"""
158+
A helper for handling errors of a specific type.
159+
160+
eff.on(error=catch(SpecificException,
161+
lambda exc_info: "got an error!"))
162+
163+
If any exception other than a ``SpecificException`` is thrown, it will be
164+
ignored by this handler and propogate further down the chain of callbacks.
165+
"""
166+
def catcher(exc_info):
167+
if isinstance(exc_info[1], exc_type):
168+
return callable(exc_info)
169+
six.reraise(*exc_info)
170+
return catcher

effect/test_base.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from __future__ import print_function, absolute_import
22

3+
import sys
34
import traceback
45

56
from testtools import TestCase
67
from testtools.matchers import MatchesException, MatchesListwise
78

8-
from ._base import Effect, NoPerformerFoundError, perform
9+
from ._base import Effect, NoPerformerFoundError, catch, perform
910
from ._test_utils import raise_
1011

1112

@@ -288,3 +289,34 @@ def get_stack(_):
288289
perform(func_dispatcher, eff)
289290
boxes[0].succeed('foo')
290291
self.assertEqual(calls[0], calls[1])
292+
293+
294+
class CatchTests(TestCase):
295+
"""Tests for :func:`catch`."""
296+
297+
def test_caught(self):
298+
"""
299+
When the exception type matches the type in the ``exc_info`` tuple, the
300+
callable is invoked and its result is returned.
301+
"""
302+
try:
303+
raise RuntimeError('foo')
304+
except:
305+
exc_info = sys.exc_info()
306+
result = catch(RuntimeError, lambda e: ('caught', e))(exc_info)
307+
self.assertEqual(result, ('caught', exc_info))
308+
309+
def test_missed(self):
310+
"""
311+
When the exception type does not match the type in the ``exc_info``
312+
tuple, the callable is not invoked and the original exception is
313+
reraised.
314+
"""
315+
try:
316+
raise ZeroDivisionError('foo')
317+
except:
318+
exc_info = sys.exc_info()
319+
e = self.assertRaises(
320+
ZeroDivisionError,
321+
lambda: catch(RuntimeError, lambda e: ('caught', e))(exc_info))
322+
self.assertEqual(str(e), 'foo')

effect/test_testing.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
Constant,
1111
Effect,
1212
base_dispatcher,
13-
parallel)
13+
parallel,
14+
sync_perform)
1415
from .testing import (
1516
ESConstant,
1617
ESError,
1718
ESFunc,
19+
EQDispatcher,
20+
EQFDispatcher,
1821
fail_effect,
1922
resolve_effect,
2023
resolve_stubs)
@@ -245,3 +248,31 @@ def test_parallel_stubs_with_element_callbacks_returning_non_stubs(self):
245248

246249
def _raise(e):
247250
raise e
251+
252+
253+
class EQDispatcherTests(TestCase):
254+
"""Tests for :obj:`EQDispatcher`."""
255+
256+
def test_no_intent(self):
257+
"""When the dispatcher can't match the intent, it returns None."""
258+
d = EQDispatcher({})
259+
self.assertIs(d('foo'), None)
260+
261+
def test_perform(self):
262+
"""When an intent matches, performing it returns the canned result."""
263+
d = EQDispatcher({'hello': 'there'})
264+
self.assertEqual(sync_perform(d, Effect('hello')), 'there')
265+
266+
267+
class EQFDispatcherTests(TestCase):
268+
"""Tests for :obj:`EQFDispatcher`."""
269+
270+
def test_no_intent(self):
271+
"""When the dispatcher can't match the intent, it returns None."""
272+
d = EQFDispatcher({})
273+
self.assertIs(d('foo'), None)
274+
275+
def test_perform(self):
276+
"""When an intent matches, performing it returns the canned result."""
277+
d = EQFDispatcher({'hello': lambda i: (i, 'there')})
278+
self.assertEqual(sync_perform(d, Effect('hello')), ('hello', 'there'))

effect/testing.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from characteristic import attributes
1515

1616
from ._base import Effect, guard, _Box, NoPerformerFoundError
17-
from ._sync import NotSynchronousError
17+
from ._sync import NotSynchronousError, sync_performer
1818
from ._intents import Constant, Error, Func, ParallelEffects
1919

2020
import six
@@ -164,3 +164,70 @@ def resolve_stubs(dispatcher, effect):
164164
break
165165

166166
return effect
167+
168+
169+
class EQDispatcher(object):
170+
"""
171+
An equality-based (constant) dispatcher.
172+
173+
This dispatcher looks up intents by equality and performs them by returning
174+
an associated constant value.
175+
176+
Users provide a mapping of intents to results, where the intents are
177+
matched against the intents being performed with a simple equality check
178+
(not a type check!).
179+
180+
e.g.::
181+
182+
>>> sync_perform(EQDispatcher({MyIntent(1, 2): 'the-result'}),
183+
... Effect(MyIntent(1, 2)))
184+
'the-result'
185+
186+
assuming MyIntent supports ``__eq__`` by value.
187+
"""
188+
def __init__(self, mapping):
189+
"""
190+
:param mapping: Mapping of intents to results.
191+
"""
192+
self.mapping = mapping
193+
194+
def __call__(self, intent):
195+
# Avoid hashing, because a lot of intents aren't hashable.
196+
for k, v in self.mapping.items():
197+
if k == intent:
198+
return sync_performer(lambda d, i: v)
199+
200+
201+
class EQFDispatcher(object):
202+
"""
203+
An Equality-based function dispatcher.
204+
205+
This dispatcher looks up intents by equality and performs them by invoking
206+
an associated function.
207+
208+
Users provide a mapping of intents to functions, where the intents are
209+
matched against the intents being performed with a simple equality check
210+
(not a type check!). The functions in the mapping will be passed only the
211+
intent and are expected to return the result or raise an exception.
212+
213+
e.g.::
214+
215+
>>> sync_perform(
216+
... EQFDispatcher({
217+
... MyIntent(1, 2): lambda i: 'the-result'}),
218+
... Effect(MyIntent(1, 2)))
219+
'the-result'
220+
221+
assuming MyIntent supports ``__eq__`` by value.
222+
"""
223+
def __init__(self, mapping):
224+
"""
225+
:param mapping: Mapping of intents to results.
226+
"""
227+
self.mapping = mapping
228+
229+
def __call__(self, intent):
230+
# Avoid hashing, because a lot of intents aren't hashable.
231+
for k, v in self.mapping.items():
232+
if k == intent:
233+
return sync_performer(lambda d, i: v(i))

0 commit comments

Comments
 (0)