git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@24541 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
		
			
				
	
	
		
			383 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			383 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#---------------------------------------------------------------------------
 | 
						|
# Name:        wxPython.lib.pubsub
 | 
						|
# Purpose:     The Publish/Subscribe framework used by evtmgr.EventManager
 | 
						|
#
 | 
						|
# Author:      Robb Shecter and Robin Dunn
 | 
						|
#
 | 
						|
# Created:     12-December-2002
 | 
						|
# RCS-ID:      $Id$
 | 
						|
# Copyright:   (c) 2002 by db-X Corporation
 | 
						|
# Licence:     wxWindows license
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
"""
 | 
						|
This module has classes for implementing the Publish/Subscribe design
 | 
						|
pattern.
 | 
						|
 | 
						|
It's a very flexible PS implementation: The message topics are tuples
 | 
						|
of any length, containing any objects (that can be used as hash keys).
 | 
						|
A subscriber's topic matches any message topic for which it's a
 | 
						|
sublist.
 | 
						|
 | 
						|
It also has many optimizations to favor time efficiency (ie., run-time
 | 
						|
speed).  I did this because I use it to support extreme uses.  For
 | 
						|
example, piping every wxWindows mouse event through to multiple
 | 
						|
listeners, and expecting the app to have no noticeable slowdown.  This
 | 
						|
has made the code somewhat obfuscated, but I've done my best to
 | 
						|
document it.
 | 
						|
 | 
						|
The Server and Message classes are the two that clients interact
 | 
						|
with..
 | 
						|
 | 
						|
This module is compatible with Python 2.1.
 | 
						|
 | 
						|
Author: Robb Shecter
 | 
						|
"""
 | 
						|
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
 | 
						|
