diff --git a/encore/concurrent/futures/decorators.py b/encore/concurrent/futures/decorators.py new file mode 100644 index 0000000..ba05671 --- /dev/null +++ b/encore/concurrent/futures/decorators.py @@ -0,0 +1,74 @@ +# +# (C) Copyright 2014 Enthought, Inc., Austin, TX +# All right reserved. +# +# This file is open source software distributed according to the terms in +# LICENSE.txt +# + +from functools import wraps + + +def dispatch(dispatcher=None, call=None): + """ Dispatch method calls using a dispatcher. + + All calls made to the decorated method are submitted to a "dispatcher" (an + executor, work scheduler, or anything else with a "submit" method), or via + some other call. The decorated function does not await any feedback from + the dispatcher. For example, futures returned by an executor are ignored. + The decorated method returns nothing. + + Parameters + ---------- + dispatcher : dispatcher or str, optional + The object used to dispatch calls. A dispatcher is any object with a + "submit" method with the Executor.submit call signature. If this is a + string, it must identify an attribute on the instance to which the + method is bound. + call : callable or str, optional + A callable used to dispatch calls, or a string identifying + a callable on the instance to which the method is bound. The callable + must support the Executor.submit call signature. + + Notes + ----- + Exactly one of ``dispatcher`` or ``call`` must be specified. + + """ + if (dispatcher, call).count(None) != 1: + msg = "Provide exactly one of 'dispatch' or 'call'" + raise ValueError(msg) + + def decorate_with_dispatcher(func): + + lookup_dispatcher = isinstance(dispatcher, basestring) + + @wraps(func) + def wrapper(self, *args, **kw): + + if lookup_dispatcher: + dispatcher_ = getattr(self, dispatcher) + else: + dispatcher_ = dispatcher + + dispatcher_.submit(func, self, *args, **kw) + + return wrapper + + def decorate_with_call(func): + + lookup_call = isinstance(call, basestring) + + @wraps(func) + def wrapper(self, *args, **kw): + + if lookup_call: + call_ = getattr(self, call) + else: + call_ = call + + call_(func, self, *args, **kw) + + return wrapper + + return decorate_with_dispatcher if call is None else decorate_with_call diff --git a/encore/concurrent/futures/tests/test_decorators.py b/encore/concurrent/futures/tests/test_decorators.py new file mode 100644 index 0000000..a101f37 --- /dev/null +++ b/encore/concurrent/futures/tests/test_decorators.py @@ -0,0 +1,95 @@ +# +# (C) Copyright 2014 Enthought, Inc., Austin, TX +# All right reserved. +# +# This file is open source software distributed according to the terms in +# LICENSE.txt +# +import unittest + +from encore.concurrent.futures.decorators import dispatch + + +class TestDispatcher(object): + + def __init__(self): + self.calls = [] + + def submit(self, func, *args, **kw): + # Drop 'self' from args + self.calls.append((args[1:], kw)) + func(*args, **kw) + + def __call__(self, func, *args, **kw): + # Drop 'self' from args + self.calls.append((args[1:], kw)) + func(*args, **kw) + + +TEST_DISPATCHER = TestDispatcher() + + +class TestClass(object): + + def __init__(self): + self.dispatcher = TestDispatcher() + self.calls = [] + + @dispatch(dispatcher=TEST_DISPATCHER) + def dispatcher_wrapped(self, *args, **kw): + self.calls.append((args, kw)) + + @dispatch(dispatcher="dispatcher") + def dispatcher_string_wrapped(self, *args, **kw): + self.calls.append((args, kw)) + + @dispatch(call=TEST_DISPATCHER) + def call_wrapped(self, *args, **kw): + self.calls.append((args, kw)) + + @dispatch(call="dispatcher") + def call_string_wrapped(self, *args, **kw): + self.calls.append((args, kw)) + + +class TestDispatch(unittest.TestCase): + + def setUp(self): + self.obj = TestClass() + self.calls = [ + ((1, 2), {"c": 3, "d": 4}), + ((5, 6), {"e": 7, "f": 8}) + ] + TEST_DISPATCHER.calls = [] + + def _make_calls(self, bound_method): + for args, kw in self.calls: + bound_method(*args, **kw) + + def test_dispatcher(self): + self._make_calls(self.obj.dispatcher_wrapped) + self.assertEqual(self.obj.calls, TEST_DISPATCHER.calls) + self.assertEqual(self.obj.calls, self.calls) + + def test_dispatcher_string(self): + self._make_calls(self.obj.dispatcher_string_wrapped) + self.assertEqual(self.calls, self.obj.dispatcher.calls) + self.assertEqual(self.obj.calls, self.calls) + + def test_call(self): + self._make_calls(self.obj.call_wrapped) + self.assertEqual(self.calls, TEST_DISPATCHER.calls) + self.assertEqual(self.obj.calls, self.calls) + + def test_call_string(self): + self._make_calls(self.obj.call_string_wrapped) + self.assertEqual(self.calls, self.obj.dispatcher.calls) + self.assertEqual(self.obj.calls, self.calls) + + def test_wrong_args(self): + self.assertRaises(ValueError, dispatch) + self.assertRaises(ValueError, dispatch, 'foo', 'bar') + + +if __name__ == '__main__': + unittest.main()