Merge "Allow for the notifier to provide a 'details_filter'"

This commit is contained in:
Jenkins 2014-12-06 01:22:16 +00:00 committed by Gerrit Code Review
commit a306fa9403
2 changed files with 154 additions and 39 deletions

View File

@ -91,11 +91,16 @@ class NotifierTest(test.TestCase):
call_counts[registered_state].append((state, details)) call_counts[registered_state].append((state, details))
notifier = nt.Notifier() notifier = nt.Notifier()
notifier.register(states.SUCCESS,
functools.partial(call_me_on, states.SUCCESS)) call_me_on_success = functools.partial(call_me_on, states.SUCCESS)
notifier.register(nt.Notifier.ANY, notifier.register(states.SUCCESS, call_me_on_success)
functools.partial(call_me_on, self.assertTrue(notifier.is_registered(states.SUCCESS,
nt.Notifier.ANY)) call_me_on_success))
call_me_on_any = functools.partial(call_me_on, nt.Notifier.ANY)
notifier.register(nt.Notifier.ANY, call_me_on_any)
self.assertTrue(notifier.is_registered(nt.Notifier.ANY,
call_me_on_any))
self.assertEqual(2, len(notifier)) self.assertEqual(2, len(notifier))
notifier.notify(states.SUCCESS, {}) notifier.notify(states.SUCCESS, {})
@ -107,3 +112,62 @@ class NotifierTest(test.TestCase):
self.assertEqual(2, len(call_counts[nt.Notifier.ANY])) self.assertEqual(2, len(call_counts[nt.Notifier.ANY]))
self.assertEqual(1, len(call_counts[states.SUCCESS])) self.assertEqual(1, len(call_counts[states.SUCCESS]))
self.assertEqual(2, len(call_counts)) self.assertEqual(2, len(call_counts))
def test_details_filter(self):
call_counts = collections.defaultdict(list)
def call_me_on(registered_state, state, details):
call_counts[registered_state].append((state, details))
def when_red(details):
return details.get('color') == 'red'
notifier = nt.Notifier()
call_me_on_success = functools.partial(call_me_on, states.SUCCESS)
notifier.register(states.SUCCESS, call_me_on_success,
details_filter=when_red)
self.assertEqual(1, len(notifier))
self.assertTrue(notifier.is_registered(
states.SUCCESS, call_me_on_success, details_filter=when_red))
notifier.notify(states.SUCCESS, {})
self.assertEqual(0, len(call_counts[states.SUCCESS]))
notifier.notify(states.SUCCESS, {'color': 'red'})
self.assertEqual(1, len(call_counts[states.SUCCESS]))
notifier.notify(states.SUCCESS, {'color': 'green'})
self.assertEqual(1, len(call_counts[states.SUCCESS]))
def test_different_details_filter(self):
call_counts = collections.defaultdict(list)
def call_me_on(registered_state, state, details):
call_counts[registered_state].append((state, details))
def when_red(details):
return details.get('color') == 'red'
def when_blue(details):
return details.get('color') == 'blue'
notifier = nt.Notifier()
call_me_on_success = functools.partial(call_me_on, states.SUCCESS)
notifier.register(states.SUCCESS, call_me_on_success,
details_filter=when_red)
notifier.register(states.SUCCESS, call_me_on_success,
details_filter=when_blue)
self.assertEqual(2, len(notifier))
self.assertTrue(notifier.is_registered(
states.SUCCESS, call_me_on_success, details_filter=when_blue))
self.assertTrue(notifier.is_registered(
states.SUCCESS, call_me_on_success, details_filter=when_red))
notifier.notify(states.SUCCESS, {})
self.assertEqual(0, len(call_counts[states.SUCCESS]))
notifier.notify(states.SUCCESS, {'color': 'red'})
self.assertEqual(1, len(call_counts[states.SUCCESS]))
notifier.notify(states.SUCCESS, {'color': 'blue'})
self.assertEqual(2, len(call_counts[states.SUCCESS]))
notifier.notify(states.SUCCESS, {'color': 'green'})
self.assertEqual(2, len(call_counts[states.SUCCESS]))

View File

