Skip to content

Commit

Permalink
Decorators
Browse files Browse the repository at this point in the history
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
Show file tree
Hide file tree
Showing 3 changed files with 371 additions and 2 deletions.
124 changes: 124 additions & 0 deletions evproc/decorator.py
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
4 changes: 2 additions & 2 deletions evproc/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,15 @@ 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)

# Save it as a requirement
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)

Expand Down
245 changes: 245 additions & 0 deletions tests/unit/test_decorator.py
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']))

0 comments on commit e405c1f

Please sign in to comment.