wx.lib.pubsub updates from Oliver Schoenborn:

- the hash problem re non-hashable objects
    - now supports listeners that use *args as an argument
      (listener(*args) was not passing the validity test)
    - corrected some mistakes in documentation
    - added some clarifications (hopefully useful for first time
      users)
    - changed the way singleton is implemented since old way prevented
      pydoc etc from extracting docs for Publisher


git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@38591 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
Robin Dunn
2006-04-06 01:15:20 +00:00
parent 2f643d0600
commit 0cdd86d6d0

View File

@@ -3,23 +3,33 @@
""" """
This module provides a publish-subscribe component that allows This module provides a publish-subscribe component that allows
listeners to subcribe to messages of a given topic. Contrary to the listeners to subcribe to messages of a given topic. Contrary to the
original wxPython.lib.pubsub module, which it is based on, it uses original wxPython.lib.pubsub module (which it is based on), it uses
weak referencing to the subscribers so the subscribers are not kept weak referencing to the subscribers so the lifetime of subscribers
alive by the Publisher. Also, callable objects can be used in addition is not affected by Publisher. Also, callable objects can be used in
to functions and bound methods. See Publisher class docs for more addition to functions and bound methods. See Publisher class docs for
details. more details.
Thanks to Robb Shecter and Robin Dunn for having provided Thanks to Robb Shecter and Robin Dunn for having provided
the basis for this module (which now shares most of the concepts but the basis for this module (which now shares most of the concepts but
very little design or implementation with the original very little design or implementation with the original
wxPython.lib.pubsub). 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 :Author: Oliver Schoenborn
:Since: Apr 2004 :Since: Apr 2004
:Version: $Id$ :Version: $Id$
:Copyright: \(c) 2004 Oliver Schoenborn :Copyright: \(c) 2004 Oliver Schoenborn
:License: wxWidgets :License: wxWidgets
"""
_implNotes = """
Implementation notes 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 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 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. much cleaner separation of concerns. But time is over, time to move on.
""" """
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -65,7 +74,9 @@ def _paramMinCountFunc(function):
assert isfunction(function) assert isfunction(function)
(args, va, kwa, dflt) = getargspec(function) (args, va, kwa, dflt) = getargspec(function)
lenDef = len(dflt or ()) 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): def _paramMinCount(callableObject):
@@ -154,17 +165,23 @@ class _WeakMethod:
else: else:
return InstanceMethod(self.fun, self.objRef(), self.cls) return InstanceMethod(self.fun, self.objRef(), self.cls)
def __cmp__(self, method2): def __eq__(self, method2):
"""Two _WeakMethod objects compare equal if they refer to the same method """Two WeakMethod objects compare equal if they refer to the same method
of the same instance.""" of the same instance. Thanks to Josiah Carlson for patch and clarifications
return hash(self) - hash(method2) 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): def __hash__(self):
"""Hash must depend on WeakRef of object, and on method, so that """Hash is an optimization for dict searches, it need not
separate methods, bound to same object, can be distinguished. return different numbers for every different object. Some objects
I'm not sure how robust this hash function is, any feedback are not hashable (eg objects of classes derived from dict) so no
welcome.""" hash(objRef()) in there, and hash(self.cls) would only be useful
return hash(self.fun)/2 + hash(self.objRef)/2 in the rare case where instance method was rebound. """
return hash(self.fun)
def __repr__(self): def __repr__(self):
dead = '' dead = ''
@@ -414,8 +431,7 @@ class _TopicTreeRoot(_TopicTreeNode):
def unsubscribe(self, listener, topicList): def unsubscribe(self, listener, topicList):
"""Remove listener from given list of topics. If topicList """Remove listener from given list of topics. If topicList
doesn't have any topics for which listener has subscribed, doesn't have any topics for which listener has subscribed,
the onNotSubscribed callback, if not None, will be called, nothing happens."""
as onNotSubscribed(listener, topic)."""
weakCB = _getWeakRef(listener) weakCB = _getWeakRef(listener)
if not self.__callbackDict.has_key(weakCB): if not self.__callbackDict.has_key(weakCB):
return return
@@ -467,7 +483,7 @@ class _TopicTreeRoot(_TopicTreeNode):
deliveryCount += node.sendMessage(message) deliveryCount += node.sendMessage(message)
else: # topic never created, don't bother continuing else: # topic never created, don't bother continuing
if onTopicNeverCreated is not None: if onTopicNeverCreated is not None:
onTopicNeverCreated(aTopic) onTopicNeverCreated(topic)
break break
return deliveryCount 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 The publish/subscribe manager. It keeps track of which listeners
are interested in which topics (see subscribe()), and sends a are interested in which topics (see subscribe()), and sends a
@@ -541,7 +559,7 @@ class Publisher:
The three important concepts for Publisher are: The three important concepts for Publisher are:
- listener: a function, bound method or - 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 (not counting 'self' in the case of methods). The parameter
will be a reference to a Message object. E.g., these listeners will be a reference to a Message object. E.g., these listeners
are ok:: are ok::
@@ -588,13 +606,17 @@ class Publisher:
:note: This class is visible to importers of pubsub only as a :note: This class is visible to importers of pubsub only as a
Singleton. I.e., every time you execute 'Publisher()', it's 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()'. use, just do 'Publisher().method()'.
""" """
__ALL_TOPICS_TPL = (ALL_TOPICS, ) __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.__messageCount = 0
self.__deliveryCount = 0 self.__deliveryCount = 0
self.__topicTree = _TopicTreeRoot() self.__topicTree = _TopicTreeRoot()
@@ -623,6 +645,23 @@ class Publisher:
listener. See the class doc for requirements on listener and listener. See the class doc for requirements on listener and
topic. 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 :note: Calling
this method for the same listener, with two topics in the same this method for the same listener, with two topics in the same
branch of the topic hierarchy, will cause the listener to be 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')) subscribe(listener, 't1') and then subscribe(listener, ('t1','t2'))
means that when calling sendMessage('t1'), listener gets one message, means that when calling sendMessage('t1'), listener gets one message,
but when calling sendMessage(('t1','t2')), listener gets message but when calling sendMessage(('t1','t2')), listener gets message
twice. This effect could be eliminated but it would not be safe to twice.
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.
""" """
self.validate(listener) self.validate(listener)
@@ -646,7 +682,7 @@ class Publisher:
def isSubscribed(self, listener, topic=None): def isSubscribed(self, listener, topic=None):
"""Return true if listener has subscribed to topic specified. """Return true if listener has subscribed to topic specified.
If no topic specified, return true if subscribed to something. 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.""" messages for all topics."""
return self.__topicTree.isSubscribed(listener, topic) return self.__topicTree.isSubscribed(listener, topic)
@@ -672,7 +708,8 @@ class Publisher:
assert (min == 0 and d>0) or (min == 1) assert (min == 0 and d>0) or (min == 1)
def isValid(self, listener): 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: try:
self.validate(listener) self.validate(listener)
return True return True
@@ -685,9 +722,9 @@ class Publisher:
list containing strings and/or tuples). If topics is not list containing strings and/or tuples). If topics is not
specified, all listeners for all topics will be unsubscribed, specified, all listeners for all topics will be unsubscribed,
ie. the Publisher singleton will have no topics and no listeners ie. the Publisher singleton will have no topics and no listeners
left. If topics was specified and is not found among contained left. If onNoSuchTopic is given, it will be called as
topics, the onNoSuchTopic, if specified, will be called, with onNoSuchTopic(topic) for each topic that is unknown.
the name of the topic.""" """
if topics is None: if topics is None:
del self.__topicTree del self.__topicTree
self.__topicTree = _TopicTreeRoot() self.__topicTree = _TopicTreeRoot()
@@ -706,9 +743,8 @@ class Publisher:
"""Unsubscribe listener. If topics not specified, listener is """Unsubscribe listener. If topics not specified, listener is
completely unsubscribed. Otherwise, it is unsubscribed only completely unsubscribed. Otherwise, it is unsubscribed only
for the topic (the usual tuple) or list of topics (ie a list for the topic (the usual tuple) or list of topics (ie a list
of tuples) specified. In this case, if listener is not actually of tuples) specified. Nothing happens if listener is not actually
subscribed for (one of) the topics, the optional onNotSubscribed subscribed to any of the topics.
callback will be called, as onNotSubscribed(listener, missingTopic).
Note that if listener subscribed for two topics (a,b) and (a,c), Note that if listener subscribed for two topics (a,b) and (a,c),
then unsubscribing for topic (a) will do nothing. You must 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): def sendMessage(self, topic=ALL_TOPICS, data=None, onTopicNeverCreated=None):
"""Send a message for given topic, with optional data, to """Send a message for given topic, with optional data, to
subscribed listeners. If topic is not specified, only the subscribed listeners. If topic is not specified, only the
listeners that are interested in all topics will receive listeners that are interested in all topics will receive message.
message. The onTopicNeverCreated is an optional callback of The onTopicNeverCreated is an optional callback of your choice that
your choice that will be called if the topic given was never will be called if the topic given was never created (i.e. it, or
created (i.e. it, or one of its subtopics, was never one of its subtopics, was never subscribed to by any listener).
subscribed to). The callback must be of the form f(a).""" It will be called as onTopicNeverCreated(topic)."""
aTopic = _tupleize(topic) aTopic = _tupleize(topic)
message = Message(aTopic, data) message = Message(aTopic, data)
self.__messageCount += 1 self.__messageCount += 1
@@ -766,8 +802,9 @@ class Publisher:
def __str__(self): def __str__(self):
return str(self.__topicTree) return str(self.__topicTree)
# Create an instance with the same name as the class, effectivly # Create the Publisher singleton. We prevent users from (inadvertently)
# hiding the class object so it can't be instantiated any more. From # 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__ # this point forward any calls to Publisher() will invoke the __call__
# of this instance which just returns itself. # of this instance which just returns itself.
# #
@@ -775,15 +812,18 @@ class Publisher:
# class from Publisher without jumping through hoops. If this ever # class from Publisher without jumping through hoops. If this ever
# becomes an issue then a new Singleton implementaion will need to be # becomes an issue then a new Singleton implementaion will need to be
# employed. # employed.
Publisher = Publisher() _key = _SingletonKey()
Publisher = PublisherClass(_key)
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
class Message: class Message:
""" """
A simple container object for the two components of A simple container object for the two components of a message: the
a message; the topic and the user data. 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): def __init__(self, topic, data):
self.topic = topic self.topic = topic
@@ -804,16 +844,28 @@ def test():
print '----------- Done %s -----------' % funcName print '----------- Done %s -----------' % funcName
def testParam(): 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: class Foo:
def testMeth(self,a,b): pass def testMeth(self,a,b): pass
def __call__(self, a): 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() foo = Foo()
assert _paramMinCount(testFunc)==(2,1)
assert _paramMinCount(Foo.testMeth)==(2,0) assert _paramMinCount(Foo.testMeth)==(2,0)
assert _paramMinCount(foo.testMeth)==(2,0) assert _paramMinCount(foo.testMeth)==(2,0)
assert _paramMinCount(foo)==(1,0) assert _paramMinCount(foo)==(1,0)
assert _paramMinCount(Foo2())==(1,0)
done('testParam') done('testParam')