forked from klmitch/evproc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Kevin L. Mitchell
committed
Oct 20, 2014
1 parent
8781013
commit e405c1f
Showing
3 changed files
with
371 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'])) |