diff --git a/wxPython/wx/lib/pubsub.py b/wxPython/wx/lib/pubsub.py index 05a180a362..695b1aa2da 100644 --- a/wxPython/wx/lib/pubsub.py +++ b/wxPython/wx/lib/pubsub.py @@ -3,23 +3,33 @@ """ This module provides a publish-subscribe component that allows listeners to subcribe to messages of a given topic. Contrary to the -original wxPython.lib.pubsub module, which it is based on, it uses -weak referencing to the subscribers so the subscribers are not kept -alive by the Publisher. Also, callable objects can be used in addition -to functions and bound methods. See Publisher class docs for more -details. +original wxPython.lib.pubsub module (which it is based on), it uses +weak referencing to the subscribers so the lifetime of subscribers +is not affected by Publisher. Also, callable objects can be used in +addition to functions and bound methods. See Publisher class docs for +more details. Thanks to Robb Shecter and Robin Dunn for having provided the basis for this module (which now shares most of the concepts but very little design or implementation with the original wxPython.lib.pubsub). +The publisher is a singleton instance of the PublisherClass class. You +access the instance via the Publisher object available from the module:: + + from wx.lib.pubsub import Publisher + Publisher().subscribe(...) + Publisher().sendMessage(...) + ... + :Author: Oliver Schoenborn :Since: Apr 2004 :Version: $Id$ :Copyright: \(c) 2004 Oliver Schoenborn :License: wxWidgets +""" +_implNotes = """ Implementation notes -------------------- @@ -40,7 +50,6 @@ subnodes, and _TopicTreeRoot would be a generic _Tree with named nodes, and Publisher would store listeners in each node and a topic tuple would be converted to a path in the tree. This would lead to a much cleaner separation of concerns. But time is over, time to move on. - """ #--------------------------------------------------------------------------- @@ -65,7 +74,9 @@ def _paramMinCountFunc(function): assert isfunction(function) (args, va, kwa, dflt) = getargspec(function) lenDef = len(dflt or ()) - return (len(args or ()) - lenDef, lenDef) + lenArgs = len(args or ()) + lenVA = int(va is not None) + return (lenArgs - lenDef + lenVA, lenDef) def _paramMinCount(callableObject): @@ -154,17 +165,23 @@ class _WeakMethod: else: return InstanceMethod(self.fun, self.objRef(), self.cls) - def __cmp__(self, method2): - """Two _WeakMethod objects compare equal if they refer to the same method - of the same instance.""" - return hash(self) - hash(method2) + def __eq__(self, method2): + """Two WeakMethod objects compare equal if they refer to the same method + of the same instance. Thanks to Josiah Carlson for patch and clarifications + on how dict uses eq/cmp and hashing. """ + if not isinstance(method2, _WeakMethod): + return False + return self.fun is method2.fun \ + and self.objRef() is method2.objRef() \ + and self.objRef() is not None def __hash__(self): - """Hash must depend on WeakRef of object, and on method, so that - separate methods, bound to same object, can be distinguished. - I'm not sure how robust this hash function is, any feedback - welcome.""" - return hash(self.fun)/2 + hash(self.objRef)/2 + """Hash is an optimization for dict searches, it need not + return different numbers for every different object. Some objects + are not hashable (eg objects of classes derived from dict) so no + hash(objRef()) in there, and hash(self.cls) would only be useful + in the rare case where instance method was rebound. """ + return hash(self.fun) def __repr__(self): dead = '' @@ -414,8 +431,7 @@ class _TopicTreeRoot(_TopicTreeNode): def unsubscribe(self, listener, topicList): """Remove listener from given list of topics. If topicList doesn't have any topics for which listener has subscribed, - the onNotSubscribed callback, if not None, will be called, - as onNotSubscribed(listener, topic).""" + nothing happens.""" weakCB = _getWeakRef(listener) if not self.__callbackDict.has_key(weakCB): return @@ -467,7 +483,7 @@ class _TopicTreeRoot(_TopicTreeNode): deliveryCount += node.sendMessage(message) else: # topic never created, don't bother continuing if onTopicNeverCreated is not None: - onTopicNeverCreated(aTopic) + onTopicNeverCreated(topic) break return deliveryCount @@ -531,7 +547,9 @@ class _TopicTreeRoot(_TopicTreeNode): # ----------------------------------------------------------------------------- -class Publisher: +class _SingletonKey: pass + +class PublisherClass: """ The publish/subscribe manager. It keeps track of which listeners are interested in which topics (see subscribe()), and sends a @@ -541,7 +559,7 @@ class Publisher: The three important concepts for Publisher are: - listener: a function, bound method or - callable object that can be called with only one parameter + callable object that can be called with one parameter (not counting 'self' in the case of methods). The parameter will be a reference to a Message object. E.g., these listeners are ok:: @@ -588,13 +606,17 @@ class Publisher: :note: This class is visible to importers of pubsub only as a Singleton. I.e., every time you execute 'Publisher()', it's - actually the same instance of publisher that is returned. So to + actually the same instance of PublisherClass that is returned. So to use, just do 'Publisher().method()'. """ __ALL_TOPICS_TPL = (ALL_TOPICS, ) - def __init__(self): + def __init__(self, singletonKey): + """Construct a Publisher. This can only be done by the pubsub + module. You just use pubsub.Publisher().""" + if not isinstance(singletonKey, _SingletonKey): + raise invalid_argument("Use Publisher() to get access to singleton") self.__messageCount = 0 self.__deliveryCount = 0 self.__topicTree = _TopicTreeRoot() @@ -623,6 +645,23 @@ class Publisher: listener. See the class doc for requirements on listener and topic. + :note: The listener is held by Publisher() only by *weak* reference. + This means you must ensure you have at least one strong reference + to listener, otherwise it will be DOA ("dead on arrival"). This is + particularly easy to forget when wrapping a listener method in a + proxy object (e.g. to bind some of its parameters), e.g. + + :code: + class Foo: + def listener(self, event): pass + class Wrapper: + def __init__(self, fun): self.fun = fun + def __call__(self, *args): self.fun(*args) + foo = Foo() + Publisher().subscribe( Wrapper(foo.listener) ) # whoops: DOA! + wrapper = Wrapper(foo.listener) + Publisher().subscribe(wrapper) # good! + :note: Calling this method for the same listener, with two topics in the same branch of the topic hierarchy, will cause the listener to be @@ -630,10 +669,7 @@ class Publisher: subscribe(listener, 't1') and then subscribe(listener, ('t1','t2')) means that when calling sendMessage('t1'), listener gets one message, but when calling sendMessage(('t1','t2')), listener gets message - twice. This effect could be eliminated but it would not be safe to - do so: how do we know what topic to give the listener? Answer appears - trivial at first but is far from obvious. It is best to rely on the - user to be careful about who registers for what topics. + twice. """ self.validate(listener) @@ -646,7 +682,7 @@ class Publisher: def isSubscribed(self, listener, topic=None): """Return true if listener has subscribed to topic specified. If no topic specified, return true if subscribed to something. - Use getStrAllTopics() to determine if a listener will receive + Use topic=getStrAllTopics() to determine if a listener will receive messages for all topics.""" return self.__topicTree.isSubscribed(listener, topic) @@ -672,7 +708,8 @@ class Publisher: assert (min == 0 and d>0) or (min == 1) def isValid(self, listener): - """Return true only if listener will be able to subscribe to Publisher.""" + """Return true only if listener will be able to subscribe to + Publisher.""" try: self.validate(listener) return True @@ -685,9 +722,9 @@ class Publisher: list containing strings and/or tuples). If topics is not specified, all listeners for all topics will be unsubscribed, ie. the Publisher singleton will have no topics and no listeners - left. If topics was specified and is not found among contained - topics, the onNoSuchTopic, if specified, will be called, with - the name of the topic.""" + left. If onNoSuchTopic is given, it will be called as + onNoSuchTopic(topic) for each topic that is unknown. + """ if topics is None: del self.__topicTree self.__topicTree = _TopicTreeRoot() @@ -706,9 +743,8 @@ class Publisher: """Unsubscribe listener. If topics not specified, listener is completely unsubscribed. Otherwise, it is unsubscribed only for the topic (the usual tuple) or list of topics (ie a list - of tuples) specified. In this case, if listener is not actually - subscribed for (one of) the topics, the optional onNotSubscribed - callback will be called, as onNotSubscribed(listener, missingTopic). + of tuples) specified. Nothing happens if listener is not actually + subscribed to any of the topics. Note that if listener subscribed for two topics (a,b) and (a,c), then unsubscribing for topic (a) will do nothing. You must @@ -742,11 +778,11 @@ class Publisher: def sendMessage(self, topic=ALL_TOPICS, data=None, onTopicNeverCreated=None): """Send a message for given topic, with optional data, to subscribed listeners. If topic is not specified, only the - listeners that are interested in all topics will receive - message. The onTopicNeverCreated is an optional callback of - your choice that will be called if the topic given was never - created (i.e. it, or one of its subtopics, was never - subscribed to). The callback must be of the form f(a).""" + listeners that are interested in all topics will receive message. + The onTopicNeverCreated is an optional callback of your choice that + will be called if the topic given was never created (i.e. it, or + one of its subtopics, was never subscribed to by any listener). + It will be called as onTopicNeverCreated(topic).""" aTopic = _tupleize(topic) message = Message(aTopic, data) self.__messageCount += 1 @@ -766,8 +802,9 @@ class Publisher: def __str__(self): return str(self.__topicTree) -# Create an instance with the same name as the class, effectivly -# hiding the class object so it can't be instantiated any more. From +# Create the Publisher singleton. We prevent users from (inadvertently) +# instantiating more than one object, by requiring a key that is +# accessible only to module. From # this point forward any calls to Publisher() will invoke the __call__ # of this instance which just returns itself. # @@ -775,15 +812,18 @@ class Publisher: # 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() +_key = _SingletonKey() +Publisher = PublisherClass(_key) #--------------------------------------------------------------------------- class Message: """ - A simple container object for the two components of - a message; the topic and the user data. + A simple container object for the two components of a message: the + topic and the user data. An instance of Message is given to your + listener when called by Publisher().sendMessage(topic) (if your + listener callback was registered for that topic). """ def __init__(self, topic, data): self.topic = topic @@ -804,16 +844,28 @@ def test(): print '----------- Done %s -----------' % funcName def testParam(): - def testFunc(a,b,c=1): pass + def testFunc00(): pass + def testFunc21(a,b,c=1): pass + def testFuncA(*args): pass + def testFuncAK(*args,**kwds): pass + def testFuncK(**kwds): pass + class Foo: def testMeth(self,a,b): pass def __call__(self, a): pass + class Foo2: + def __call__(self, *args): pass + assert _paramMinCount(testFunc00)==(0,0) + assert _paramMinCount(testFunc21)==(2,1) + assert _paramMinCount(testFuncA) ==(1,0) + assert _paramMinCount(testFuncAK)==(1,0) + assert _paramMinCount(testFuncK) ==(0,0) foo = Foo() - assert _paramMinCount(testFunc)==(2,1) assert _paramMinCount(Foo.testMeth)==(2,0) assert _paramMinCount(foo.testMeth)==(2,0) assert _paramMinCount(foo)==(1,0) + assert _paramMinCount(Foo2())==(1,0) done('testParam')