]>
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, tim 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
, type([])):
94 raise TypeError, 'Not allowed to tuple-ize a list'
95 elif not isinstance(items
, type(())):
100 def _getCallableName(callable):
101 """Get name for a callable, ie function, bound
102 method or callable instance"""
103 if ismethod(callable):
104 return '%s.%s ' % (callable.im_self
, callable.im_func
.func_name
)
105 elif isfunction(callable):
106 return '%s ' % callable.__name
__
108 return '%s ' % callable
111 def _removeItem(item
, fromList
):
112 """Attempt to remove item from fromList, return true
113 if successful, false otherwise."""
115 fromList
.remove(item
)
121 # -----------------------------------------------------------------------------
124 """Represent a weak bound method, i.e. a method doesn't keep alive the
125 object that it is bound to. It uses WeakRef which, used on its own,
126 produces weak methods that are dead on creation, not very useful.
127 Typically, you will use the getRef() function instead of using
128 this class directly. """
130 def __init__(self
, method
, notifyDead
= None):
131 """The method must be bound. notifyDead will be called when
132 object that method is bound to dies. """
133 assert ismethod(method
)
134 if method
.im_self
is None:
135 raise ValueError, "We need a bound method!"
136 if notifyDead
is None:
137 self
.objRef
= WeakRef(method
.im_self
)
139 self
.objRef
= WeakRef(method
.im_self
, notifyDead
)
140 self
.fun
= method
.im_func
141 self
.cls
= method
.im_class
144 """Returns a new.instancemethod if object for method still alive.
145 Otherwise return None. Note that instancemethod causes a
146 strong reference to object to be created, so shouldn't save
147 the return value of this call. Note also that this __call__
148 is required only for compatibility with WeakRef.ref(), otherwise
149 there would be more efficient ways of providing this functionality."""
150 if self
.objRef() is None:
153 return InstanceMethod(self
.fun
, self
.objRef(), self
.cls
)
155 def __cmp__(self
, method2
):
156 """Two _WeakMethod objects compare equal if they refer to the same method
157 of the same instance."""
158 return hash(self
) - hash(method2
)
161 """Hash must depend on WeakRef of object, and on method, so that
162 separate methods, bound to same object, can be distinguished.
163 I'm not sure how robust this hash function is, any feedback
165 return hash(self
.fun
)/2 + hash(self
.objRef
)/2
169 if self
.objRef() is None:
171 obj
= '<%s at %s%s>' % (self
.__class
__, id(self
), dead
)
174 def refs(self
, weakRef
):
175 """Return true if we are storing same object referred to by weakRef."""
176 return self
.objRef
== weakRef
179 def _getWeakRef(obj
, notifyDead
=None):
180 """Get a weak reference to obj. If obj is a bound method, a _WeakMethod
181 object, that behaves like a WeakRef, is returned, if it is
182 anything else a WeakRef is returned. If obj is an unbound method,
183 a ValueError will be raised."""
185 createRef
= _WeakMethod
189 if notifyDead
is None:
190 return createRef(obj
)
192 return createRef(obj
, notifyDead
)
195 # -----------------------------------------------------------------------------
197 def getStrAllTopics():
198 """Function to call if, for whatever reason, you need to know
199 explicitely what is the string to use to indicate 'all topics'."""
203 # alias, easier to see where used
204 ALL_TOPICS
= getStrAllTopics()
206 # -----------------------------------------------------------------------------
210 """Encapsulate a weak reference to a method of a TopicTreeNode
211 in such a way that the method can be called, if the node is
212 still alive, but the callback does not *keep* the node alive.
213 Also, define two methods, preNotify() and noNotify(), which can
214 be redefined to something else, very useful for testing.
217 def __init__(self
, obj
):
218 self
.objRef
= _getWeakRef(obj
)
220 def __call__(self
, weakCB
):
221 notify
= self
.objRef()
222 if notify
is not None:
223 self
.preNotify(weakCB
)
228 def preNotify(self
, dead
):
229 """'Gets called just before our callback (self.objRef) is called"""
233 """Gets called if the TopicTreeNode for this callback is dead"""
237 class _TopicTreeNode
:
238 """A node in the topic tree. This contains a list of callables
239 that are interested in the topic that this node is associated
240 with, and contains a dictionary of subtopics, whose associated
241 values are other _TopicTreeNodes. The topic of a node is not stored
242 in the node, so that the tree can be implemented as a dictionary
243 rather than a list, for ease of use (and, likely, performance).
245 Note that it uses _NodeCallback to encapsulate a callback for
246 when a registered listener dies, possible thanks to WeakRef.
247 Whenever this callback is called, the onDeadListener() function,
248 passed in at construction time, is called (unless it is None).
251 def __init__(self
, topicPath
, onDeadListenerWeakCB
):
252 self
.__subtopics
= {}
253 self
.__callables
= []
254 self
.__topicPath
= topicPath
255 self
.__onDeadListenerWeakCB
= onDeadListenerWeakCB
257 def getPathname(self
):
258 """The complete node path to us, ie., the topic tuple that would lead to us"""
259 return self
.__topicPath
261 def createSubtopic(self
, subtopic
, topicPath
):
262 """Create a child node for subtopic"""
263 return self
.__subtopics
.setdefault(subtopic
,
264 _TopicTreeNode(topicPath
, self
.__onDeadListenerWeakCB
))
266 def hasSubtopic(self
, subtopic
):
267 """Return true only if topic string is one of subtopics of this node"""
268 return self
.__subtopics
.has_key(subtopic
)
270 def getNode(self
, subtopic
):
271 """Return ref to node associated with subtopic"""
272 return self
.__subtopics
[subtopic
]
274 def addCallable(self
, callable):
275 """Add a callable to list of callables for this topic node"""
277 id = self
.__callables
.index(_getWeakRef(callable))
278 return self
.__callables
[id]
280 wrCall
= _getWeakRef(callable, _NodeCallback(self
.__notifyDead
))
281 self
.__callables
.append(wrCall
)
284 def getCallables(self
):
285 """Get callables associated with this topic node"""
286 return [cb() for cb
in self
.__callables
if cb() is not None]
288 def hasCallable(self
, callable):
289 """Return true if callable in this node"""
291 self
.__callables
.index(_getWeakRef(callable))
296 def sendMessage(self
, message
):
297 """Send a message to our callables"""
299 for cb
in self
.__callables
:
301 if listener
is not None:
306 def removeCallable(self
, callable):
307 """Remove weak callable from our node (and return True).
308 Does nothing if not here (and returns False)."""
310 self
.__callables
.remove(_getWeakRef(callable))
315 def clearCallables(self
):
316 """Abandon list of callables to caller. We no longer have
317 any callables after this method is called."""
318 tmpList
= [cb
for cb
in self
.__callables
if cb() is not None]
319 self
.__callables
= []
322 def __notifyDead(self
, dead
):
323 """Gets called when a listener dies, thanks to WeakRef"""
324 #print 'TreeNODE', `self`, 'received death certificate for ', dead
326 if self
.__onDeadListenerWeakCB
is not None:
327 cb
= self
.__onDeadListenerWeakCB
()
331 def __cleanupDead(self
):
332 """Remove all dead objects from list of callables"""
333 self
.__callables
= [cb
for cb
in self
.__callables
if cb() is not None]
336 """Print us in a not-so-friendly, but readable way, good for debugging."""
338 for callable in self
.getCallables():
339 strVal
.append(_getCallableName(callable))
340 for topic
, node
in self
.__subtopics
.iteritems():
341 strVal
.append(' (%s: %s)' %(topic
, node
))
342 return ''.join(strVal
)
345 class _TopicTreeRoot(_TopicTreeNode
):
347 The root of the tree knows how to access other node of the
348 tree and is the gateway of the tree user to the tree nodes.
349 It can create topics, and and remove callbacks, etc.
351 For efficiency, it stores a dictionary of listener-topics,
352 so that unsubscribing a listener just requires finding the
353 topics associated to a listener, and finding the corresponding
354 nodes of the tree. Without it, unsubscribing would require
355 that we search the whole tree for all nodes that contain
356 given listener. Since Publisher is a singleton, it will
357 contain all topics in the system so it is likely to be a large
358 tree. However, it is possible that in some runs, unsubscribe()
359 is called very little by the user, in which case most unsubscriptions
360 are automatic, ie caused by the listeners dying. In this case,
361 a flag is set to indicate that the dictionary should be cleaned up
362 at the next opportunity. This is not necessary, it is just an
367 self
.__callbackDict
= {}
368 self
.__callbackDictCleanup
= 0
369 # all child nodes will call our __rootNotifyDead method
370 # when one of their registered listeners dies
371 _TopicTreeNode
.__init
__(self
, (ALL_TOPICS
,),
372 _getWeakRef(self
.__rootNotifyDead
))
374 def addTopic(self
, topic
, listener
):
375 """Add topic to tree if doesnt exist, and add listener to topic node"""
376 assert isinstance(topic
, tuple)
377 topicNode
= self
.__getTreeNode
(topic
, make
=True)
378 weakCB
= topicNode
.addCallable(listener
)
379 assert topicNode
.hasCallable(listener
)
381 theList
= self
.__callbackDict
.setdefault(weakCB
, [])
382 assert self
.__callbackDict
.has_key(weakCB
)
383 # add it only if we don't already have it
385 weakTopicNode
= WeakRef(topicNode
)
386 theList
.index(weakTopicNode
)
388 theList
.append(weakTopicNode
)
389 assert self
.__callbackDict
[weakCB
].index(weakTopicNode
) >= 0
391 def getTopics(self
, listener
):
392 """Return the list of topics for given listener"""
393 weakNodes
= self
.__callbackDict
.get(_getWeakRef(listener
), [])
394 return [weakNode().getPathname() for weakNode
in weakNodes
395 if weakNode() is not None]
397 def isSubscribed(self
, listener
, topic
=None):
398 """Return true if listener is registered for topic specified.
399 If no topic specified, return true if subscribed to something.
400 Use topic=getStrAllTopics() to determine if a listener will receive
401 messages for all topics."""
402 weakCB
= _getWeakRef(listener
)
404 return self
.__callbackDict
.has_key(weakCB
)
406 topicPath
= _tupleize(topic
)
407 for weakNode
in self
.__callbackDict
[weakCB
]:
408 if topicPath
== weakNode().getPathname():
412 def unsubscribe(self
, listener
, topicList
):
413 """Remove listener from given list of topics. If topicList
414 doesn't have any topics for which listener has subscribed,
415 the onNotSubscribed callback, if not None, will be called,
416 as onNotSubscribed(listener, topic)."""
417 weakCB
= _getWeakRef(listener
)
418 if not self
.__callbackDict
.has_key(weakCB
):
421 cbNodes
= self
.__callbackDict
[weakCB
]
422 if topicList
is None:
423 for weakNode
in cbNodes
:
424 weakNode().removeCallable(listener
)
425 del self
.__callbackDict
[weakCB
]
428 for weakNode
in cbNodes
:
430 if node
is not None and node
.getPathname() in topicList
:
431 success
= node
.removeCallable(listener
)
432 assert success
== True
433 cbNodes
.remove(weakNode
)
434 assert not self
.isSubscribed(listener
, node
.getPathname())
436 def unsubAll(self
, topicList
, onNoSuchTopic
):
437 """Unsubscribe all listeners registered for any topic in
438 topicList. If a topic in the list does not exist, and
439 onNoSuchTopic is not None, a call
440 to onNoSuchTopic(topic) is done for that topic."""
441 for topic
in topicList
:
442 node
= self
.__getTreeNode
(topic
)
444 weakCallables
= node
.clearCallables()
445 for callable in weakCallables
:
446 weakNodes
= self
.__callbackDict
[callable]
447 success
= _removeItem(WeakRef(node
), weakNodes
)
448 assert success
== True
450 del self
.__callbackDict
[callable]
451 elif onNoSuchTopic
is not None:
454 def sendMessage(self
, topic
, message
, onTopicNeverCreated
):
455 """Send a message for given topic to all registered listeners. If
456 topic doesn't exist, call onTopicNeverCreated(topic)."""
457 # send to the all-toipcs listeners
458 deliveryCount
= _TopicTreeNode
.sendMessage(self
, message
)
459 # send to those who listen to given topic or any of its supertopics
461 for topicItem
in topic
:
462 assert topicItem
!= ''
463 if node
.hasSubtopic(topicItem
):
464 node
= node
.getNode(topicItem
)
465 deliveryCount
+= node
.sendMessage(message
)
466 else: # topic never created, don't bother continuing
467 if onTopicNeverCreated
is not None:
468 onTopicNeverCreated(aTopic
)
472 def numListeners(self
):
473 """Return a pair (live, dead) with count of live and dead listeners in tree"""
475 for cb
in self
.__callbackDict
:
482 # clean up the callback dictionary after how many dead listeners
483 callbackDeadLimit
= 10
485 def __rootNotifyDead(self
, dead
):
486 #print 'TreeROOT received death certificate for ', dead
487 self
.__callbackDictCleanup
+= 1
488 if self
.__callbackDictCleanup
> _TopicTreeRoot
.callbackDeadLimit
:
489 self
.__callbackDictCleanup
= 0
490 oldDict
= self
.__callbackDict
491 self
.__callbackDict
= {}
492 for weakCB
, weakNodes
in oldDict
.iteritems():
493 if weakCB() is not None:
494 self
.__callbackDict
[weakCB
] = weakNodes
496 def __getTreeNode(self
, topic
, make
=False):
497 """Return the tree node for 'topic' from the topic tree. If it
498 doesnt exist and make=True, create it first."""
499 # if the all-topics, give root;
500 if topic
== (ALL_TOPICS
,):
503 # not root, so traverse tree
506 for topicItem
in topic
:
508 if topicItem
== ALL_TOPICS
:
509 raise ValueError, 'Topic tuple must not contain ""'
511 node
= node
.createSubtopic(topicItem
, path
)
512 elif node
.hasSubtopic(topicItem
):
513 node
= node
.getNode(topicItem
)
519 def printCallbacks(self
):
520 strVal
= ['Callbacks:\n']
521 for listener
, weakTopicNodes
in self
.__callbackDict
.iteritems():
522 topics
= [topic() for topic
in weakTopicNodes
if topic() is not None]
523 strVal
.append(' %s: %s\n' % (_getCallableName(listener()), topics
))
524 return ''.join(strVal
)
527 return 'all: %s' % _TopicTreeNode
.__str
__(self
)
530 # -----------------------------------------------------------------------------
534 The publish/subscribe manager. It keeps track of which listeners
535 are interested in which topics (see subscribe()), and sends a
536 Message for a given topic to listeners that have subscribed to
537 that topic, with optional user data (see sendMessage()).
539 The three important concepts for Publisher are:
541 - listener: a function, bound method or
542 callable object that can be called with only one parameter
543 (not counting 'self' in the case of methods). The parameter
544 will be a reference to a Message object. E.g., these listeners
548 def __call__(self, a, b=1): pass # can be called with only one arg
549 def meth(self, a): pass # takes only one arg
550 def meth2(self, a=2, b=''): pass # can be called with one arg
552 def func(a, b=''): pass
555 Publisher().subscribe(foo) # functor
556 Publisher().subscribe(foo.meth) # bound method
557 Publisher().subscribe(foo.meth2) # bound method
558 Publisher().subscribe(func) # function
560 The three types of callables all have arguments that allow a call
561 with only one argument. In every case, the parameter 'a' will contain
564 - topic: a single word or tuple of words (though word could probably
565 be any kind of object, not just a string, but this has not been
566 tested). A tuple denotes a hierarchy of topics from most general
567 to least. For example, a listener of this topic::
569 ('sports','baseball')
571 would receive messages for these topics::
573 ('sports', 'baseball') # because same
574 ('sports', 'baseball', 'highscores') # because more specific
578 'sports' # because more general
579 ('sports',) # because more general
580 () or ('') # because only for those listening to 'all' topics
581 ('news') # because different topic
583 - message: this is an instance of Message, containing the topic for
584 which the message was sent, and any data the sender specified.
586 :note: This class is visible to importers of pubsub only as a
587 Singleton. I.e., every time you execute 'Publisher()', it's
588 actually the same instance of publisher that is returned. So to
589 use, just do 'Publisher().method()'.
592 __ALL_TOPICS_TPL
= (ALL_TOPICS
, )
595 self
.__messageCount
= 0
596 self
.__deliveryCount
= 0
597 self
.__topicTree
= _TopicTreeRoot()
603 def getDeliveryCount(self
):
604 """How many listeners have received a message since beginning of run"""
605 return self
.__deliveryCount
607 def getMessageCount(self
):
608 """How many times sendMessage() was called since beginning of run"""
609 return self
.__messageCount
611 def subscribe(self
, listener
, topic
= ALL_TOPICS
):
613 Subscribe listener for given topic. If topic is not specified,
614 listener will be subscribed for all topics (that listener will
615 receive a Message for any topic for which a message is generated).
618 called multiple times for one listener, registering it with
619 many topics. It can also be invoked many times for a
620 particular topic, each time with a different listener.
621 See the class doc for requirements on listener and topic.
624 this method for the same listener, with two topics in the same
625 branch of the topic hierarchy, will cause the listener to be
626 notified twice when a message for the deepest topic is sent. E.g.
627 subscribe(listener, 't1') and then subscribe(listener, ('t1','t2'))
628 means that when calling sendMessage('t1'), listener gets one message,
629 but when calling sendMessage(('t1','t2')), listener gets message
630 twice. This effect could be eliminated but it would not be safe to
631 do so: how do we know what topic to give the listener? Answer appears
632 trivial at first but is far from obvious. It is best to rely on the
633 user to be careful about who registers for what topics.
635 self
.validate(listener
)
638 raise TypeError, 'Topic must be either a word, tuple of '\
639 'words, or getStrAllTopics()'
641 self
.__topicTree
.addTopic(_tupleize(topic
), listener
)
643 def isSubscribed(self
, listener
, topic
=None):
644 """Return true if listener has subscribed to topic specified.
645 If no topic specified, return true if subscribed to something.
646 Use getStrAllTopics() to determine if a listener will receive
647 messages for all topics."""
648 return self
.__topicTree
.isSubscribed(listener
, topic
)
650 def validate(self
, listener
):
651 """Similar to isValid(), but raises a TypeError exception if not valid"""
653 if not callable(listener
):
654 raise TypeError, 'Listener '+`listener`
+' must be a '\
655 'function, bound method or instance.'
656 # ok, callable, but if method, is it bound:
657 elif ismethod(listener
) and not _isbound(listener
):
658 raise TypeError, 'Listener '+`listener`
+\
659 ' is a method but it is unbound!'
661 # check that it takes the right number of parameters
662 min, d
= _paramMinCount(listener
)
664 raise TypeError, 'Listener '+`listener`
+" can't"\
665 ' require more than one parameter!'
666 if min <= 0 and d
== 0:
667 raise TypeError, 'Listener '+`listener`
+' lacking arguments!'
669 assert (min == 0 and d
>0) or (min == 1)
671 def isValid(self
, listener
):
672 """Return true only if listener will be able to subscribe to Publisher."""
674 self
.validate(listener
)
679 def unsubAll(self
, topics
=None, onNoSuchTopic
=None):
680 """Unsubscribe all listeners subscribed for topics. Topics can
681 be a single topic (string or tuple) or a list of topics (ie
682 list containing strings and/or tuples). If topics is not
683 specified, all listeners for all topics will be unsubscribed,
684 ie. the Publisher singleton will have no topics and no listeners
685 left. If topics was specified and is not found among contained
686 topics, the onNoSuchTopic, if specified, will be called, with
687 the name of the topic."""
690 self
.__topicTree
= _TopicTreeRoot()
693 # make sure every topics are in tuple form
694 if isinstance(topics
, type([])):
695 topicList
= [_tupleize(x
) for x
in topics
]
697 topicList
= [_tupleize(topics
)]
699 # unsub every listener of topics
700 self
.__topicTree
.unsubAll(topicList
, onNoSuchTopic
)
702 def unsubscribe(self
, listener
, topics
=None):
703 """Unsubscribe listener. If topics not specified, listener is
704 completely unsubscribed. Otherwise, it is unsubscribed only
705 for the topic (the usual tuple) or list of topics (ie a list
706 of tuples) specified. In this case, if listener is not actually
707 subscribed for (one of) the topics, the optional onNotSubscribed
708 callback will be called, as onNotSubscribed(listener, missingTopic).
710 Note that if listener subscribed for two topics (a,b) and (a,c),
711 then unsubscribing for topic (a) will do nothing. You must
712 use getAssociatedTopics(listener) and give unsubscribe() the returned
713 list (or a subset thereof).
715 self
.validate(listener
)
717 if topics
is not None:
718 if isinstance(topics
, list):
719 topicList
= [_tupleize(x
) for x
in topics
]
721 topicList
= [_tupleize(topics
)]
723 self
.__topicTree
.unsubscribe(listener
, topicList
)
725 def getAssociatedTopics(self
, listener
):
726 """Return a list of topics the given listener is registered with.
727 Returns [] if listener never subscribed.
729 :attention: when using the return of this method to compare to
730 expected list of topics, remember that topics that are not in the
731 form of a tuple appear as a one-tuple in the return. E.g. if you
732 have subscribed a listener to 'topic1' and ('topic2','subtopic2'),
733 this method returns::
735 associatedTopics = [('topic1',), ('topic2','subtopic2')]
737 return self
.__topicTree
.getTopics(listener
)
739 def sendMessage(self
, topic
=ALL_TOPICS
, data
=None, onTopicNeverCreated
=None):
740 """Send a message for given topic, with optional data, to
741 subscribed listeners. If topic is not specified, only the
742 listeners that are interested in all topics will receive
743 message. The onTopicNeverCreated is an optional callback of
744 your choice that will be called if the topic given was never
745 created (i.e. it, or one of its subtopics, was never
746 subscribed to). The callback must be of the form f(a)."""
747 aTopic
= _tupleize(topic
)
748 message
= Message(aTopic
, data
)
749 self
.__messageCount
+= 1
751 # send to those who listen to all topics
752 self
.__deliveryCount
+= \
753 self
.__topicTree
.sendMessage(aTopic
, message
, onTopicNeverCreated
)
760 """Allows for singleton"""
764 return str(self
.__topicTree
)
766 # Create an instance with the same name as the class, effectivly
767 # hiding the class object so it can't be instantiated any more. From
768 # this point forward any calls to Publisher() will invoke the __call__
769 # of this instance which just returns itself.
771 # The only flaw with this approach is that you can't derive a new
772 # class from Publisher without jumping through hoops. If this ever
773 # becomes an issue then a new Singleton implementaion will need to be
775 Publisher
= Publisher()
778 #---------------------------------------------------------------------------
782 A simple container object for the two components of
783 a message; the topic and the user data.
785 def __init__(self
, topic
, data
):
790 return '[Topic: '+`self
.topic`
+', Data: '+`self
.data`
+']'
793 #---------------------------------------------------------------------------
797 # Code for a simple command-line test
801 print '----------- Done %s -----------' % funcName
804 def testFunc(a
,b
,c
=1): pass
806 def testMeth(self
,a
,b
): pass
807 def __call__(self
, a
): pass
810 assert _paramMinCount(testFunc
)==(2,1)
811 assert _paramMinCount(Foo
.testMeth
)==(2,0)
812 assert _paramMinCount(foo
.testMeth
)==(2,0)
813 assert _paramMinCount(foo
)==(1,0)
818 #------------------------
820 _NodeCallback
.notified
= 0
821 def testPreNotifyNode(self
, dead
):
822 _NodeCallback
.notified
+= 1
823 print 'testPreNotifyNODE heard notification of', `dead`
824 _NodeCallback
.preNotify
= testPreNotifyNode
829 def __init__(self
, s
):
831 def __call__(self
, msg
):
832 print 'WS#', self
.s
, ' received msg ', msg
836 def testPreNotifyRoot(dead
):
837 print 'testPreNotifyROOT heard notification of', `dead`
839 node
= _TopicTreeNode((ALL_TOPICS
,), WeakRef(testPreNotifyRoot
))
840 boo
, baz
, bid
= WS('boo'), WS('baz'), WS('bid')
841 node
.addCallable(boo
)
842 node
.addCallable(baz
)
843 node
.addCallable(boo
)
844 assert node
.getCallables() == [boo
,baz
]
845 assert node
.hasCallable(boo
)
847 node
.removeCallable(bid
) # no-op
848 assert node
.hasCallable(baz
)
849 assert node
.getCallables() == [boo
,baz
]
851 node
.removeCallable(boo
)
852 assert node
.getCallables() == [baz
]
853 assert node
.hasCallable(baz
)
854 assert not node
.hasCallable(boo
)
856 node
.removeCallable(baz
)
857 assert node
.getCallables() == []
858 assert not node
.hasCallable(baz
)
860 node2
= node
.createSubtopic('st1', ('st1',))
861 node3
= node
.createSubtopic('st2', ('st2',))
862 cb1
, cb2
, cb
= WS('st1_cb1'), WS('st1_cb2'), WS('st2_cb')
863 node2
.addCallable(cb1
)
864 node2
.addCallable(cb2
)
865 node3
.addCallable(cb
)
866 node2
.createSubtopic('st3', ('st1','st3'))
867 node2
.createSubtopic('st4', ('st1','st4'))
870 assert str(node
) == ' (st1: st1_cb1 st1_cb2 (st4: ) (st3: )) (st2: st2_cb )'
872 # verify send message, and that a dead listener does not get sent one
873 delivered
= node2
.sendMessage('hello')
874 assert delivered
== 2
876 delivered
= node2
.sendMessage('hello')
877 assert delivered
== 1
878 assert _NodeCallback
.notified
== 1
883 #------------------------
887 def __call__(self
, a
): pass
888 def fun(self
, b
): pass
889 def fun2(self
, b
=1): pass
890 def fun3(self
, a
, b
=2): pass
891 def badFun(self
): pass
893 def badFun3(self
, a
, b
): pass
898 server
.validate(foo
.fun
)
899 server
.validate(foo
.fun2
)
900 server
.validate(foo
.fun3
)
901 assert not server
.isValid(foo
.badFun
)
902 assert not server
.isValid(foo
.badFun2
)
903 assert not server
.isValid(foo
.badFun3
)
908 #------------------------
910 class SimpleListener
:
911 def __init__(self
, number
):
913 def __call__(self
, message
= ''):
914 print 'Callable #%s got the message "%s"' %(self
.number
, message
)
915 def notify(self
, message
):
916 print '%s.notify() got the message "%s"' %(self
.number
, message
)
918 return "SimpleListener_%s" % self
.number
921 publisher
= Publisher()
924 topic2
= ('history','middle age')
925 topic3
= ('politics','UN')
926 topic4
= ('politics','NATO')
927 topic5
= ('politics','NATO','US')
929 lisnr1
= SimpleListener(1)
930 lisnr2
= SimpleListener(2)
931 def func(message
, a
=1):
932 print 'Func received message "%s"' % message
934 lisnr4
= lambda x
: 'Lambda received message "%s"' % x
936 assert not publisher
.isSubscribed(lisnr1
)
937 assert not publisher
.isSubscribed(lisnr2
)
938 assert not publisher
.isSubscribed(lisnr3
)
939 assert not publisher
.isSubscribed(lisnr4
)
941 publisher
.subscribe(lisnr1
, topic1
)
942 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,)]
943 publisher
.subscribe(lisnr1
, topic2
)
944 publisher
.subscribe(lisnr1
, topic1
) # do it again, should be no-op
945 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,),topic2
]
946 publisher
.subscribe(lisnr2
.notify
, topic3
)
947 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == [topic3
]
948 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,),topic2
]
949 publisher
.subscribe(lisnr3
, topic5
)
950 assert publisher
.getAssociatedTopics(lisnr3
) == [topic5
]
951 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == [topic3
]
952 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,),topic2
]
953 publisher
.subscribe(lisnr4
)
955 print "Publisher tree: ", publisher
956 assert publisher
.isSubscribed(lisnr1
)
957 assert publisher
.isSubscribed(lisnr1
, topic1
)
958 assert publisher
.isSubscribed(lisnr1
, topic2
)
959 assert publisher
.isSubscribed(lisnr2
.notify
)
960 assert publisher
.isSubscribed(lisnr3
, topic5
)
961 assert publisher
.isSubscribed(lisnr4
, ALL_TOPICS
)
962 expectTopicTree
= 'all: <lambda> (politics: SimpleListener_1 (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: SimpleListener_1 ))'
963 print "Publisher tree: ", publisher
964 assert str(publisher
) == expectTopicTree
966 publisher
.unsubscribe(lisnr1
, 'booboo') # should do nothing
967 assert publisher
.getAssociatedTopics(lisnr1
) == [(topic1
,),topic2
]
968 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == [topic3
]
969 assert publisher
.getAssociatedTopics(lisnr3
) == [topic5
]
970 publisher
.unsubscribe(lisnr1
, topic1
)
971 assert publisher
.getAssociatedTopics(lisnr1
) == [topic2
]
972 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == [topic3
]
973 assert publisher
.getAssociatedTopics(lisnr3
) == [topic5
]
974 publisher
.unsubscribe(lisnr1
, topic2
)
975 publisher
.unsubscribe(lisnr1
, topic2
)
976 publisher
.unsubscribe(lisnr2
.notify
, topic3
)
977 publisher
.unsubscribe(lisnr3
, topic5
)
978 assert publisher
.getAssociatedTopics(lisnr1
) == []
979 assert publisher
.getAssociatedTopics(lisnr2
.notify
) == []
980 assert publisher
.getAssociatedTopics(lisnr3
) == []
981 publisher
.unsubscribe(lisnr4
)
983 expectTopicTree
= 'all: (politics: (UN: ) (NATO: (US: ))) (history: (middle age: ))'
984 print "Publisher tree: ", publisher
985 assert str(publisher
) == expectTopicTree
986 assert publisher
.getDeliveryCount() == 0
987 assert publisher
.getMessageCount() == 0
990 assert str(publisher
) == 'all: '
992 done('testSubscribe')
995 #------------------------
998 publisher
= Publisher()
1001 topic2
= ('history','middle age')
1002 topic3
= ('politics','UN')
1003 topic4
= ('politics','NATO')
1004 topic5
= ('politics','NATO','US')
1006 lisnr1
= SimpleListener(1)
1007 lisnr2
= SimpleListener(2)
1008 def func(message
, a
=1):
1009 print 'Func received message "%s"' % message
1011 lisnr4
= lambda x
: 'Lambda received message "%s"' % x
1013 publisher
.subscribe(lisnr1
, topic1
)
1014 publisher
.subscribe(lisnr1
, topic2
)
1015 publisher
.subscribe(lisnr2
.notify
, topic3
)
1016 publisher
.subscribe(lisnr3
, topic2
)
1017 publisher
.subscribe(lisnr3
, topic5
)
1018 publisher
.subscribe(lisnr4
)
1020 expectTopicTree
= 'all: <lambda> (politics: SimpleListener_1 (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: SimpleListener_1 func ))'
1021 print "Publisher tree: ", publisher
1022 assert str(publisher
) == expectTopicTree
1024 publisher
.unsubAll(topic1
)
1025 assert publisher
.getAssociatedTopics(lisnr1
) == [topic2
]
1026 assert not publisher
.isSubscribed(lisnr1
, topic1
)
1028 publisher
.unsubAll(topic2
)
1030 assert publisher
.getAssociatedTopics(lisnr1
) == []
1031 assert publisher
.getAssociatedTopics(lisnr3
) == [topic5
]
1032 assert not publisher
.isSubscribed(lisnr1
)
1033 assert publisher
.isSubscribed(lisnr3
, topic5
)
1035 #print "Publisher tree: ", publisher
1036 expectTopicTree
= 'all: <lambda> (politics: (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: ))'
1037 assert str(publisher
) == expectTopicTree
1038 publisher
.unsubAll(ALL_TOPICS
)
1039 #print "Publisher tree: ", publisher
1040 expectTopicTree
= 'all: (politics: (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: ))'
1041 assert str(publisher
) == expectTopicTree
1043 publisher
.unsubAll()
1044 done('testUnsubAll')
1047 #------------------------
1050 publisher
= Publisher()
1054 def __init__(self
, num
):
1056 def __call__(self
, b
):
1057 called
.append( 'TL%scb' % self
.number
)
1058 def notify(self
, b
):
1059 called
.append( 'TL%sm' % self
.number
)
1060 def funcListener(b
):
1061 called
.append('func')
1063 lisnr1
= TestListener(1)
1064 lisnr2
= TestListener(2)
1065 lisnr3
= funcListener
1066 lisnr4
= lambda x
: called
.append('lambda')
1070 topic3
= ('politics','UN')
1071 topic4
= ('politics','NATO','US')
1072 topic5
= ('politics','NATO')
1074 publisher
.subscribe(lisnr1
, topic1
)
1075 publisher
.subscribe(lisnr2
, topic2
)
1076 publisher
.subscribe(lisnr2
.notify
, topic2
)
1077 publisher
.subscribe(lisnr3
, topic4
)
1078 publisher
.subscribe(lisnr4
)
1082 # setup ok, now test send/receipt
1083 publisher
.sendMessage(topic1
)
1084 assert called
== ['lambda','TL1cb']
1086 publisher
.sendMessage(topic2
)
1087 assert called
== ['lambda','TL2cb','TL2m']
1089 publisher
.sendMessage(topic3
)
1090 assert called
== ['lambda','TL1cb']
1092 publisher
.sendMessage(topic4
)
1093 assert called
== ['lambda','TL1cb','func']
1095 publisher
.sendMessage(topic5
)
1096 assert called
== ['lambda','TL1cb']
1097 assert publisher
.getDeliveryCount() == 12
1098 assert publisher
.getMessageCount() == 5
1100 # test weak referencing works:
1101 _NodeCallback
.notified
= 0
1104 publisher
.sendMessage(topic2
)
1105 assert called
== ['lambda']
1106 assert _NodeCallback
.notified
== 2
1111 assert _NodeCallback
.notified
== 5
1114 # verify if weak references work as expected
1115 print '------ Starting testDead ----------'
1116 node
= _TopicTreeNode('t1', None)
1117 lisnr1
= SimpleListener(1)
1118 lisnr2
= SimpleListener(2)
1119 lisnr3
= SimpleListener(3)
1120 lisnr4
= SimpleListener(4)
1122 node
.addCallable(lisnr1
)
1123 node
.addCallable(lisnr2
)
1124 node
.addCallable(lisnr3
)
1125 node
.addCallable(lisnr4
)
1127 print 'Deleting listeners first'
1128 _NodeCallback
.notified
= 0
1131 assert _NodeCallback
.notified
== 2
1133 print 'Deleting node first'
1134 _NodeCallback
.notified
= 0
1138 assert _NodeCallback
.notified
== 0
1140 lisnr1
= SimpleListener(1)
1141 lisnr2
= SimpleListener(2)
1142 lisnr3
= SimpleListener(3)
1143 lisnr4
= SimpleListener(4)
1145 # try same with root of tree
1146 node
= _TopicTreeRoot()
1147 node
.addTopic(('',), lisnr1
)
1148 node
.addTopic(('',), lisnr2
)
1149 node
.addTopic(('',), lisnr3
)
1150 node
.addTopic(('',), lisnr4
)
1151 # add objects that will die immediately to see if cleanup occurs
1152 # this must be done visually as it is a low-level detail
1153 _NodeCallback
.notified
= 0
1154 _TopicTreeRoot
.callbackDeadLimit
= 3
1155 node
.addTopic(('',), SimpleListener(5))
1156 node
.addTopic(('',), SimpleListener(6))
1157 node
.addTopic(('',), SimpleListener(7))
1158 print node
.numListeners()
1159 assert node
.numListeners() == (4, 3)
1160 node
.addTopic(('',), SimpleListener(8))
1161 assert node
.numListeners() == (4, 0)
1162 assert _NodeCallback
.notified
== 4
1164 print 'Deleting listeners first'
1165 _NodeCallback
.notified
= 0
1168 assert _NodeCallback
.notified
== 2
1169 print 'Deleting node first'
1170 _NodeCallback
.notified
= 0
1174 assert _NodeCallback
.notified
== 0
1180 print 'Exiting tests'
1181 #---------------------------------------------------------------------------
1183 if __name__
== '__main__':