]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/pubsub.py
2 #---------------------------------------------------------------------------
4 This module provides a publish-subscribe component that allows
5 listeners to subcribe to messages of a given topic. Contrary to the
6 original wxPython.lib.pubsub module, which it is based on, it uses
7 weak referencing to the subscribers so the subscribers are not kept
8 alive by the Publisher. Also, callable objects can be used in addition
9 to functions and bound methods. See Publisher class docs for more
12 Thanks to Robb Shecter and Robin Dunn for having provided
13 the basis for this module (which now shares most of the concepts but
14 very little design or implementation with the original
17 :Author: Oliver Schoenborn
20 :Copyright: \(c) 2004 Oliver Schoenborn
26 In class Publisher, I represent the topics-listener set as a tree
27 where each node is a topic, and contains a list of listeners of that
28 topic, and a dictionary of subtopics of that topic. When the Publisher
29 is told to send a message for a given topic, it traverses the tree
30 down to the topic for which a message is being generated, all
31 listeners on the way get sent the message.
33 Publisher currently uses a weak listener topic tree to store the
34 topics for each listener, and if a listener dies before being
35 unsubscribed, the tree is notified, and the tree eliminates the
38 Ideally, _TopicTreeNode would be a generic _TreeNode with named
39 subnodes, and _TopicTreeRoot would be a generic _Tree with named
40 nodes, and Publisher would store listeners in each node and a topic
41 tuple would be converted to a path in the tree. This would lead to a
42 much cleaner separation of concerns. But time is over, time to move on.
45 #---------------------------------------------------------------------------
47 # for function and method parameter counting:
48 from types
import InstanceType
49 from inspect
import getargspec
, ismethod
, isfunction
50 # for weakly bound methods:
51 from new
import instancemethod
as InstanceMethod
52 from weakref
import ref
as WeakRef
54 # -----------------------------------------------------------------------------
57 """Return true if method is a bound method, false otherwise"""
58 assert ismethod(method
)
59 return method
.im_self
is not None
62 def _paramMinCountFunc(function
):
63 """Given a function, return pair (min,d) where min is minimum # of
64 args required, and d is number of default arguments."""
65 assert isfunction(function
)
66 (args
, va
, kwa
, dflt
) = getargspec(function
)
67 lenDef
= len(dflt
or ())
68 return (len(args
or ()) - lenDef
, lenDef
)
71 def _paramMinCount(callableObject
):
73 Given a callable object (function, method or callable instance),
74 return pair (min,d) where min is minimum # of args required, and d
75 is number of default arguments. The 'self' parameter, in the case
76 of methods, is not counted.
78 if type(callableObject
) is InstanceType
:
79 min, d
= _paramMinCountFunc(callableObject
.__call
__.im_func
)
81 elif ismethod(callableObject
):
82 min, d
= _paramMinCountFunc(callableObject
.im_func
)
84 elif isfunction(callableObject
):
85 return _paramMinCountFunc(callableObject
)
87 raise 'Cannot determine type of callable: '+repr(callableObject
)
91 """Convert items to tuple if not already one,
92 so items must be a list, tuple or non-sequence"""
93 if isinstance(items
, list):
94 raise TypeError, 'Not allowed to tuple-ize a list'
95 elif isinstance(items
, (str, unicode)) and items
.find('.') != -1:
96 items
= tuple(items
.split('.'))
97 elif not isinstance(items
, tuple):
102 def _getCallableName(callable):
103 """Get name for a callable, ie function, bound
104 method or callable instance"""
105 if ismethod(callable):
106 return '%s.%s ' % (callable.im_self
, callable.im_func
.func_name
)
107 elif isfunction(callable):
108 return '%s ' % callable.__name
__
110 return '%s ' % callable
113 def _removeItem(item
, fromList
):
114 """Attempt to remove item from fromList, return true
115 if successful, false otherwise."""
117 fromList
.remove(item
)
123 # -----------------------------------------------------------------------------
126 """Represent a weak bound method, i.e. a method doesn't keep alive the
127 object that it is bound to. It uses WeakRef which, used on its own,
128 produces weak methods that are dead on creation, not very useful.
129 Typically, you will use the getRef() function instead of using
130 this class directly. """
132 def __init__(self
, method
, notifyDead
= None):
133 """The method must be bound. notifyDead will be called when
134 object that method is bound to dies. """
135 assert ismethod(method
)
136 if method
.im_self
is None:
137 raise ValueError, "We need a bound method!"
138 if notifyDead
is None:
139 self
.objRef
= WeakRef(method
.im_self
)
141 self
.objRef
= WeakRef(method
.im_self
, notifyDead
)
142 self
.fun
= method
.im_func
143 self
.cls
= method
.im_class
146 """Returns a new.instancemethod if object for method still alive.
147 Otherwise return None. Note that instancemethod causes a
148 strong reference to object to be created, so shouldn't save
149 the return value of this call. Note also that this __call__
150 is required only for compatibility with WeakRef.ref(), otherwise
151 there would be more efficient ways of providing this functionality."""
152 if self
.objRef() is None:
155 return InstanceMethod(self
.fun
, self
.objRef(), self
.cls
)
157 def __cmp__(self
, method2
):
158 """Two _WeakMethod objects compare equal if they refer to the same method
159 of the same instance."""
160 return hash(self
) - hash(method2
)
163 """Hash must depend on WeakRef of object, and on method, so that
164 separate methods, bound to same object, can be distinguished.
165 I'm not sure how robust this hash function is, any feedback
167 return hash(self
.fun
)/2 + hash(self
.objRef
)/2
171 if self
.objRef() is None:
173 obj
= '<%s at %s%s>' % (self
.__class
__, id(self
), dead
)
176 def refs(self
, weakRef
):
177 """Return true if we are storing same object referred to by weakRef."""
178 return self
.objRef
== weakRef
181 def _getWeakRef(obj
, notifyDead
=None):
182 """Get a weak reference to obj. If obj is a bound method, a _WeakMethod
183 object, that behaves like a WeakRef, is returned, if it is
184 anything else a WeakRef is returned. If obj is an unbound method,
185 a ValueError will be raised."""
187 createRef
= _WeakMethod
191 if notifyDead
is None:
192 return createRef(obj
)
194 return createRef(obj
, notifyDead
)
197 # -----------------------------------------------------------------------------
199 def getStrAllTopics():
200 """Function to call if, for whatever reason, you need to know
201 explicitely what is the string to use to indicate 'all topics'."""
205 # alias, easier to see where used
206 ALL_TOPICS
= getStrAllTopics()
208 # -----------------------------------------------------------------------------
212 """Encapsulate a weak reference to a method of a TopicTreeNode
213 in such a way that the method can be called, if the node is
214 still alive, but the callback does not *keep* the node alive.
215 Also, define two methods, preNotify() and noNotify(), which can
216 be redefined to something else, very useful for testing.
219 def __init__(self
, obj
):
220 self
.objRef
= _getWeakRef(obj
)
222 def __call__(self
, weakCB
):
223 notify
= self
.objRef()
224 if notify
is not None:
225 self
.preNotify(weakCB
)
230 def preNotify(self
, dead
):
231 """'Gets called just before our callback (self.objRef) is called"""
235 """Gets called if the TopicTreeNode for this callback is dead"""
239 class _TopicTreeNode
:
240 """A node in the topic tree. This contains a list of callables
241 that are interested in the topic that this node is associated
242 with, and contains a dictionary of subtopics, whose associated
243 values are other _TopicTreeNodes. The topic of a node is not stored
244 in the node, so that the tree can be implemented as a dictionary
245 rather than a list, for ease of use (and, likely, performance).
247 Note that it uses _NodeCallback to encapsulate a callback for
248 when a registered listener dies, possible thanks to WeakRef.
249 Whenever this callback is called, the onDeadListener() function,
250 passed in at construction time, is called (unless it is None).
253 def __init__(self
, topicPath
, onDeadListenerWeakCB
):
254 self
.__subtopics
= {}
255 self
.__callables
= []
256 self
.__topicPath
= topicPath
257 self
.__onDeadListenerWeakCB
= onDeadListenerWeakCB
259 def getPathname(self
):
260 """The complete node path to us, ie., the topic tuple that would lead to us"""
261 return self
.__topicPath
263 def createSubtopic(self
, subtopic
, topicPath
):
264 """Create a child node for subtopic"""
265 return self
.__subtopics
.setdefault(subtopic
,
266 _TopicTreeNode(topicPath
, self
.__onDeadListenerWeakCB
))
268 def hasSubtopic(self
, subtopic
):
269 """Return true only if topic string is one of subtopics of this node"""
270 return self
.__subtopics
.has_key(subtopic
)
272 def getNode(self
, subtopic
):
273 """Return ref to node associated with subtopic"""
274 return self
.__subtopics
[subtopic
]
276 def addCallable(self
, callable):
277 """Add a callable to list of callables for this topic node"""
279 id = self
.__callables
.index(_getWeakRef(callable))
280 return self
.__callables
[id]
282 wrCall
= _getWeakRef(callable, _NodeCallback(self
.__notifyDead
))
283 self
.__callables
.append(wrCall
)
286 def getCallables(self
):
287 """Get callables associated with this topic node"""
288 return [cb() for cb
in self
.__callables
if cb() is not None]
290 def hasCallable(self
, callable):
291 """Return true if callable in this node"""
293 self
.__callables
.index(_getWeakRef(callable))
298 def sendMessage(self
, message
):
299 """Send a message to our callables"""
301 for cb
in self
.__callables
:
303 if listener
is not None:
308 def removeCallable(self
, callable):
309 """Remove weak callable from our node (and return True).
310 Does nothing if not here (and returns False)."""
312 self
.__callables
.remove(_getWeakRef(callable))
317 def clearCallables(self
):
318 """Abandon list of callables to caller. We no longer have
319 any callables after this method is called."""
320 tmpList
= [cb
for cb
in self
.__callables
if cb() is not None]
321 self
.__callables
= []
324 def __notifyDead(self
, dead
):
325 """Gets called when a listener dies, thanks to WeakRef"""
326 #print 'TreeNODE', `self`, 'received death certificate for ', dead
328 if self
.__onDeadListenerWeakCB
is not None:
329 cb
= self
.__onDeadListenerWeakCB
()
333 def __cleanupDead(self
):
334 """Remove all dead objects from list of callables"""
335 self
.__callables
= [cb
for cb
in self
.__callables
if cb() is not None]
338 """Print us in a not-so-friendly, but readable way, good for debugging."""
340 for callable in self
.getCallables():
341 strVal
.append(_getCallableName(callable))
342 for topic
, node
in self
.__subtopics
.iteritems():
343 strVal
.append(' (%s: %s)' %(topic
, node
))
344 return ''.join(strVal
)
347 class _TopicTreeRoot(_TopicTreeNode
):
349 The root of the tree knows how to access other node of the
350 tree and is the gateway of the tree user to the tree nodes.
351 It can create topics, and and remove callbacks, etc.
353 For efficiency, it stores a dictionary of listener-topics,
354 so that unsubscribing a listener just requires finding the
355 topics associated to a listener, and finding the corresponding
356 nodes of the tree. Without it, unsubscribing would require
357 that we search the whole tree for all nodes that contain
358 given listener. Since Publisher is a singleton, it will
359 contain all topics in the system so it is likely to be a large
360 tree. However, it is possible that in some runs, unsubscribe()
361 is called very little by the user, in which case most unsubscriptions
362 are automatic, ie caused by the listeners dying. In this case,
363 a flag is set to indicate that the dictionary should be cleaned up
364 at the next opportunity. This is not necessary, it is just an
369 self
.__callbackDict
= {}
370 self
.__callbackDictCleanup
= 0
371 # all child nodes will call our __rootNotifyDead method
372 # when one of their registered listeners dies
373 _TopicTreeNode
.__init
__(self
, (ALL_TOPICS
,),
374 _getWeakRef(self
.__rootNotifyDead
))
376 def addTopic(self
, topic
, listener
):
377 """Add topic to tree if doesnt exist, and add listener to topic node"""
378 assert isinstance(topic
, tuple)
379 topicNode
= self
.__getTreeNode
(topic
, make
=True)
380 weakCB
= topicNode
.addCallable(listener
)
381 assert topicNode
.hasCallable(listener
)
383 theList
= self
.__callbackDict
.setdefault(weakCB
, [])
384 assert self
.__callbackDict
.has_key(weakCB
)
385 # add it only if we don't already have it
387 weakTopicNode
= WeakRef(topicNode
)
388 theList
.index(weakTopicNode
)
390 theList
.append(weakTopicNode
)
391 assert self
.__callbackDict
[weakCB
].index(weakTopicNode
) >= 0
393 def getTopics(self
, listener
):
394 """Return the list of topics for given listener"""
395 weakNodes
= self
.__callbackDict
.get(_getWeakRef(listener
), [])
396 return [weakNode().getPathname() for weakNode
in weakNodes
397 if weakNode() is not None]
399 def isSubscribed(self
, listener
, topic
=None):
400 """Return true if listener is registered for topic specified.
401 If no topic specified, return true if subscribed to something.
402 Use topic=getStrAllTopics() to determine if a listener will receive
403 messages for all topics."""
404 weakCB
= _getWeakRef(listener
)
406 return self
.__callbackDict
.has_key(weakCB
)
408 topicPath
= _tupleize(topic
)
409 for weakNode
in self
.__callbackDict
[weakCB
]:
410 if topicPath
== weakNode().getPathname():
414 def unsubscribe(self
, listener
, topicList
):
415 """Remove listener from given list of topics. If topicList
416 doesn't have any topics for which listener has subscribed,
417 the onNotSubscribed callback, if not None, will be called,
418 as onNotSubscribed(listener, topic)."""
419 weakCB
= _getWeakRef(listener
)
420 if not self
.__callbackDict
.has_key(weakCB
):
423 cbNodes
= self
.__callbackDict
[weakCB
]
424 if topicList
is None:
425 for weakNode
in cbNodes
:
426 weakNode().removeCallable(listener
)
427 del self
.__callbackDict
[weakCB
]
430 for weakNode
in cbNodes
:
432 if node
is not None and node
.getPathname() in topicList
:
433 success
= node
.removeCallable(listener
)
434 assert success
== True
435 cbNodes
.remove(weakNode
)
436 assert not self
.isSubscribed(listener
, node
.getPathname())
438 def unsubAll(self
, topicList
, onNoSuchTopic
):
439 """Unsubscribe all listeners registered for any topic in
440 topicList. If a topic in the list does not exist, and
441 onNoSuchTopic is not None, a call
442 to onNoSuchTopic(topic) is done for that topic."""
443 for topic
in topicList
:
444 node
= self
.__getTreeNode
(topic
)
446 weakCallables
= node
.clearCallables()
447 for callable in weakCallables
:
448 weakNodes
= self
.__callbackDict
[callable]
449 success
= _removeItem(WeakRef(node
), weakNodes
)
450 assert success
== True
452 del self
.__callbackDict
[callable]
453 elif onNoSuchTopic
is not None:
456 def sendMessage(self
, topic
, message
, onTopicNeverCreated
):
457 """Send a message for given topic to all registered listeners. If
458 topic doesn't exist, call onTopicNeverCreated(topic)."""
459 # send to the all-toipcs listeners
460 deliveryCount
= _TopicTreeNode
.sendMessage(self
, message
)
461 # send to those who listen to given topic or any of its supertopics
463 for topicItem
in topic
:
464 assert topicItem
!= ''
465 if node
.hasSubtopic(topicItem
):
466 node
= node
.getNode(topicItem
)
467 deliveryCount
+= node
.sendMessage(message
)
468 else: # topic never created, don't bother continuing
469 if onTopicNeverCreated
is not None:
470 onTopicNeverCreated(aTopic
)
474 def numListeners(self
):
475 """Return a pair (live, dead) with count of live and dead listeners in tree"""
477 for cb
in self
.__callbackDict
:
484 # clean up the callback dictionary after how many dead listeners
485 callbackDeadLimit
= 10
487 def __rootNotifyDead(self
, dead
):
488 #print 'TreeROOT received death certificate for ', dead
489 self
.__callbackDictCleanup
+= 1
490 if self
.__callbackDictCleanup
> _TopicTreeRoot
.callbackDeadLimit
:
491 self
.__callbackDictCleanup
= 0
492 oldDict
= self
.__callbackDict
493 self
.__callbackDict
= {}
494 for weakCB
, weakNodes
in oldDict
.iteritems():
495 if weakCB() is not None:
496 self
.__callbackDict
[weakCB
] = weakNodes
498 def __getTreeNode(self
, topic
, make
=False):
499 """Return the tree node for 'topic' from the topic tree. If it
500 doesnt exist and make=True, create it first."""
501 # if the all-topics, give root;
502 if topic
== (ALL_TOPICS
,):
505 # not root, so traverse tree
508 for topicItem
in topic
:
510 if topicItem
== ALL_TOPICS
:
511 raise ValueError, 'Topic tuple must not contain ""'
513 node
= node
.createSubtopic(topicItem
, path
)
514 elif node
.hasSubtopic(topicItem
):
515 node
= node
.getNode(topicItem
)
521 def printCallbacks(self
):
522 strVal
= ['Callbacks:\n']
523 for listener
, weakTopicNodes
in self
.__callbackDict
.iteritems():
524 topics
= [topic() for topic
in weakTopicNodes
if topic() is not None]
525 strVal
.append(' %s: %s\n' % (_getCallableName(listener()), topics
))
526 return ''.join(strVal
)
529 return 'all: %s' % _TopicTreeNode
.__str
__(self
)
532 # -----------------------------------------------------------------------------
536 The publish/subscribe manager. It keeps track of which listeners
537 are interested in which topics (see subscribe()), and sends a
538 Message for a given topic to listeners that have subscribed to
539 that topic, with optional user data (see sendMessage()).
541 The three important concepts for Publisher are:
543 - listener: a function, bound method or
544 callable object that can be called with only one parameter
545 (not counting 'self' in the case of methods). The parameter
546 will be a reference to a Message object. E.g., these listeners
550 def __call__(self, a, b=1): pass # can be called with only one arg
551 def meth(self, a): pass # takes only one arg
552 def meth2(self, a=2, b=''): pass # can be called with one arg
554 def func(a, b=''): pass
557 Publisher().subscribe(foo) # functor
558 Publisher().subscribe(foo.meth) # bound method
559 Publisher().subscribe(foo.meth2) # bound method
560 Publisher().subscribe(func) # function
562 The three types of callables all have arguments that allow a call
563 with only one argument. In every case, the parameter 'a' will contain
566 - topic: a single word, a tuple of words, or a string containing a
567 set of words separated by dots, for example: 'sports.baseball'.
568 A tuple or a dotted notation string denotes a hierarchy of
569 topics from most general to least. For example, a listener of
572 ('sports','baseball')
574 would receive messages for these topics::
576 ('sports', 'baseball') # because same
577 ('sports', 'baseball', 'highscores') # because more specific
581 'sports' # because more general
582 ('sports',) # because more general
583 () or ('') # because only for those listening to 'all' topics
584 ('news') # because different topic
586 - message: this is an instance of Message, containing the topic for
587 which the message was sent, and any data the sender specified.
589 :note: This class is visible to importers of pubsub only as a
590 Singleton. I.e., every time you execute 'Publisher()', it's
591 actually the same instance of publisher that is returned. So to
592 use, just do 'Publisher().method()'.
595 __ALL_TOPICS_TPL
= (ALL_TOPICS
, )
598 self
.__messageCount
= 0
599 self
.__deliveryCount
= 0
600 self
.__topicTree
= _TopicTreeRoot()
606 def getDeliveryCount(self
):
607 """How many listeners have received a message since beginning of run"""
608 return self
.__deliveryCount
610 def getMessageCount(self
):
611 """How many times sendMessage() was called since beginning of run"""
612 return self
.__messageCount
614 def subscribe(self
, listener
, topic
= ALL_TOPICS
):
616 Subscribe listener for given topic. If topic is not specified,
617 listener will be subscribed for all topics (that listener will
618 receive a Message for any topic for which a message is generated).
620 This method may be called multiple times for one listener,
621 registering it with many topics. It can also be invoked many
622 times for a particular topic, each time with a different
623 listener. See the class doc for requirements on listener and
627 this method for the same listener, with two topics in the same
628 branch of the topic hierarchy, will cause the listener to be
629 notified twice when a message for the deepest topic is sent. E.g.
630 subscribe(listener, 't1') and then subscribe(listener, ('t1','t2'))
631 means that when calling sendMessage('t1'), listener gets one message,
632 but when calling sendMessage(('t1','t2')), listener gets message
633 twice. This effect could be eliminated but it would not be safe to
634 do so: how do we know what topic to give the listener? Answer appears
635 trivial at first but is far from obvious. It is best to rely on the
636 user to be careful about who registers for what topics.
638 self
.validate(listener
)
641 raise TypeError, 'Topic must be either a word, tuple of '\
642 'words, or getStrAllTopics()'
644 self
.__topicTree
.addTopic(_tupleize(topic
), listener
)
646 def isSubscribed(self
, listener
, topic
=None):
647 """Return true if listener has subscribed to topic specified.
648 If no topic specified, return true if subscribed to something.
649 Use getStrAllTopics() to determine if a listener will receive
650 messages for all topics."""
651 return self
.__topicTree
.isSubscribed(listener
, topic
)
653 def validate(self
, listener
):
654 """Similar to isValid(), but raises a TypeError exception if not valid"""
656 if not callable(listener
):
657 raise TypeError, 'Listener '+`listener`
+' must be a '\
658 'function, bound method or instance.'
659 # ok, callable, but if method, is it bound:
660 elif ismethod(listener
) and not _isbound(listener
):
661 raise TypeError, 'Listener '+`listener`
+\
662 ' is a method but it is unbound!'
664 # check that it takes the right number of parameters
665 min, d
= _paramMinCount(listener
)
667 raise TypeError, 'Listener '+`listener`
+" can't"\
668 ' require more than one parameter!'
669 if min <= 0 and d
== 0:
670 raise TypeError, 'Listener '+`listener`
+' lacking arguments!'
672 assert (min == 0 and d
>0) or (min == 1)
674 def isValid(self
, listener
):
675 """Return true only if listener will be able to subscribe to Publisher."""
677 self
.validate(listener
)
682 def unsubAll(self
, topics
=None, onNoSuchTopic
=None):
683 """Unsubscribe all listeners subscribed for topics. Topics can
684 be a single topic (string or tuple) or a list of topics (ie
685 list containing strings and/or tuples). If topics is not
686 specified, all listeners for all topics will be unsubscribed,
687 ie. the Publisher singleton will have no topics and no listeners
688 left. If topics was specified and is not found among contained
689 topics, the onNoSuchTopic, if specified, will be called, with
690 the name of the topic."""
693 self
.__topicTree
= _TopicTreeRoot()
696 # make sure every topics are in tuple form
697 if isinstance(topics
, list):
698 topicList
= [_tupleize(x
) for x
in topics
]
700 topicList
= [_tupleize(topics
)]
702 # unsub every listener of topics
703 self
.__topicTree
.unsubAll(topicList
, onNoSuchTopic
)
705 def unsubscribe(self
, listener
, topics
=None):
706 """Unsubscribe listener. If topics not specified, listener is
707 completely unsubscribed. Otherwise, it is unsubscribed only
708 for the topic (the usual tuple) or list of topics (ie a list
709 of tuples) specified. In this case, if listener is not actually
710 subscribed for (one of) the topics, the optional onNotSubscribed
711 callback will be called, as onNotSubscribed(listener, missingTopic).
713 Note that if listener subscribed for two topics (a,b) and (a,c),
714 then unsubscribing for topic (a) will do nothing. You must
715 use getAssociatedTopics(listener) and give unsubscribe() the returned
716 list (or a subset thereof).
718 self
.validate(listener
)
720 if topics
is not None:
721 if isinstance(topics
, list):
722 topicList
= [_tupleize(x
) for x
in topics
]
724 topicList
= [_tupleize(topics
)]
726 self
.__topicTree
.unsubscribe(listener
, topicList
)
728 def getAssociatedTopics(self
, listener
):
729 """Return a list of topics the given listener is registered with.
730 Returns [] if listener never subscribed.
732 :attention: when using the return of this method to compare to
733 expected list of topics, remember that topics that are not in the
734 form of a tuple appear as a one-tuple in the return. E.g. if you
735 have subscribed a listener to 'topic1' and ('topic2','subtopic2'),
736 this method returns::
738 associatedTopics = [('topic1',), ('topic2','subtopic2')]
740 return self
.__topicTree
.getTopics(listener
)
742 def sendMessage(self
, topic
=ALL_TOPICS
, data
=None, onTopicNeverCreated
=None):
743 """Send a message for given topic, with optional data, to
744 subscribed listeners. If topic is not specified, only the
745 listeners that are interested in all topics will receive
746 message. The onTopicNeverCreated is an optional callback of
747 your choice that will be called if the topic given was never
748 created (i.e. it, or one of its subtopics, was never
749 subscribed to). The callback must be of the form f(a)."""
750 aTopic
= _tupleize(topic
)
751 message
= Message(aTopic
, data
)
752 self
.__messageCount
+= 1
754 # send to those who listen to all topics
755 self
.__deliveryCount
+= \
756 self
.__topicTree
.sendMessage(aTopic
, message
, onTopicNeverCreated
)
763 """Allows for singleton"""
767 return str(self
.__topicTree
)
769 # Create an instance with the same name as the class, effectivly
770 # hiding the class object so it can't be instantiated any more. From
771 # this point forward any calls to Publisher() will invoke the __call__
772 # of this instance which just returns itself.
774 # The only flaw with this approach is that you can't derive a new
775 # class from Publisher without jumping through hoops. If this ever
776 # becomes an issue then a new Singleton implementaion will need to be
778 Publisher
= Publisher()
781 #---------------------------------------------------------------------------
785 A simple container object for the two components of
786 a message; the topic and the user data.
788 def __init__(self
, topic
, data
):
793 return '[Topic: '+`self
.topic`
+', Data: '+`self
.data`
+']'
796 #---------------------------------------------------------------------------
800 # Code for a simple command-line test
804 print '----------- Done %s -----------' % funcName
807 def testFunc(a
,b
,c
=1): pass
809 def testMeth(self
,a
,b
): pass
810 def __call__(self
, a
): pass
813 assert _paramMinCount(testFunc
)==(2,1)
814 assert _paramMinCount(Foo
.testMeth
)==(2,0)
815 assert _paramMinCount(foo
.testMeth
)==(2,0)
816 assert _paramMinCount(foo
)==(1,0)
821 #------------------------
823 _NodeCallback
.notified
= 0
824 def testPreNotifyNode(self
, dead
):
825 _NodeCallback
.notified
+= 1
826 print 'testPreNotifyNODE heard notification of', `dead`
827 _NodeCallback
.preNotify
= testPreNotifyNode
832 def __init__(self
, s
):
834 def __call__(self
, msg
):
835 print 'WS#', self
.s
, ' received msg ', msg
839 def testPreNotifyRoot(dead
):
840 print 'testPreNotifyROOT heard notification of', `dead`
842 node
= _TopicTreeNode((ALL_TOPICS
,), WeakRef(testPreNotifyRoot
))
843 boo
, baz
, bid
= WS('boo'), WS('baz'), WS('bid')
844 node
.addCallable(boo
)
845 node
.addCallable(baz
)
846 node
.addCallable(boo
)
847 assert node
.getCallables() == [boo
,baz
]
848 assert node
.hasCallable(boo
)
850 node
.removeCallable(bid
) # no-op
851 assert node
.hasCallable(baz
)
852 assert node
.getCallables() == [boo
,baz
]
854 node
.removeCallable(boo
)
855 assert node
.getCallables() == [baz
]
856 assert node
.hasCallable(baz
)
857 assert not node
.hasCallable(boo
)
859 node
.removeCallable(baz
)
860 assert node
.getCallables() == []
861 assert not node
.hasCallable(baz
)
863 node2
= node
.createSubtopic('st1', ('st1',))
864 node3
= node
.createSubtopic('st2', ('st2',))
865 cb1
, cb2
, cb
= WS('st1_cb1'), WS('st1_cb2'), WS('st2_cb')
866 node2
.addCallable(cb1
)
867 node2
.addCallable(cb2
)
868 node3
.addCallable(cb
)
869 node2
.createSubtopic('st3', ('st1','st3'))
870 node2
.createSubtopic('st4', ('st1','st4'))
873 assert str(node
) == ' (st1: st1_cb1 st1_cb2 (st4: ) (st3: )) (st2: st2_cb )'
875 # verify send message, and that a dead listener does not get sent one
876 delivered
= node2
.sendMessage('hello')
877 assert delivered
== 2
879 delivered
= node2
.sendMessage('hello')
880 assert delivered
== 1
881 assert _NodeCallback
.notified
== 1
886 #------------------------
890 def __call__(self
, a
): pass
891 def fun(self
, b
): pass
892 def fun2(self
, b
=1): pass
893 def fun3(self
, a
, b
=2): pass
894 def badFun(self
): pass
896 def badFun3(self
, a
, b
): pass
901 server
.validate(foo
.fun
)
902 server
.validate(foo
.fun2
)
903 server
.validate(foo
.fun3
)
904 assert not server
.isValid(foo
.badFun
)
905 assert not server
.isValid(foo
.badFun2
)
906 assert not server
.isValid(foo
.badFun3
)
911 #------------------------
913 class SimpleListener
:
914 def __init__(self
, number
):
916 def __call__(self
, message
= ''):
917 print 'Callable #%s got the message "%s"' %(self
.number
, message
)
918 def notify(self
, message
):
919 print '%s.notify() got the message "%s"' %(self
.number
, message
)
921 return "SimpleListener_%s" % self
.number
924 publisher
= Publisher()
927 topic2
= ('history','middle age')
928 topic3
= ('politics','UN')
929 topic4
= ('politics','NATO')
930 topic5
= ('politics','NATO','US')
932 lisnr1
= SimpleListener(1)
933 lisnr2
= SimpleListener(2)
934 def func(message
, a
=1):
935 print 'Func received message "%s"' % message
937 lisnr4
= lambda x
: 'Lambda received message "%s"' % x
939 assert not publisher
.isSubscribed(lisnr1
)
940 assert not publisher
.isSubscribed(lisnr2
)
941 assert not publisher
.isSubscribed(lisnr3
)
942 assert not publisher
.isSubscribed(lisnr4
)
944 publisher
.subscribe(lisnr1
, topic1
)
945 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,)]
946 publisher
.subscribe(lisnr1
, topic2
)
947 publisher
.subscribe(lisnr1
, topic1
) # do it again, should be no-op
948 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,),topic2
]
949 publisher
.subscribe(lisnr2
.notify
, topic3
)
950 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == [topic3
]
951 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,),topic2
]
952 publisher
.subscribe(lisnr3
, topic5
)
953 assert publisher
.getAssociatedTopics(lisnr3
) == [topic5
]
954 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == [topic3
]
955 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,),topic2
]
956 publisher
.subscribe(lisnr4
)
958 print "Publisher tree: ", publisher
959 assert publisher
.isSubscribed(lisnr1
)
960 assert publisher
.isSubscribed(lisnr1
, topic1
)
961 assert publisher
.isSubscribed(lisnr1
, topic2
)
962 assert publisher
.isSubscribed(lisnr2
.notify
)
963 assert publisher
.isSubscribed(lisnr3
, topic5
)
964 assert publisher
.isSubscribed(lisnr4
, ALL_TOPICS
)
965 expectTopicTree
= 'all: <lambda> (politics: SimpleListener_1 (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: SimpleListener_1 ))'
966 print "Publisher tree: ", publisher
967 assert str(publisher
) == expectTopicTree
969 publisher
.unsubscribe(lisnr1
, 'booboo') # should do nothing
970 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,),topic2
]
971 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == [topic3
]
972 assert publisher
.getAssociatedTopics(lisnr3
) == [topic5
]
973 publisher
.unsubscribe(lisnr1
, topic1
)
974 assert publisher
.getAssociatedTopics(lisnr1
) == [topic2
]
975 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == [topic3
]
976 assert publisher
.getAssociatedTopics(lisnr3
) == [topic5
]
977 publisher
.unsubscribe(lisnr1
, topic2
)
978 publisher
.unsubscribe(lisnr1
, topic2
)
979 publisher
.unsubscribe(lisnr2
.notify
, topic3
)
980 publisher
.unsubscribe(lisnr3
, topic5
)
981 assert publisher
.getAssociatedTopics(lisnr1
) == []
982 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == []
983 assert publisher
.getAssociatedTopics(lisnr3
) == []
984 publisher
.unsubscribe(lisnr4
)
986 expectTopicTree
= 'all: (politics: (UN: ) (NATO: (US: ))) (history: (middle age: ))'
987 print "Publisher tree: ", publisher
988 assert str(publisher
) == expectTopicTree
989 assert publisher
.getDeliveryCount() == 0
990 assert publisher
.getMessageCount() == 0
993 assert str(publisher
) == 'all: '
995 done('testSubscribe')
998 #------------------------
1001 publisher
= Publisher()
1004 topic2
= ('history','middle age')
1005 topic3
= ('politics','UN')
1006 topic4
= ('politics','NATO')
1007 topic5
= ('politics','NATO','US')
1009 lisnr1
= SimpleListener(1)
1010 lisnr2
= SimpleListener(2)
1011 def func(message
, a
=1):
1012 print 'Func received message "%s"' % message
1014 lisnr4
= lambda x
: 'Lambda received message "%s"' % x
1016 publisher
.subscribe(lisnr1
, topic1
)
1017 publisher
.subscribe(lisnr1
, topic2
)
1018 publisher
.subscribe(lisnr2
.notify
, topic3
)
1019 publisher
.subscribe(lisnr3
, topic2
)
1020 publisher
.subscribe(lisnr3
, topic5
)
1021 publisher
.subscribe(lisnr4
)
1023 expectTopicTree
= 'all: <lambda> (politics: SimpleListener_1 (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: SimpleListener_1 func ))'
1024 print "Publisher tree: ", publisher
1025 assert str(publisher
) == expectTopicTree
1027 publisher
.unsubAll(topic1
)
1028 assert publisher
.getAssociatedTopics(lisnr1
) == [topic2
]
1029 assert not publisher
.isSubscribed(lisnr1
, topic1
)
1031 publisher
.unsubAll(topic2
)
1033 assert publisher
.getAssociatedTopics(lisnr1
) == []
1034 assert publisher
.getAssociatedTopics(lisnr3
) == [topic5
]
1035 assert not publisher
.isSubscribed(lisnr1
)
1036 assert publisher
.isSubscribed(lisnr3
, topic5
)
1038 #print "Publisher tree: ", publisher
1039 expectTopicTree
= 'all: <lambda> (politics: (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: ))'
1040 assert str(publisher
) == expectTopicTree
1041 publisher
.unsubAll(ALL_TOPICS
)
1042 #print "Publisher tree: ", publisher
1043 expectTopicTree
= 'all: (politics: (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: ))'
1044 assert str(publisher
) == expectTopicTree
1046 publisher
.unsubAll()
1047 done('testUnsubAll')
1050 #------------------------
1053 publisher
= Publisher()
1057 def __init__(self
, num
):
1059 def __call__(self
, b
):
1060 called
.append( 'TL%scb' % self
.number
)
1061 def notify(self
, b
):
1062 called
.append( 'TL%sm' % self
.number
)
1063 def funcListener(b
):
1064 called
.append('func')
1066 lisnr1
= TestListener(1)
1067 lisnr2
= TestListener(2)
1068 lisnr3
= funcListener
1069 lisnr4
= lambda x
: called
.append('lambda')
1073 topic3
= ('politics','UN')
1074 topic4
= ('politics','NATO','US')
1075 topic5
= ('politics','NATO')
1077 publisher
.subscribe(lisnr1
, topic1
)
1078 publisher
.subscribe(lisnr2
, topic2
)
1079 publisher
.subscribe(lisnr2
.notify
, topic2
)
1080 publisher
.subscribe(lisnr3
, topic4
)
1081 publisher
.subscribe(lisnr4
)
1085 # setup ok, now test send/receipt
1086 publisher
.sendMessage(topic1
)
1087 assert called
== ['lambda','TL1cb']
1089 publisher
.sendMessage(topic2
)
1090 assert called
== ['lambda','TL2cb','TL2m']
1092 publisher
.sendMessage(topic3
)
1093 assert called
== ['lambda','TL1cb']
1095 publisher
.sendMessage(topic4
)
1096 assert called
== ['lambda','TL1cb','func']
1098 publisher
.sendMessage(topic5
)
1099 assert called
== ['lambda','TL1cb']
1100 assert publisher
.getDeliveryCount() == 12
1101 assert publisher
.getMessageCount() == 5
1103 # test weak referencing works:
1104 _NodeCallback
.notified
= 0
1107 publisher
.sendMessage(topic2
)
1108 assert called
== ['lambda']
1109 assert _NodeCallback
.notified
== 2
1114 assert _NodeCallback
.notified
== 5
1117 # verify if weak references work as expected
1118 print '------ Starting testDead ----------'
1119 node
= _TopicTreeNode('t1', None)
1120 lisnr1
= SimpleListener(1)
1121 lisnr2
= SimpleListener(2)
1122 lisnr3
= SimpleListener(3)
1123 lisnr4
= SimpleListener(4)
1125 node
.addCallable(lisnr1
)
1126 node
.addCallable(lisnr2
)
1127 node
.addCallable(lisnr3
)
1128 node
.addCallable(lisnr4
)
1130 print 'Deleting listeners first'
1131 _NodeCallback
.notified
= 0
1134 assert _NodeCallback
.notified
== 2
1136 print 'Deleting node first'
1137 _NodeCallback
.notified
= 0
1141 assert _NodeCallback
.notified
== 0
1143 lisnr1
= SimpleListener(1)
1144 lisnr2
= SimpleListener(2)
1145 lisnr3
= SimpleListener(3)
1146 lisnr4
= SimpleListener(4)
1148 # try same with root of tree
1149 node
= _TopicTreeRoot()
1150 node
.addTopic(('',), lisnr1
)
1151 node
.addTopic(('',), lisnr2
)
1152 node
.addTopic(('',), lisnr3
)
1153 node
.addTopic(('',), lisnr4
)
1154 # add objects that will die immediately to see if cleanup occurs
1155 # this must be done visually as it is a low-level detail
1156 _NodeCallback
.notified
= 0
1157 _TopicTreeRoot
.callbackDeadLimit
= 3
1158 node
.addTopic(('',), SimpleListener(5))
1159 node
.addTopic(('',), SimpleListener(6))
1160 node
.addTopic(('',), SimpleListener(7))
1161 print node
.numListeners()
1162 assert node
.numListeners() == (4, 3)
1163 node
.addTopic(('',), SimpleListener(8))
1164 assert node
.numListeners() == (4, 0)
1165 assert _NodeCallback
.notified
== 4
1167 print 'Deleting listeners first'
1168 _NodeCallback
.notified
= 0
1171 assert _NodeCallback
.notified
== 2
1172 print 'Deleting node first'
1173 _NodeCallback
.notified
= 0
1177 assert _NodeCallback
.notified
== 0
1183 print 'Exiting tests'
1184 #---------------------------------------------------------------------------
1186 if __name__
== '__main__':