@ -15,7 +15,6 @@
# under the License. # under the License.
import collections import collections
import copy
import logging import logging
import six import six
@ -25,6 +24,56 @@ from taskflow.utils import reflection
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class _Listener(object):
"""Internal helper that represents a notification listener/target."""
def __init__(self, callback, args=None, kwargs=None, details_filter=None):
self._callback = callback
self._details_filter = details_filter
if not args:
self._args = ()
else:
self._args = args[:]
if not kwargs:
self._kwargs = {}
else:
self._kwargs = kwargs.copy()
def __call__(self, event_type, details):
if self._details_filter is not None:
if not self._details_filter(details):
return
kwargs = self._kwargs.copy()
kwargs['details'] = details
self._callback(event_type, *self._args, **kwargs)
def __repr__(self):
repr_msg = "%s object at 0x%x calling into '%r'" % (
reflection.get_class_name(self), id(self), self._callback)
if self._details_filter is not None:
repr_msg += " using details filter '%r'" % self._details_filter
return "<%s>" % repr_msg
def is_equivalent(self, callback, details_filter=None):
if not reflection.is_same_callback(self._callback, callback):
return False
if details_filter is not None:
if self._details_filter is None:
return False
else:
return reflection.is_same_callback(self._details_filter,
details_filter)
else:
return self._details_filter is None
def __eq__(self, other):
if isinstance(other, _Listener):
return self.is_equivalent(other._callback,
details_filter=other._details_filter)
else:
return NotImplemented
class Notifier(object): class Notifier(object):
"""A notification helper class. """A notification helper class.
@ -34,7 +83,7 @@ class Notifier(object):
notification occurs. notification occurs.
""" """
#: Keys that can not be used in callbacks arguments #: Keys that can *not* be used in callbacks arguments
RESERVED_KEYS = ('details',) RESERVED_KEYS = ('details',)
#: Kleene star constant that is used to recieve all notifications #: Kleene star constant that is used to recieve all notifications
@ -46,15 +95,14 @@ class Notifier(object):
def __len__(self): def __len__(self):
"""Returns how many callbacks are registered.""" """Returns how many callbacks are registered."""
count = 0 count = 0
for (_event_type, callbacks) in six.iteritems(self._listeners): for (_event_type, listeners) in six.iteritems(self._listeners):
count += len(callbacks) count += len(listeners)
return count return count
def is_registered(self, event_type, callback): def is_registered(self, event_type, callback, details_filter=None):
"""Check if a callback is registered.""" """Check if a callback is registered."""
listeners = list(self._listeners.get(event_type, [])) for listener in self._listeners.get(event_type, []):
for (cb, _args, _kwargs) in listeners: if listener.is_equivalent(callback, details_filter=details_filter):
if reflection.is_same_callback(cb, callback):
return True return True
return False return False
@ -72,52 +120,55 @@ class Notifier(object):
:param details: addition event details :param details: addition event details
""" """
listeners = list(self._listeners.get(self.ANY, [])) listeners = list(self._listeners.get(self.ANY, []))
for i in self._listeners[event_type]: for listener in self._listeners[event_type]:
if i not in listeners: if listener not in listeners:
listeners.append(i) listeners.append(listener)
if not listeners: if not listeners:
return return
for (callback, args, kwargs) in listeners: for listener in listeners:
if args is None:
args = []
if kwargs is None:
kwargs = {}
kwargs['details'] = details
try: try:
callback(event_type, *args, **kwargs) listener(event_type, details)
except Exception: except Exception:
LOG.warn("Failure calling callback %s to notify about event" LOG.warn("Failure calling listener %s to notify about event"
" %s, details: %s", callback, event_type, " %s, details: %s", listener, event_type,
details, exc_info=True) details, exc_info=True)
def register(self, event_type, callback, args=None, kwargs=None): def register(self, event_type, callback,
args=None, kwargs=None, details_filter=None):
"""Register a callback to be called when event of a given type occurs. """Register a callback to be called when event of a given type occurs.
Callback will be called with provided ``args`` and ``kwargs`` and Callback will be called with provided ``args`` and ``kwargs`` and
when event type occurs (or on any event if ``event_type`` equals to when event type occurs (or on any event if ``event_type`` equals to
:attr:`.ANY`). It will also get additional keyword argument, :attr:`.ANY`). It will also get additional keyword argument,
``details``, that will hold event details provided to the ``details``, that will hold event details provided to the
:meth:`.notify` method. :meth:`.notify` method (if a details filter callback is provided then
the target callback will *only* be triggered if the details filter
callback returns a truthy value).
""" """
if not six.callable(callback): if not six.callable(callback):
raise ValueError("Notification callback must be callable") raise ValueError("Event callback must be callable")
if self.is_registered(event_type, callback): if details_filter is not None:
raise ValueError("Notification callback already registered") if not six.callable(details_filter):
raise ValueError("Details filter must be callable")
if self.is_registered(event_type, callback,
details_filter=details_filter):
raise ValueError("Event callback already registered with"
" equivalent details filter")
if kwargs: if kwargs:
for k in self.RESERVED_KEYS: for k in self.RESERVED_KEYS:
if k in kwargs: if k in kwargs:
raise KeyError(("Reserved key '%s' not allowed in " raise KeyError("Reserved key '%s' not allowed in "
"kwargs") % k) "kwargs" % k)
kwargs = copy.copy(kwargs) self._listeners[event_type].append(
if args: _Listener(callback,
args = copy.copy(args) args=args, kwargs=kwargs,
self._listeners[event_type].append((callback, args, kwargs)) details_filter=details_filter))
def deregister(self, event_type, callback): def deregister(self, event_type, callback, details_filter=None):
"""Remove a single callback from listening to event ``event_type``.""" """Remove a single callback from listening to event ``event_type``."""
if event_type not in self._listeners: if event_type not in self._listeners:
return return
for i, (cb, args, kwargs) in enumerate(self._listeners[event_type]): for i, listener in enumerate(self._listeners[event_type]):
if reflection.is_same_callback(cb, callback): if listener.is_equivalent(callback, details_filter=details_filter):
self._listeners[event_type].pop(i) self._listeners[event_type].pop(i)
break break