class Publisher:
 | 
						|
    """
 | 
						|
    The publish/subscribe server.  This class is a Singleton.
 | 
						|
    """
 | 
						|
    def __init__(self):
 | 
						|
        self.topicDict         = {}
 | 
						|
        self.functionDict      = {}
 | 
						|
        self.subscribeAllList  = []
 | 
						|
        self.messageCount      = 0
 | 
						|
        self.deliveryCount     = 0
 | 
						|
 | 
						|
 | 
						|
    #
 | 
						|
    # Public API
 | 
						|
    #
 | 
						|
 | 
						|
    def subscribe(self, topic, listener):
 | 
						|
        """
 | 
						|
        Add the given subscription to the list.  This will
 | 
						|
        add an entry recording the fact that the listener wants
 | 
						|
        to get messages for (at least) the given topic.  This
 | 
						|
        method may be called multiple times for one listener,
 | 
						|
        registering it with many topics.  It can also be invoked
 | 
						|
        many times for a particular topic, each time with a
 | 
						|
        different listener.
 | 
						|
 | 
						|
        listener: expected to be either a method or function that
 | 
						|
        takes zero or one parameters.  (Not counting 'self' in the
 | 
						|
        case of methods. If it accepts a parameter, it will be given
 | 
						|
        a reference to a Message object.
 | 
						|
 | 
						|
        topic: will  be converted to a tuple if it isn't one.
 | 
						|
        It's a pattern matches any topic that it's a sublist
 | 
						|
        of.  For example, this pattern:
 | 
						|
 | 
						|
          ('sports',)
 | 
						|
 | 
						|
        would match these:
 | 
						|
 | 
						|
          ('sports',)
 | 
						|
          ('sports', 'baseball')
 | 
						|
          ('sports', 'baseball', 'highscores')
 | 
						|
 | 
						|
        but not these:
 | 
						|
 | 
						|
          ()
 | 
						|
          ('news')
 | 
						|
          (12345)
 | 
						|
        """
 | 
						|
        if not callable(listener):
 | 
						|
            raise TypeError('The P/S listener, '+`listener`+', is not callable.')
 | 
						|
        aTopic = Topic(topic)
 | 
						|
 | 
						|
        # Determine now (at registration time) how many parameters
 | 
						|
        # the listener expects, and get a reference to a function which
 | 
						|
        # calls it correctly at message-send time.
 | 
						|
        callableVersion = self.__makeCallable(listener)
 | 
						|
 | 
						|
        # Add this tuple to a list which is in a dict keyed by
 | 
						|
        # the topic's first element.
 | 
						|
        self.__addTopicToCorrectList(aTopic, listener, callableVersion)
 | 
						|
 | 
						|
        # Add to a dict in order to speed-up unsubscribing.
 | 
						|
        self.__addFunctionLookup(listener, aTopic)
 | 
						|
 | 
						|
 | 
						|
    def unsubscribe(self, listener):
 | 
						|
        """
 | 
						|
        Remove the given listener from the registry,
 | 
						|
        for all topics that it's associated with.
 | 
						|
        """
 | 
						|
        if not callable(listener):
 | 
						|
            raise TypeError('The P/S listener, '+`listener`+', is not callable.')
 | 
						|
        topicList = self.getAssociatedTopics(listener)
 | 
						|
        for aTopic in topicList:
 | 
						|
            subscriberList = self.__getTopicList(aTopic)
 | 
						|
            listToKeep = []
 | 
						|
            for subscriber in subscriberList:
 | 
						|
                if subscriber[0] != listener:
 | 
						|
                    listToKeep.append(subscriber)
 | 
						|
            self.__setTopicList(aTopic, listToKeep)
 | 
						|
        self.__delFunctionLookup(listener)
 | 
						|
 | 
						|
 | 
						|
    def getAssociatedTopics(self, listener):
 | 
						|
        """
 | 
						|
        Return a list of topics the given listener is
 | 
						|
        registered with.
 | 
						|
        """
 | 
						|
        return self.functionDict.get(listener, [])
 | 
						|
 | 
						|
 | 
						|
    def sendMessage(self, topic, data=None):
 | 
						|
        """
 | 
						|
        Relay a message to registered listeners.
 | 
						|
        """
 | 
						|
        aTopic    = Topic(topic)
 | 
						|
        message   = Message(aTopic.items, data)
 | 
						|
        topicList = self.__getTopicList(aTopic)
 | 
						|
 | 
						|
        # Send to the matching topics
 | 
						|
        for subscriber in topicList:
 | 
						|
            if subscriber[1].matches(aTopic):
 | 
						|
                subscriber[2](message)
 | 
						|
 | 
						|
        # Send to any listeners registered for ALL
 | 
						|
        for subscriber in self.subscribeAllList:
 | 
						|
            subscriber[2](message)
 | 
						|
 | 
						|
 | 
						|
    #
 | 
						|
    # Private methods
 | 
						|
    #
 | 
						|
 | 
						|
    def __makeCallable(self, function):
 | 
						|
        """
 | 
						|
        Return a function that is what the server
 | 
						|
        will actually call.
 | 
						|
 | 
						|
        This is a time optimization: this removes a test
 | 
						|
        for the number of parameters from the inner loop
 | 
						|
        of sendMessage().
 | 
						|
        """
 | 
						|
        parameters = self.__parameterCount(function)
 | 
						|
        if parameters == 0:
 | 
						|
            # Return a function that calls the listener
 | 
						|
            # with no arguments.
 | 
						|
            return lambda m, f=function: f()
 | 
						|
        elif parameters == 1:
 | 
						|
            # Return a function that calls the listener
 | 
						|
            # with one argument (which will be the message).
 | 
						|
            return lambda m, f=function: f(m)
 | 
						|
        else:
 | 
						|
            raise TypeError('The publish/subscribe listener, '+`function`+', has wrong parameter count')
 | 
						|
 | 
						|
 | 
						|
    def __parameterCount(self, callableObject):
 | 
						|
        """
 | 
						|
        Return the effective number of parameters required
 | 
						|
        by the callable object.  In other words, the 'self'
 | 
						|
        parameter of methods is not counted.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            # Try to handle this like a method
 | 
						|
            return callableObject.im_func.func_code.co_argcount - 1
 | 
						|
        except AttributeError:
 | 
						|
            pass
 | 
						|
 | 
						|
        try:
 | 
						|
            # Try to handle this like a function
 | 
						|
            return callableObject.func_code.co_argcount
 | 
						|
        except AttributeError:
 | 
						|
            raise 'Cannot determine if this is a method or function: '+str(callableObject)
 | 
						|
 | 
						|
    def __addFunctionLookup(self, aFunction, aTopic):
 | 
						|
        try:
 | 
						|
            aList = self.functionDict[aFunction]
 | 
						|
        except KeyError:
 | 
						|
            aList = []
 | 
						|
            self.functionDict[aFunction] = aList
 | 
						|
        aList.append(aTopic)
 | 
						|
 | 
						|
 | 
						|
    def __delFunctionLookup(self, aFunction):
 | 
						|
        try:
 | 
						|
            del self.functionDict[aFunction]
 | 
						|
        except KeyError:
 | 
						|
            print 'Warning: listener not found. Logic error in PublishSubscribe?', aFunction
 | 
						|
 | 
						|
 | 
						|
    def __addTopicToCorrectList(self, topic, listener, callableVersion):
 | 
						|
        if len(topic.items) == 0:
 | 
						|
            self.subscribeAllList.append((listener, topic, callableVersion))
 | 
						|
        else:
 | 
						|
            self.__getTopicList(topic).append((listener, topic, callableVersion))
 | 
						|
 | 
						|
 | 
						|
    def __getTopicList(self, aTopic):
 | 
						|
        """
 | 
						|
        Return the correct sublist of subscribers based on the
 | 
						|
        given topic.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            elementZero = aTopic.items[0]
 | 
						|
        except IndexError:
 | 
						|
            return self.subscribeAllList
 | 
						|
 | 
						|
        try:
 | 
						|
            subList = self.topicDict[elementZero]
 | 
						|
        except KeyError:
 | 
						|
            subList = []
 | 
						|
            self.topicDict[elementZero] = subList
 | 
						|
        return subList
 | 
						|
 | 
						|
 | 
						|
    def __setTopicList(self, aTopic, aSubscriberList):
 | 
						|
        try:
 | 
						|
            self.topicDict[aTopic.items[0]] = aSubscriberList
 | 
						|
        except IndexError:
 | 
						|
            self.subscribeAllList = aSubscriberList
 | 
						|
 | 
						|
 | 
						|
    def __call__(self):
 | 
						|
        return self
 | 
						|
 | 
						|
 | 
						|
