X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/8b9a4190f70909de9568f45389e7aa3ecbc66b8a..c1d1a9cb391ff629c37cd011042fc887eba52a6c:/wxPython/wx/lib/pubsub.py diff --git a/wxPython/wx/lib/pubsub.py b/wxPython/wx/lib/pubsub.py index a2a576a037..6efa12c378 100644 --- a/wxPython/wx/lib/pubsub.py +++ b/wxPython/wx/lib/pubsub.py @@ -1,8 +1,382 @@ +#--------------------------------------------------------------------------- +# 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. -"""Renamer stub: provides a way to drop the wx prefix from wxPython objects.""" +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. -from wx import _rename -from wxPython.lib import pubsub -_rename(globals(), pubsub.__dict__, modulename='lib.pubsub') -del pubsub -del _rename +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 '' + string.join(map(repr, self.items), ', ') + '' + + + 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.') + +#---------------------------------------------------------------------------