[Python-Dev] Generic notifier module

Ka-Ping Yee ping@lfw.org
Wed, 19 Apr 2000 10:07:36 -0700 (PDT)


I think it would be very nice for the Python standard library to
provide a messaging mechanism (you may know it as signals/slots,
publish/subscribe, listen/notify, etc.).  This could be very useful,
especially for interactive applications where various components
need to keep each other up to date about things.  I know of several
Tkinter programs where i'd like to use this mechanism.

The proposed interface is:

    To add notification ability, mix in class notifier.Notifier.

    object.notify(message, callback) - Set up notification for message.

    object.denotify(message[, callback]) - Turn off notification.

    object.send(message, **args) - Call all callbacks registered on
        object for message, in reverse order of registration, passing
        along message and **args as arguments to each callback.
        If a callback returns notifier.BREAK, no further callbacks
        are called.

(Alternatively, we could use signals/slots terminology:
connect/disconnect/emit.  I'm not aware of anything the signals/slots
mechanism has that the above lacks.)

Two kinds of messages are supported:
    
    1.  The 'message' passed to notify/denotify may be a class, and
        the 'message' passed to send may be a class or instance of
        a message class.  In this case callbacks registered on that
        class and all its bases are called.

    2.  The 'message' passed to all three methods may be any other
        hashable object, in which case it is looked up by its hash,
        and callbacks registered on a hash-equal object are called.

Thoughts and opinions are solicited (especially from those who have
worked with messaging-type things before, and know the gotchas!).
I haven't run into many tricky problems with these things in
general, and i figure that the predictable order of callbacks should
reduce complication.  (I chose reverse ordering so that you always
have the ability to add a callback that overrides existing ones.)

A straw-man implementation follows.  The callback registry is
maintained in the notifier module so you don't have to worry
about it messing up the attributes of your objects.



-------- snip snip ---------------------------------- notifier.py --------

# If a callback returns BREAK, no more callbacks are called.
BREAK = "break"

# This number goes up every time a callback is added.
serial = 0

# This dictionary maps callback functions to serial numbers.
callbacks = {}

def recipients(sender, message):
    """Return a list of (serial, callback) pairs for all the callbacks
    on this message and its base classes."""
    key = (sender, message)
    if callbacks.has_key(key):
        list = map(lambda (k, v): (v, k), callbacks[key].items())
    else:
        list = []
    if hasattr(message, "__bases__"):
        for base in message.__bases__:
            list.extend(recipients(sender, base))
    return list

class Notifier:
    def send(self, message, **args):
        """Call any callbacks registered on this object for the given message.
        If message is a class or instance, callbacks registered on the class
        or any base class are called.  Otherwise callbacks registered on a
        message of the same value (compared by hash) are called.  The message
        and any extra keyword arguments are passed along to each callback."""
        if hasattr(message, "__class__"):
            message = message.__class__
        recip = recipients(self, message)
        recip.sort()
        recip.reverse()
        for serial, callback in recip:
            if callback(message, **args) == BREAK: return

    def notify(self, message, callback):
        """Register a callback on this object for a given message.  The
        message should be a class (not an instance) or a hashable object."""
        key = (self, message)
        if not callbacks.has_key(key):
            callbacks[key] = {}
        callbacks[key][callback] = serial = serial + 1

    def denotify(self, message, callback=None):
        """Unregister a particular callback or all existing callbacks on
        this object for a given message.  The message should be a class
        (not an instance) or a hashable object."""
        key = (self, message)
        if callbacks.has_key(key):
            if callback is None:
                del callbacks[key]
            elif callbacks[key].has_key(callback):
                del callbacks[key][callback]

-------- snip snip ---------------------------------- notifier.py --------



-- ?!ng

"Je n'aime pas les stupides garçons, même quand ils sont intelligents."
    -- Roople Unia