]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/pubsub.py
Don't scroll too far if the child getting the focus is large.
[wxWidgets.git] / wxPython / wx / lib / pubsub.py
1
2 #---------------------------------------------------------------------------
3 """
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
10 details.
11
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
15 wxPython.lib.pubsub).
16
17 :Author: Oliver Schoenborn
18 :Since: Apr 2004
19 :Version: $Id$
20 :Copyright: \(c) 2004 Oliver Schoenborn
21 :License: wxWidgets
22
23 Implementation notes
24 --------------------
25
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.
32
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
36 listener from itself.
37
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.
43
44 """
45 #---------------------------------------------------------------------------
46
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
53
54 # -----------------------------------------------------------------------------
55
56 def _isbound(method):
57 """Return true if method is a bound method, false otherwise"""
58 assert ismethod(method)
59 return method.im_self is not None
60
61
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)
69
70
71 def _paramMinCount(callableObject):
72 """
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.
77 """
78 if type(callableObject) is InstanceType:
79 min, d = _paramMinCountFunc(callableObject.__call__.im_func)
80 return min-1, d
81 elif ismethod(callableObject):
82 min, d = _paramMinCountFunc(callableObject.im_func)
83 return min-1, d
84 elif isfunction(callableObject):
85 return _paramMinCountFunc(callableObject)
86 else:
87 raise 'Cannot determine type of callable: '+repr(callableObject)
88
89
90 def _tupleize(items):
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(())):
96 items = (items,)
97 return items
98
99
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__
107 else:
108 return '%s ' % callable
109
110
111 def _removeItem(item, fromList):
112 """Attempt to remove item from fromList, return true
113 if successful, false otherwise."""
114 try:
115 fromList.remove(item)
116 return True
117 except ValueError:
118 return False
119
120
121 # -----------------------------------------------------------------------------
122
123 class _WeakMethod:
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. """
129
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)
138 else:
139 self.objRef = WeakRef(method.im_self, notifyDead)
140 self.fun = method.im_func
141 self.cls = method.im_class
142
143 def __call__(self):
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:
151 return None
152 else:
153 return InstanceMethod(self.fun, self.objRef(), self.cls)
154
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)
159
160 def __hash__(self):
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
164 welcome."""
165 return hash(self.fun)/2 + hash(self.objRef)/2
166
167 def __repr__(self):
168 dead = ''
169 if self.objRef() is None:
170 dead = '; DEAD'
171 obj = '<%s at %s%s>' % (self.__class__, id(self), dead)
172 return obj
173
174 def refs(self, weakRef):
175 """Return true if we are storing same object referred to by weakRef."""
176 return self.objRef == weakRef
177
178
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."""
184 if ismethod(obj):
185 createRef = _WeakMethod
186 else:
187 createRef = WeakRef
188
189 if notifyDead is None:
190 return createRef(obj)
191 else:
192 return createRef(obj, notifyDead)
193
194
195 # -----------------------------------------------------------------------------
196
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'."""
200 return ''
201
202
203 # alias, easier to see where used
204 ALL_TOPICS = getStrAllTopics()
205
206 # -----------------------------------------------------------------------------
207
208
209 class _NodeCallback:
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.
215 """
216
217 def __init__(self, obj):
218 self.objRef = _getWeakRef(obj)
219
220 def __call__(self, weakCB):
221 notify = self.objRef()
222 if notify is not None:
223 self.preNotify(weakCB)
224 notify(weakCB)
225 else:
226 self.noNotify()
227
228 def preNotify(self, dead):
229 """'Gets called just before our callback (self.objRef) is called"""
230 pass
231
232 def noNotify(self):
233 """Gets called if the TopicTreeNode for this callback is dead"""
234 pass
235
236
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).
244
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).
249 """
250
251 def __init__(self, topicPath, onDeadListenerWeakCB):
252 self.__subtopics = {}
253 self.__callables = []
254 self.__topicPath = topicPath
255 self.__onDeadListenerWeakCB = onDeadListenerWeakCB
256
257 def getPathname(self):
258 """The complete node path to us, ie., the topic tuple that would lead to us"""
259 return self.__topicPath
260
261 def createSubtopic(self, subtopic, topicPath):
262 """Create a child node for subtopic"""
263 return self.__subtopics.setdefault(subtopic,
264 _TopicTreeNode(topicPath, self.__onDeadListenerWeakCB))
265
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)
269
270 def getNode(self, subtopic):
271 """Return ref to node associated with subtopic"""
272 return self.__subtopics[subtopic]
273
274 def addCallable(self, callable):
275 """Add a callable to list of callables for this topic node"""
276 try:
277 id = self.__callables.index(_getWeakRef(callable))
278 return self.__callables[id]
279 except ValueError:
280 wrCall = _getWeakRef(callable, _NodeCallback(self.__notifyDead))
281 self.__callables.append(wrCall)
282 return wrCall
283
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]
287
288 def hasCallable(self, callable):
289 """Return true if callable in this node"""
290 try:
291 self.__callables.index(_getWeakRef(callable))
292 return True
293 except ValueError:
294 return False
295
296 def sendMessage(self, message):
297 """Send a message to our callables"""
298 deliveryCount = 0
299 for cb in self.__callables:
300 listener = cb()
301 if listener is not None:
302 listener(message)
303 deliveryCount += 1
304 return deliveryCount
305
306 def removeCallable(self, callable):
307 """Remove weak callable from our node (and return True).
308 Does nothing if not here (and returns False)."""
309 try:
310 self.__callables.remove(_getWeakRef(callable))
311 return True
312 except ValueError:
313 return False
314
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 = []
320 return tmpList
321
322 def __notifyDead(self, dead):
323 """Gets called when a listener dies, thanks to WeakRef"""
324 #print 'TreeNODE', `self`, 'received death certificate for ', dead
325 self.__cleanupDead()
326 if self.__onDeadListenerWeakCB is not None:
327 cb = self.__onDeadListenerWeakCB()
328 if cb is not None:
329 cb(dead)
330
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]
334
335 def __str__(self):
336 """Print us in a not-so-friendly, but readable way, good for debugging."""
337 strVal = []
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)
343
344
345 class _TopicTreeRoot(_TopicTreeNode):
346 """
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.
350
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
363 optimization.
364 """
365
366 def __init__(self):
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))
373
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)
380
381 theList = self.__callbackDict.setdefault(weakCB, [])
382 assert self.__callbackDict.has_key(weakCB)
383 # add it only if we don't already have it
384 try:
385 weakTopicNode = WeakRef(topicNode)
386 theList.index(weakTopicNode)
387 except ValueError:
388 theList.append(weakTopicNode)
389 assert self.__callbackDict[weakCB].index(weakTopicNode) >= 0
390
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]
396
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)
403 if topic is None:
404 return self.__callbackDict.has_key(weakCB)
405 else:
406 topicPath = _tupleize(topic)
407 for weakNode in self.__callbackDict[weakCB]:
408 if topicPath == weakNode().getPathname():
409 return True
410 return False
411
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):
419 return
420
421 cbNodes = self.__callbackDict[weakCB]
422 if topicList is None:
423 for weakNode in cbNodes:
424 weakNode().removeCallable(listener)
425 del self.__callbackDict[weakCB]
426 return
427
428 for weakNode in cbNodes:
429 node = weakNode()
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())
435
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)
443 if node is not None:
444 weakCallables = node.clearCallables()
445 for callable in weakCallables:
446 weakNodes = self.__callbackDict[callable]
447 success = _removeItem(WeakRef(node), weakNodes)
448 assert success == True
449 if weakNodes == []:
450 del self.__callbackDict[callable]
451 elif onNoSuchTopic is not None:
452 onNoSuchTopic(topic)
453
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
460 node = self
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)
469 break
470 return deliveryCount
471
472 def numListeners(self):
473 """Return a pair (live, dead) with count of live and dead listeners in tree"""
474 dead, live = 0, 0
475 for cb in self.__callbackDict:
476 if cb() is None:
477 dead += 1
478 else:
479 live += 1
480 return live, dead
481
482 # clean up the callback dictionary after how many dead listeners
483 callbackDeadLimit = 10
484
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
495
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,):
501 return self
502
503 # not root, so traverse tree
504 node = self
505 path = ()
506 for topicItem in topic:
507 path += (topicItem,)
508 if topicItem == ALL_TOPICS:
509 raise ValueError, 'Topic tuple must not contain ""'
510 if make:
511 node = node.createSubtopic(topicItem, path)
512 elif node.hasSubtopic(topicItem):
513 node = node.getNode(topicItem)
514 else:
515 return None
516 # done
517 return node
518
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)
525
526 def __str__(self):
527 return 'all: %s' % _TopicTreeNode.__str__(self)
528
529
530 # -----------------------------------------------------------------------------
531
532 class Publisher:
533 """
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()).
538
539 The three important concepts for Publisher are:
540
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
545 are ok::
546
547 class Foo:
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
551
552 def func(a, b=''): pass
553
554 Foo foo
555 Publisher().subscribe(foo) # functor
556 Publisher().subscribe(foo.meth) # bound method
557 Publisher().subscribe(foo.meth2) # bound method
558 Publisher().subscribe(func) # function
559
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
562 the message.
563
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::
568
569 ('sports','baseball')
570
571 would receive messages for these topics::
572
573 ('sports', 'baseball') # because same
574 ('sports', 'baseball', 'highscores') # because more specific
575
576 but not these::
577
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
582
583 - message: this is an instance of Message, containing the topic for
584 which the message was sent, and any data the sender specified.
585
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()'.
590 """
591
592 __ALL_TOPICS_TPL = (ALL_TOPICS, )
593
594 def __init__(self):
595 self.__messageCount = 0
596 self.__deliveryCount = 0
597 self.__topicTree = _TopicTreeRoot()
598
599 #
600 # Public API
601 #
602
603 def getDeliveryCount(self):
604 """How many listeners have received a message since beginning of run"""
605 return self.__deliveryCount
606
607 def getMessageCount(self):
608 """How many times sendMessage() was called since beginning of run"""
609 return self.__messageCount
610
611 def subscribe(self, listener, topic = ALL_TOPICS):
612 """
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).
616
617 This method may be
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.
622
623 :note: Calling
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.
634 """
635 self.validate(listener)
636
637 if topic is None:
638 raise TypeError, 'Topic must be either a word, tuple of '\
639 'words, or getStrAllTopics()'
640
641 self.__topicTree.addTopic(_tupleize(topic), listener)
642
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)
649
650 def validate(self, listener):
651 """Similar to isValid(), but raises a TypeError exception if not valid"""
652 # check callable
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!'
660
661 # check that it takes the right number of parameters
662 min, d = _paramMinCount(listener)
663 if min > 1:
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!'
668
669 assert (min == 0 and d>0) or (min == 1)
670
671 def isValid(self, listener):
672 """Return true only if listener will be able to subscribe to Publisher."""
673 try:
674 self.validate(listener)
675 return True
676 except TypeError:
677 return False
678
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."""
688 if topics is None:
689 del self.__topicTree
690 self.__topicTree = _TopicTreeRoot()
691 return
692
693 # make sure every topics are in tuple form
694 if isinstance(topics, type([])):
695 topicList = [_tupleize(x) for x in topics]
696 else:
697 topicList = [_tupleize(topics)]
698
699 # unsub every listener of topics
700 self.__topicTree.unsubAll(topicList, onNoSuchTopic)
701
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).
709
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).
714 """
715 self.validate(listener)
716 topicList = None
717 if topics is not None:
718 if isinstance(topics, list):
719 topicList = [_tupleize(x) for x in topics]
720 else:
721 topicList = [_tupleize(topics)]
722
723 self.__topicTree.unsubscribe(listener, topicList)
724
725 def getAssociatedTopics(self, listener):
726 """Return a list of topics the given listener is registered with.
727 Returns [] if listener never subscribed.
728
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::
734
735 associatedTopics = [('topic1',), ('topic2','subtopic2')]
736 """
737 return self.__topicTree.getTopics(listener)
738
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
750
751 # send to those who listen to all topics
752 self.__deliveryCount += \
753 self.__topicTree.sendMessage(aTopic, message, onTopicNeverCreated)
754
755 #
756 # Private methods
757 #
758
759 def __call__(self):
760 """Allows for singleton"""
761 return self
762
763 def __str__(self):
764 return str(self.__topicTree)
765
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.
770 #
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
774 # employed.
775 Publisher = Publisher()
776
777
778 #---------------------------------------------------------------------------
779
780 class Message:
781 """
782 A simple container object for the two components of
783 a message; the topic and the user data.
784 """
785 def __init__(self, topic, data):
786 self.topic = topic
787 self.data = data
788
789 def __str__(self):
790 return '[Topic: '+`self.topic`+', Data: '+`self.data`+']'
791
792
793 #---------------------------------------------------------------------------
794
795
796 #
797 # Code for a simple command-line test
798 #
799 def test():
800 def done(funcName):
801 print '----------- Done %s -----------' % funcName
802
803 def testParam():
804 def testFunc(a,b,c=1): pass
805 class Foo:
806 def testMeth(self,a,b): pass
807 def __call__(self, a): pass
808
809 foo = Foo()
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)
814
815 done('testParam')
816
817 testParam()
818 #------------------------
819
820 _NodeCallback.notified = 0
821 def testPreNotifyNode(self, dead):
822 _NodeCallback.notified += 1
823 print 'testPreNotifyNODE heard notification of', `dead`
824 _NodeCallback.preNotify = testPreNotifyNode
825
826 def testTreeNode():
827
828 class WS:
829 def __init__(self, s):
830 self.s = s
831 def __call__(self, msg):
832 print 'WS#', self.s, ' received msg ', msg
833 def __str__(self):
834 return self.s
835
836 def testPreNotifyRoot(dead):
837 print 'testPreNotifyROOT heard notification of', `dead`
838
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)
846
847 node.removeCallable(bid) # no-op
848 assert node.hasCallable(baz)
849 assert node.getCallables() == [boo,baz]
850
851 node.removeCallable(boo)
852 assert node.getCallables() == [baz]
853 assert node.hasCallable(baz)
854 assert not node.hasCallable(boo)
855
856 node.removeCallable(baz)
857 assert node.getCallables() == []
858 assert not node.hasCallable(baz)
859
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'))
868
869 print str(node)
870 assert str(node) == ' (st1: st1_cb1 st1_cb2 (st4: ) (st3: )) (st2: st2_cb )'
871
872 # verify send message, and that a dead listener does not get sent one
873 delivered = node2.sendMessage('hello')
874 assert delivered == 2
875 del cb1
876 delivered = node2.sendMessage('hello')
877 assert delivered == 1
878 assert _NodeCallback.notified == 1
879
880 done('testTreeNode')
881
882 testTreeNode()
883 #------------------------
884
885 def testValidate():
886 class Foo:
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
892 def badFun2(): pass
893 def badFun3(self, a, b): pass
894
895 server = Publisher()
896 foo = Foo()
897 server.validate(foo)
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)
904
905 done('testValidate')
906
907 testValidate()
908 #------------------------
909
910 class SimpleListener:
911 def __init__(self, number):
912 self.number = 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)
917 def __str__(self):
918 return "SimpleListener_%s" % self.number
919
920 def testSubscribe():
921 publisher = Publisher()
922
923 topic1 = 'politics'
924 topic2 = ('history','middle age')
925 topic3 = ('politics','UN')
926 topic4 = ('politics','NATO')
927 topic5 = ('politics','NATO','US')
928
929 lisnr1 = SimpleListener(1)
930 lisnr2 = SimpleListener(2)
931 def func(message, a=1):
932 print 'Func received message "%s"' % message
933 lisnr3 = func
934 lisnr4 = lambda x: 'Lambda received message "%s"' % x
935
936 assert not publisher.isSubscribed(lisnr1)
937 assert not publisher.isSubscribed(lisnr2)
938 assert not publisher.isSubscribed(lisnr3)
939 assert not publisher.isSubscribed(lisnr4)
940
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)
954
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
965
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)
982
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
988
989 publisher.unsubAll()
990 assert str(publisher) == 'all: '
991
992 done('testSubscribe')
993
994 testSubscribe()
995 #------------------------
996
997 def testUnsubAll():
998 publisher = Publisher()
999
1000 topic1 = 'politics'
1001 topic2 = ('history','middle age')
1002 topic3 = ('politics','UN')
1003 topic4 = ('politics','NATO')
1004 topic5 = ('politics','NATO','US')
1005
1006 lisnr1 = SimpleListener(1)
1007 lisnr2 = SimpleListener(2)
1008 def func(message, a=1):
1009 print 'Func received message "%s"' % message
1010 lisnr3 = func
1011 lisnr4 = lambda x: 'Lambda received message "%s"' % x
1012
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)
1019
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
1023
1024 publisher.unsubAll(topic1)
1025 assert publisher.getAssociatedTopics(lisnr1) == [topic2]
1026 assert not publisher.isSubscribed(lisnr1, topic1)
1027
1028 publisher.unsubAll(topic2)
1029 print publisher
1030 assert publisher.getAssociatedTopics(lisnr1) == []
1031 assert publisher.getAssociatedTopics(lisnr3) == [topic5]
1032 assert not publisher.isSubscribed(lisnr1)
1033 assert publisher.isSubscribed(lisnr3, topic5)
1034
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
1042
1043 publisher.unsubAll()
1044 done('testUnsubAll')
1045
1046 testUnsubAll()
1047 #------------------------
1048
1049 def testSend():
1050 publisher = Publisher()
1051 called = []
1052
1053 class TestListener:
1054 def __init__(self, num):
1055 self.number = 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')
1062
1063 lisnr1 = TestListener(1)
1064 lisnr2 = TestListener(2)
1065 lisnr3 = funcListener
1066 lisnr4 = lambda x: called.append('lambda')
1067
1068 topic1 = 'politics'
1069 topic2 = 'history'
1070 topic3 = ('politics','UN')
1071 topic4 = ('politics','NATO','US')
1072 topic5 = ('politics','NATO')
1073
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)
1079
1080 print publisher
1081
1082 # setup ok, now test send/receipt
1083 publisher.sendMessage(topic1)
1084 assert called == ['lambda','TL1cb']
1085 called = []
1086 publisher.sendMessage(topic2)
1087 assert called == ['lambda','TL2cb','TL2m']
1088 called = []
1089 publisher.sendMessage(topic3)
1090 assert called == ['lambda','TL1cb']
1091 called = []
1092 publisher.sendMessage(topic4)
1093 assert called == ['lambda','TL1cb','func']
1094 called = []
1095 publisher.sendMessage(topic5)
1096 assert called == ['lambda','TL1cb']
1097 assert publisher.getDeliveryCount() == 12
1098 assert publisher.getMessageCount() == 5
1099
1100 # test weak referencing works:
1101 _NodeCallback.notified = 0
1102 del lisnr2
1103 called = []
1104 publisher.sendMessage(topic2)
1105 assert called == ['lambda']
1106 assert _NodeCallback.notified == 2
1107
1108 done('testSend')
1109
1110 testSend()
1111 assert _NodeCallback.notified == 5
1112
1113 def testDead():
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)
1121
1122 node.addCallable(lisnr1)
1123 node.addCallable(lisnr2)
1124 node.addCallable(lisnr3)
1125 node.addCallable(lisnr4)
1126
1127 print 'Deleting listeners first'
1128 _NodeCallback.notified = 0
1129 del lisnr1
1130 del lisnr2
1131 assert _NodeCallback.notified == 2
1132
1133 print 'Deleting node first'
1134 _NodeCallback.notified = 0
1135 del node
1136 del lisnr3
1137 del lisnr4
1138 assert _NodeCallback.notified == 0
1139
1140 lisnr1 = SimpleListener(1)
1141 lisnr2 = SimpleListener(2)
1142 lisnr3 = SimpleListener(3)
1143 lisnr4 = SimpleListener(4)
1144
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
1163
1164 print 'Deleting listeners first'
1165 _NodeCallback.notified = 0
1166 del lisnr1
1167 del lisnr2
1168 assert _NodeCallback.notified == 2
1169 print 'Deleting node first'
1170 _NodeCallback.notified = 0
1171 del node
1172 del lisnr3
1173 del lisnr4
1174 assert _NodeCallback.notified == 0
1175
1176 done('testDead')
1177
1178 testDead()
1179
1180 print 'Exiting tests'
1181 #---------------------------------------------------------------------------
1182
1183 if __name__ == '__main__':
1184 test()