# Create an instance with the same name as the class, effectivly
 | 
						|
# hiding the class object so it can't be instantiated any more.  From
 | 
						|
# this point forward any calls to Publisher() will invoke the __call__
 | 
						|
# of this instance which just returns itself.
 | 
						|
#
 | 
						|
# The only flaw with this approach is that you can't derive a new
 | 
						|
# class from Publisher without jumping through hoops.  If this ever
 | 
						|
# becomes an issue then a new Singleton implementaion will need to be
 | 
						|
# employed.
 | 
						|
Publisher = Publisher()
 | 
						|
 | 
						|
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
 | 
						|
class Message:
 | 
						|
    """
 | 
						|
    A simple container object for the two components of
 | 
						|
    a message; the topic and the data.
 | 
						|
    """
 | 
						|
    def __init__(self, topic, data):
 | 
						|
        self.topic = topic
 | 
						|
        self.data  = data
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return '[Topic: '+`self.topic`+',  Data: '+`self.data`+']'
 | 
						|
 | 
						|
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
 | 
						|
class Topic:
 | 
						|
    """
 | 
						|
    A class that represents a publish/subscribe topic.
 | 
						|
    Currently, it's only used internally in the framework; the
 | 
						|
    API expects and returns plain old tuples.
 | 
						|
 | 
						|
    It currently exists mostly as a place to keep the matches()
 | 
						|
    function.  This function, though, could also correctly be
 | 
						|
    seen as an attribute of the P/S server.  Getting rid of this
 | 
						|
    class would also mean one fewer object instantiation per
 | 
						|
    message send.
 | 
						|
    """
 | 
						|
 | 
						|
    listType   = type([])
 | 
						|
    tupleType  = type(())
 | 
						|
 | 
						|
    def __init__(self, items):
 | 
						|
        # Make sure we have a tuple.
 | 
						|
        if type(items) == self.__class__.listType:
 | 
						|
            items = tuple(items)
 | 
						|
        elif type(items) != self.__class__.tupleType:
 | 
						|
            items = (items,)
 | 
						|
        self.items  = items
 | 
						|
        self.length = len(items)
 | 
						|
 | 
						|
 | 
						|
    def matches(self, aTopic):
 | 
						|
        """
 | 
						|
        Consider myself to be a topic pattern,
 | 
						|
        and return True if I match the given specific
 | 
						|
        topic.  For example,
 | 
						|
        a = ('sports')
 | 
						|
        b = ('sports','baseball')
 | 
						|
        a.matches(b) --> 1
 | 
						|
        b.matches(a) --> 0
 | 
						|
        """
 | 
						|
        # The question this method answers is equivalent to;
 | 
						|
        # is my list a sublist of aTopic's?  So, my algorithm
 | 
						|
        # is: 1) make a copy of the aTopic list which is
 | 
						|
        # truncated to the pattern's length. 2) Test for
 | 
						|
        # equality.
 | 
						|
        #
 | 
						|
        # This algorithm may be somewhat memory-intensive,
 | 
						|
        # because it creates a temporary list on each
 | 
						|
        # call to match.  A possible to-do would be to
 | 
						|
        # re-write this with a hand-coded loop.
 | 
						|
        return (self.items == aTopic.items[:self.length])
 | 
						|
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        import string
 | 
						|
        return '<Topic>' + string.join(map(repr, self.items), ', ') + '</Topic>'
 | 
						|
 | 
						|
 | 
						|
    def __eq__(self, aTopic):
 | 
						|
        """
 | 
						|
        Return True if I equal the given topic.  We're considered
 | 
						|
        equal if our tuples are equal.
 | 
						|
        """
 | 
						|
        if type(self) != type(aTopic):
 | 
						|
            return 0
 | 
						|
        else:
 | 
						|
            return self.items == aTopic.items
 | 
						|
 | 
						|
 | 
						|
    def __ne__(self, aTopic):
 | 
						|
        """
 | 
						|
        Return False if I equal the given topic.
 | 
						|
        """
 | 
						|
        return not self == aTopic
 | 
						|
 | 
						|
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
 | 
						|
 | 
						|
