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:
parent
2f643d0600
commit
0cdd86d6d0
@ -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')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user