From 3e7d72c2ce450546071dc0df72a25c8a0a0c0e8b Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Wed, 1 Jun 2016 18:27:38 +0100 Subject: [PATCH] The Notifier class now allows notifiers to be grouped by 'topic'. --- fsl/utils/notifier.py | 109 +++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/fsl/utils/notifier.py b/fsl/utils/notifier.py index b7c50f500..21f40b38f 100644 --- a/fsl/utils/notifier.py +++ b/fsl/utils/notifier.py @@ -19,11 +19,18 @@ import props log = logging.getLogger(__name__) +DEFAULT_TOPIC = 'default' +"""Topic used when the caller does not specify one when registering, +deregistering, or notifying listeners. +""" + + class Notifier(object): """The ``Notifier`` class is a mixin which provides simple notification - capability. Listeners can be registered/deregistered via the - :meth:`register` and :meth:`deregister` methods, and notified via - the :meth:`notify` method. + capability. Listeners can be registered/deregistered to listen via the + :meth:`register` and :meth:`deregister` methods, and notified via the + :meth:`notify` method. Listeners can optionally listen on a specific + *topic*, or be notified for all topics. .. note:: The ``Notifier`` class stores ``weakref`` references to registered callback functions, using the :class:`WeakFunctionRef` @@ -35,37 +42,53 @@ class Notifier(object): instance. """ new = object.__new__(cls) - new.__listeners = collections.OrderedDict() + new.__listeners = collections.defaultdict(collections.OrderedDict) return new - def register(self, name, callback): + def register(self, name, callback, topic=None): """Register a listener with this ``Notifier``. :arg name: A unique name for the listener. :arg callback: The function to call - must accept this ``Notifier`` instance as its sole argument. + :arg topic: Optional topic on which fto listen for notifications. """ + if topic is None: + topic = DEFAULT_TOPIC + # We use a WeakFunctionRef so we can refer to # both functions and class/instance methods - self.__listeners[name] = props.WeakFunctionRef(callback) + self.__listeners[topic][name] = props.WeakFunctionRef(callback) - log.debug('{}: Registered listener {} (function: {})'.format( - type(self).__name__, - name, - getattr(callback, '__name__', '<callable>'))) + log.debug('{}: Registered listener {} ' + '[topic: {}] (function: {})'.format( + type(self).__name__, + name, + topic, + getattr(callback, '__name__', '<callable>'))) - def deregister(self, name): + def deregister(self, name, topic=None): """De-register a listener that has been previously registered with this ``Notifier``. - :arg name: Name of the listener to de-register. + :arg name: Name of the listener to de-register. + :arg topic: Topic on which the listener was registered. """ - callback = self.__listeners.pop(name, None) + if topic is None: + topic = DEFAULT_TOPIC + + listeners = self.__listeners.get(topic, None) + + # Silently absorb invalid topics + if listeners is None: + return + + callback = listeners.pop(name, None) # Silently absorb invalid names - the # notify function may have removed gc'd @@ -73,6 +96,10 @@ class Notifier(object): # in the dictionary. if callback is None: return + + # No more listeners for this topic + if len(listeners) == 0: + self.__listeners.pop(topic) callback = callback.function() @@ -81,17 +108,33 @@ class Notifier(object): else: cbName = '<deleted>' - log.debug('{}: De-registered listener {} (function: {})'.format( - type(self).__name__, name, cbName)) + log.debug('{}: De-registered listener {} ' + '[topic: {}] (function: {})'.format( + type(self).__name__, + name, + topic, + cbName)) def notify(self, *args, **kwargs): """Notify all registered listeners of this ``Notifier``. - All arguments passed to this method are ignored. + + :args notifier_topic: Must be passed as a keyword argument. + The topic on which to notify. Default + listeners are always notified, regardless + of the specified topic. + + All other arguments passed to this method are ignored. """ + topic = kwargs.get('notifier_topic', DEFAULT_TOPIC) + listeners = [self.__listeners[topic]] - listeners = list(self.__listeners.items()) + if topic != DEFAULT_TOPIC: + listeners.append(self.__listeners[DEFAULT_TOPIC]) + + if sum(map(len, listeners)) == 0: + return if log.getEffectiveLevel() >= logging.DEBUG: stack = inspect.stack() @@ -100,22 +143,24 @@ class Notifier(object): srcMod = '...{}'.format(frame[1][-20:]) srcLine = frame[2] - log.debug('{}: Notifying {} listeners [{}:{}]'.format( + log.debug('{}: Notifying {} listeners (topic: {}) [{}:{}]'.format( type(self).__name__, - len(listeners), + sum(map(len, listeners)), + topic, srcMod, srcLine)) + + for ldict in listeners: + for name, callback in list(ldict.items()): - for name, callback in listeners: - - callback = callback.function() - - # The callback, or the owner of the - # callback function may have been - # gc'd - remove it if this is the case. - if callback is None: - log.debug('Listener {} has been gc\'d - ' - 'removing from list'.format(name)) - self.__listeners.pop(name) - else: - callback(self) + callback = callback.function() + + # The callback, or the owner of the + # callback function may have been + # gc'd - remove it if this is the case. + if callback is None: + log.debug('Listener {} has been gc\'d - ' + 'removing from list'.format(name)) + ldict.pop(name) + else: + callback(self) -- GitLab