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
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')