Skip to content

Add equality-based dispatchers for testing #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion effect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from __future__ import absolute_import

from ._base import Effect, perform, NoPerformerFoundError
from ._base import Effect, perform, NoPerformerFoundError, catch
from ._sync import (
NotSynchronousError,
sync_perform,
Expand All @@ -28,4 +28,5 @@
"Constant", "Error", "FirstError", "Func",
"base_dispatcher",
"TypeDispatcher", "ComposedDispatcher",
"catch",
]
19 changes: 19 additions & 0 deletions effect/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from characteristic import attributes

import six

from ._continuation import trampoline


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

trampoline(_perform, effect)


def catch(exc_type, callable):
"""
A helper for handling errors of a specific type.

eff.on(error=catch(SpecificException,
lambda exc_info: "got an error!"))

If any exception other than a ``SpecificException`` is thrown, it will be
ignored by this handler and propogate further down the chain of callbacks.
"""
def catcher(exc_info):
if isinstance(exc_info[1], exc_type):
return callable(exc_info)
six.reraise(*exc_info)
return catcher
34 changes: 33 additions & 1 deletion effect/test_base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import print_function, absolute_import

import sys
import traceback

from testtools import TestCase
from testtools.matchers import MatchesException, MatchesListwise

from ._base import Effect, NoPerformerFoundError, perform
from ._base import Effect, NoPerformerFoundError, catch, perform
from ._test_utils import raise_


Expand Down Expand Up @@ -288,3 +289,34 @@ def get_stack(_):
perform(func_dispatcher, eff)
boxes[0].succeed('foo')
self.assertEqual(calls[0], calls[1])


class CatchTests(TestCase):
"""Tests for :func:`catch`."""

def test_caught(self):
"""
When the exception type matches the type in the ``exc_info`` tuple, the
callable is invoked and its result is returned.
"""
try:
raise RuntimeError('foo')
except:
exc_info = sys.exc_info()
result = catch(RuntimeError, lambda e: ('caught', e))(exc_info)
self.assertEqual(result, ('caught', exc_info))

def test_missed(self):
"""
When the exception type does not match the type in the ``exc_info``
tuple, the callable is not invoked and the original exception is
reraised.
"""
try:
raise ZeroDivisionError('foo')
except:
exc_info = sys.exc_info()
e = self.assertRaises(
ZeroDivisionError,
lambda: catch(RuntimeError, lambda e: ('caught', e))(exc_info))
self.assertEqual(str(e), 'foo')
33 changes: 32 additions & 1 deletion effect/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
Constant,
Effect,
base_dispatcher,
parallel)
parallel,
sync_perform)
from .testing import (
ESConstant,
ESError,
ESFunc,
EQDispatcher,
EQFDispatcher,
fail_effect,
resolve_effect,
resolve_stubs)
Expand Down Expand Up @@ -245,3 +248,31 @@ def test_parallel_stubs_with_element_callbacks_returning_non_stubs(self):

def _raise(e):
raise e


class EQDispatcherTests(TestCase):
"""Tests for :obj:`EQDispatcher`."""

def test_no_intent(self):
"""When the dispatcher can't match the intent, it returns None."""
d = EQDispatcher({})
self.assertIs(d('foo'), None)

def test_perform(self):
"""When an intent matches, performing it returns the canned result."""
d = EQDispatcher({'hello': 'there'})
self.assertEqual(sync_perform(d, Effect('hello')), 'there')


class EQFDispatcherTests(TestCase):
"""Tests for :obj:`EQFDispatcher`."""

def test_no_intent(self):
"""When the dispatcher can't match the intent, it returns None."""
d = EQFDispatcher({})
self.assertIs(d('foo'), None)

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'))
69 changes: 68 additions & 1 deletion effect/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from characteristic import attributes

from ._base import Effect, guard, _Box, NoPerformerFoundError
from ._sync import NotSynchronousError
from ._sync import NotSynchronousError, sync_performer
from ._intents import Constant, Error, Func, ParallelEffects

import six
Expand Down Expand Up @@ -164,3 +164,70 @@ def resolve_stubs(dispatcher, effect):
break

return effect


class EQDispatcher(object):
"""
An equality-based (constant) dispatcher.

This dispatcher looks up intents by equality and performs them by returning
an associated constant value.

Users provide a mapping of intents to results, where the intents are
matched against the intents being performed with a simple equality check
(not a type check!).

e.g.::

>>> sync_perform(EQDispatcher({MyIntent(1, 2): 'the-result'}),
... Effect(MyIntent(1, 2)))
'the-result'

assuming MyIntent supports ``__eq__`` by value.
"""
def __init__(self, mapping):
"""
:param mapping: Mapping of intents to results.
"""
self.mapping = mapping

def __call__(self, intent):
# Avoid hashing, because a lot of intents aren't hashable.
for k, v in self.mapping.items():
if k == intent:
return sync_performer(lambda d, i: v)


class EQFDispatcher(object):
"""
An Equality-based function dispatcher.

This dispatcher looks up intents by equality and performs them by invoking
an associated function.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to document that the function needs to take a single argument: the intent


Users provide a mapping of intents to functions, where the intents are
matched against the intents being performed with a simple equality check
(not a type check!). The functions in the mapping will be passed only the
intent and are expected to return the result or raise an exception.

e.g.::

>>> sync_perform(
... EQFDispatcher({
... MyIntent(1, 2): lambda i: 'the-result'}),
... Effect(MyIntent(1, 2)))
'the-result'

assuming MyIntent supports ``__eq__`` by value.
"""
def __init__(self, mapping):
"""
:param mapping: Mapping of intents to results.
"""
self.mapping = mapping

def __call__(self, intent):
# Avoid hashing, because a lot of intents aren't hashable.
for k, v in self.mapping.items():
if k == intent:
return sync_performer(lambda d, i: v(i))