From e405c1fe9430a0174fee9c9eb48f60b2c85e0751 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Mon, 20 Oct 2014 16:50:18 -0500 Subject: [PATCH] Decorators This contains the definition of the decorators that may be used on a processor function. The @want() decorator may be used to select which events the processor function should be called for; the @requires() decorator may be used to specify other processors that should run first; and the @required_by() decorator may be used to specify that the decorated processor should run before the listed processors. --- evproc/decorator.py | 124 ++++++++++++++++++ evproc/processor.py | 4 +- tests/unit/test_decorator.py | 245 +++++++++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 evproc/decorator.py create mode 100644 tests/unit/test_decorator.py diff --git a/evproc/decorator.py b/evproc/decorator.py new file mode 100644 index 0000000..43a3910 --- /dev/null +++ b/evproc/decorator.py @@ -0,0 +1,124 @@ +# Copyright 2014 Rackspace +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS +# IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language +# governing permissions and limitations under the License. + +import six + + +def want(*filters): + """ + A decorator which may be used to indicate which events a given + processor function will be called for. Each argument may be + either a string or a callable. For strings, the event name must + match one of the strings; for callables, each callable will be + called with the event to be processed, and must return either a + ``True`` or ``False`` value. Only if the event name matches one + of the strings (if any) *and* all of the callables (if any) return + ``True`` will this filter match. + + This decorator may be used multiple times to establish an *OR* + relationship; that is, if any of the filters declared with + ``@want()`` match, then that processor function will be called + with that event. + + :returns: A function decorator. + """ + + # First, sanity-check the arguments + if not filters: + raise TypeError("@want() takes at least 1 argument (0 given)") + + # Now, process them into a useable representation + events = set() + filt_funcs = [] + for filt in filters: + if isinstance(filt, six.string_types): + events.add(filt) + elif callable(filt): + filt_funcs.append(filt) + else: + raise TypeError("@want() must be called with strings or " + "callables, not %r" % filt) + + # Convert the events set into an appropriate filter + if events: + filt_funcs.insert(0, lambda ev: ev.name in events) + + # Set up the fulter function + if len(filt_funcs) > 1: + filt_func = lambda ev: all(filt(ev) for filt in filt_funcs) + else: + filt_func = filt_funcs[0] + + # The actual decorator function to return + def decorator(func): + filters = getattr(func, '_ev_filters', []) + filters.insert(0, filt_func) + func._ev_filters = filters + return func + + return decorator + + +def requires(*procs): + """ + A decorator which may be used to indicate that an event processor + requires certain other event processors to have been run first. + Each argument must be a string containing the name of the other + event processor. The decorator may be used multiple times, or it + may be used once with all the event processor names passed in the + argument list. + + :returns: A function decorator. + """ + + # First, sanity-check the arguments + if not procs: + raise TypeError("@requires() takes at least 1 argument (0 given)") + + # The actual decorator function to return + def decorator(func): + reqs = getattr(func, '_ev_requires', set()) + reqs |= set(procs) + func._ev_requires = reqs + return func + + return decorator + + +def required_by(*procs): + """ + A decorator which may be used to indicate that an event processor + will be required by certain other event processors. This may be + used to ensure that a given event processor runs before another + event processor. Each argument must be a string containing the + name of the other event processor. The decorator may be used + multiple times, or it may be used once with all the event + processor names passed in the argument list. + + :returns: A function decorator. + """ + + # First, sanity-check the arguments + if not procs: + raise TypeError("@required_by() takes at least 1 argument (0 given)") + + # The actual decorator function to return + def decorator(func): + reqs = getattr(func, '_ev_required_by', set()) + reqs |= set(procs) + func._ev_required_by = reqs + return func + + return decorator diff --git a/evproc/processor.py b/evproc/processor.py index 4b2b99b..eaea720 100644 --- a/evproc/processor.py +++ b/evproc/processor.py @@ -166,7 +166,7 @@ def func(self, value): self.filters = getattr(value, '_ev_filters', []) # Resolve requirements - for req_name in getattr(value, '_ev_requires', []): + for req_name in getattr(value, '_ev_requires', set()): # Get the requirement req = self.proc._get(req_name) @@ -174,7 +174,7 @@ def func(self, value): self.reqs.add(req) # Resolve any required_by values - for req_name in getattr(value, '_ev_required_by', []): + for req_name in getattr(value, '_ev_required_by', set()): # Get the requirement req = self.proc._get(req_name) diff --git a/tests/unit/test_decorator.py b/tests/unit/test_decorator.py new file mode 100644 index 0000000..404479c --- /dev/null +++ b/tests/unit/test_decorator.py @@ -0,0 +1,245 @@ +# Copyright 2014 Rackspace +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS +# IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language +# governing permissions and limitations under the License. + +import unittest + +import mock + +from evproc import decorator + + +class WantTest(unittest.TestCase): + def test_no_filters(self): + self.assertRaises(TypeError, decorator.want) + + def test_bad_type(self): + self.assertRaises(TypeError, decorator.want, + 'one', lambda ev: False, 123) + + def test_one_callable(self): + filt = mock.Mock() + func = mock.Mock(spec=[]) + + dec = decorator.want(filt) + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(func._ev_filters, [filt]) + + def test_additional_callable(self): + filt = mock.Mock() + func = mock.Mock(spec=['_ev_filters'], _ev_filters=['foo']) + + dec = decorator.want(filt) + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(func._ev_filters, [filt, 'foo']) + + def test_multi_callable(self): + filts = [ + mock.Mock(return_value=True), + mock.Mock(return_value=True), + mock.Mock(return_value=True), + ] + func = mock.Mock(spec=[]) + + dec = decorator.want(*filts) + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(len(func._ev_filters), 1) + self.assertTrue(callable(func._ev_filters[0])) + self.assertNotEqual(func._ev_filters, [filts]) + self.assertNotEqual(func._ev_filters, [filts[0]]) + + ev_result = func._ev_filters[0]('ev') + + self.assertEqual(ev_result, True) + for filt in filts: + filt.assert_called_once_with('ev') + + def test_multi_callable_short_circuit(self): + filts = [ + mock.Mock(return_value=True), + mock.Mock(return_value=False), + mock.Mock(return_value=True), + ] + func = mock.Mock(spec=[]) + + dec = decorator.want(*filts) + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(len(func._ev_filters), 1) + self.assertTrue(callable(func._ev_filters[0])) + self.assertNotEqual(func._ev_filters, [filts]) + self.assertNotEqual(func._ev_filters, [filts[0]]) + + ev_result = func._ev_filters[0]('ev') + + self.assertEqual(ev_result, False) + filts[0].assert_called_once_with('ev') + filts[1].assert_called_once_with('ev') + self.assertFalse(filts[2].called) + + def test_name_filter(self): + evs = [ + mock.Mock(expected=True, ev_name='ev1'), + mock.Mock(expected=False, ev_name='ev2'), + mock.Mock(expected=True, ev_name='ev3'), + ] + for ev in evs: + ev.name = ev.ev_name + func = mock.Mock(spec=[]) + + dec = decorator.want('ev1', 'ev3') + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(len(func._ev_filters), 1) + self.assertTrue(callable(func._ev_filters[0])) + + for ev in evs: + ev_result = func._ev_filters[0](ev) + + self.assertEqual(ev_result, ev.expected) + + def test_full_function(self): + filts = [ + mock.Mock(return_value=True), + 'ev1', + mock.Mock(side_effect=lambda ev: ev.match), + 'ev3', + mock.Mock(return_value=True), + ] + evs = [ + mock.Mock(expected=True, match=True, ev_name='ev1', + filts=set([0, 2, 4])), + mock.Mock(expected=False, match=True, ev_name='ev2', + filts=set()), + mock.Mock(expected=True, match=True, ev_name='ev3', + filts=set([0, 2, 4])), + mock.Mock(expected=False, match=False, ev_name='ev3', + filts=set([0, 2])), + ] + for ev in evs: + ev.name = ev.ev_name + func = mock.Mock(spec=[]) + + dec = decorator.want(*filts) + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(len(func._ev_filters), 1) + self.assertTrue(callable(func._ev_filters[0])) + + for ev in evs: + # Reset the filter mocks + for filt in filts: + if callable(filt): + filt.reset_mock() + + ev_result = func._ev_filters[0](ev) + + self.assertEqual(ev_result, ev.expected) + + # Check that the filters were called + for idx, filt in enumerate(filts): + if not callable(filt): + continue + + # Was it to be called? + if idx in ev.filts: + filt.assert_called_once_with(ev) + else: + self.assertFalse(filt.called) + + +class RequiresTest(unittest.TestCase): + def test_no_procs(self): + self.assertRaises(TypeError, decorator.requires) + + def test_function(self): + func = mock.Mock(spec=[]) + + dec = decorator.requires('a', 'b', 'c') + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(func._ev_requires, set(['a', 'b', 'c'])) + + def test_addition(self): + func = mock.Mock(spec='_ev_requires', _ev_requires=set(['d', 'e'])) + + dec = decorator.requires('a', 'b', 'c') + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(func._ev_requires, set(['a', 'b', 'c', 'd', 'e'])) + + +class RequiredByTest(unittest.TestCase): + def test_no_procs(self): + self.assertRaises(TypeError, decorator.required_by) + + def test_function(self): + func = mock.Mock(spec=[]) + + dec = decorator.required_by('a', 'b', 'c') + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(func._ev_required_by, set(['a', 'b', 'c'])) + + def test_addition(self): + func = mock.Mock(spec='_ev_required_by', + _ev_required_by=set(['d', 'e'])) + + dec = decorator.required_by('a', 'b', 'c') + + self.assertTrue(callable(dec)) + + result = dec(func) + + self.assertEqual(result, func) + self.assertEqual(func._ev_required_by, set(['a', 'b', 'c', 'd', 'e']))