#
 | 
						|
# Code for a simple command-line test
 | 
						|
#
 | 
						|
if __name__ == '__main__':
 | 
						|
 | 
						|
    class SimpleListener:
 | 
						|
        def __init__(self, number):
 | 
						|
            self.number = number
 | 
						|
        def notify(self, message):
 | 
						|
            print '#'+str(self.number)+' got the message:', message
 | 
						|
 | 
						|
    # Build a list of ten listeners.
 | 
						|
    lList = []
 | 
						|
    for x in range(10):
 | 
						|
        lList.append(SimpleListener(x))
 | 
						|
 | 
						|
    server = Publisher()
 | 
						|
 | 
						|
    # Everyone's interested in politics...
 | 
						|
    for x in lList:
 | 
						|
        Publisher().subscribe(topic='politics', listener=x.notify)  # also tests singleton
 | 
						|
 | 
						|
    # But only the first four are interested in trivia.
 | 
						|
    for x in lList[:4]:
 | 
						|
        server.subscribe(topic='trivia',   listener=x.notify)
 | 
						|
 | 
						|
    # This one subscribes to everything.
 | 
						|
    everythingListener = SimpleListener(999)
 | 
						|
    server.subscribe(topic=(), listener=everythingListener.notify)
 | 
						|
 | 
						|
    # Now send out two messages, testing topic matching.
 | 
						|
    server.sendMessage(topic='trivia',               data='What is the capitol of Oregon?')
 | 
						|
    server.sendMessage(topic=('politics','germany'), data='The Greens have picked up another seat in the Bundestag.')
 | 
						|
 | 
						|
#---------------------------------------------------------------------------
 |