]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/pubsub.py
avoid assert in wxMac port, only change the font point size when we
[wxWidgets.git] / wxPython / wx / lib / pubsub.py
CommitLineData
d1e05453 1
d14a1e28
RD
2#---------------------------------------------------------------------------
3"""
d1e05453
RD
4This module provides a publish-subscribe component that allows
5listeners to subcribe to messages of a given topic. Contrary to the
6original wxPython.lib.pubsub module, which it is based on, it uses
7weak referencing to the subscribers so the subscribers are not kept
8alive by the Publisher. Also, callable objects can be used in addition
9to functions and bound methods. See Publisher class docs for more
10details.
11
12Thanks to Robb Shecter and Robin Dunn for having provided
13the basis for this module (which now shares most of the concepts but
14very little design or implementation with the original
15wxPython.lib.pubsub).
16
17:Author: Oliver Schoenborn
18:Since: Apr 2004
19:Version: $Id$
20:Copyright: \(c) 2004 Oliver Schoenborn
21:License: wxWidgets
22
23Implementation notes
24--------------------
25
26In class Publisher, I represent the topics-listener set as a tree
27where each node is a topic, and contains a list of listeners of that
28topic, and a dictionary of subtopics of that topic. When the Publisher
29is told to send a message for a given topic, it traverses the tree
30down to the topic for which a message is being generated, all
31listeners on the way get sent the message.
32
33Publisher currently uses a weak listener topic tree to store the
34topics for each listener, and if a listener dies before being
35unsubscribed, the tree is notified, and the tree eliminates the
36listener from itself.
37
38Ideally, _TopicTreeNode would be a generic _TreeNode with named
39subnodes, and _TopicTreeRoot would be a generic _Tree with named
40nodes, and Publisher would store listeners in each node and a topic
41tuple would be converted to a path in the tree. This would lead to a
b0429a40 42much cleaner separation of concerns. But time is over, time to move on.
1fded56b 43
d1e05453
RD
44"""
45#---------------------------------------------------------------------------
1fded56b 46
d1e05453
RD
47# for function and method parameter counting:
48from types import InstanceType
49from inspect import getargspec, ismethod, isfunction
50# for weakly bound methods:
51from new import instancemethod as InstanceMethod
52from weakref import ref as WeakRef
d14a1e28 53
d1e05453 54# -----------------------------------------------------------------------------
d14a1e28 55
d1e05453
RD
56def _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
d14a1e28 60
d14a1e28 61
d1e05453
RD
62def _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)
d14a1e28 69
d1e05453
RD
70
71def _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
90def _tupleize(items):
91 """Convert items to tuple if not already one,
92 so items must be a list, tuple or non-sequence"""
b0429a40 93 if isinstance(items, list):
d1e05453 94 raise TypeError, 'Not allowed to tuple-ize a list'
b0429a40
RD
95 elif isinstance(items, (str, unicode)) and items.find('.') != -1:
96 items = tuple(items.split('.'))
97 elif not isinstance(items, tuple):
d1e05453
RD
98 items = (items,)
99 return items
100
101
102def _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__
109 else:
110 return '%s ' % callable
111
112
113def _removeItem(item, fromList):
114 """Attempt to remove item from fromList, return true
115 if successful, false otherwise."""
116 try:
117 fromList.remove(item)
118 return True
119 except ValueError:
120 return False
121
122
123# -----------------------------------------------------------------------------
124
125class _WeakMethod:
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. """
131
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)
140 else:
141 self.objRef = WeakRef(method.im_self, notifyDead)
142 self.fun = method.im_func
143 self.cls = method.im_class
144
145 def __call__(self):
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:
153 return None
154 else:
155 return InstanceMethod(self.fun, self.objRef(), self.cls)
156
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)
161
162 def __hash__(self):
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
166 welcome."""
167 return hash(self.fun)/2 + hash(self.objRef)/2
168
169 def __repr__(self):
170 dead = ''
171 if self.objRef() is None:
172 dead = '; DEAD'
173 obj = '<%s at %s%s>' % (self.__class__, id(self), dead)
174 return obj
175
176 def refs(self, weakRef):
177 """Return true if we are storing same object referred to by weakRef."""
178 return self.objRef == weakRef
179
180
181def _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."""
186 if ismethod(obj):
187 createRef = _WeakMethod
188 else:
189 createRef = WeakRef
190
191 if notifyDead is None:
192 return createRef(obj)
193 else:
194 return createRef(obj, notifyDead)
195
196
197# -----------------------------------------------------------------------------
198
199def 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'."""
202 return ''
203
204
205# alias, easier to see where used
206ALL_TOPICS = getStrAllTopics()
207
208# -----------------------------------------------------------------------------
209
210
211class _NodeCallback:
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.
d14a1e28 217 """
d1e05453
RD
218
219 def __init__(self, obj):
220 self.objRef = _getWeakRef(obj)
221
222 def __call__(self, weakCB):
223 notify = self.objRef()
224 if notify is not None:
225 self.preNotify(weakCB)
226 notify(weakCB)
227 else:
228 self.noNotify()
229
230 def preNotify(self, dead):
231 """'Gets called just before our callback (self.objRef) is called"""
232 pass
233
234 def noNotify(self):
235 """Gets called if the TopicTreeNode for this callback is dead"""
236 pass
237
238
239class _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).
246
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).
d14a1e28 251 """
d1e05453
RD
252
253 def __init__(self, topicPath, onDeadListenerWeakCB):
254 self.__subtopics = {}
255 self.__callables = []
256 self.__topicPath = topicPath
257 self.__onDeadListenerWeakCB = onDeadListenerWeakCB
258
259 def getPathname(self):
260 """The complete node path to us, ie., the topic tuple that would lead to us"""
261 return self.__topicPath
262
263 def createSubtopic(self, subtopic, topicPath):
264 """Create a child node for subtopic"""
265 return self.__subtopics.setdefault(subtopic,
266 _TopicTreeNode(topicPath, self.__onDeadListenerWeakCB))
267
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)
271
272 def getNode(self, subtopic):
273 """Return ref to node associated with subtopic"""
274 return self.__subtopics[subtopic]
275
276 def addCallable(self, callable):
277 """Add a callable to list of callables for this topic node"""
278 try:
279 id = self.__callables.index(_getWeakRef(callable))
280 return self.__callables[id]
281 except ValueError:
282 wrCall = _getWeakRef(callable, _NodeCallback(self.__notifyDead))
283 self.__callables.append(wrCall)
284 return wrCall
285
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]
289
290 def hasCallable(self, callable):
291 """Return true if callable in this node"""
292 try:
293 self.__callables.index(_getWeakRef(callable))
294 return True
295 except ValueError:
296 return False
297
298 def sendMessage(self, message):
299 """Send a message to our callables"""
300 deliveryCount = 0
301 for cb in self.__callables:
302 listener = cb()
303 if listener is not None:
304 listener(message)
305 deliveryCount += 1
306 return deliveryCount
307
308 def removeCallable(self, callable):
309 """Remove weak callable from our node (and return True).
310 Does nothing if not here (and returns False)."""
311 try:
312 self.__callables.remove(_getWeakRef(callable))
313 return True
314 except ValueError:
315 return False
316
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 = []
322 return tmpList
323
324 def __notifyDead(self, dead):
325 """Gets called when a listener dies, thanks to WeakRef"""
326 #print 'TreeNODE', `self`, 'received death certificate for ', dead
327 self.__cleanupDead()
328 if self.__onDeadListenerWeakCB is not None:
329 cb = self.__onDeadListenerWeakCB()
330 if cb is not None:
331 cb(dead)
332
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]
336
337 def __str__(self):
338 """Print us in a not-so-friendly, but readable way, good for debugging."""
339 strVal = []
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)
345
346
347class _TopicTreeRoot(_TopicTreeNode):
348 """
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.
352
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
365 optimization.
366 """
367
d14a1e28 368 def __init__(self):
d1e05453
RD
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))
375
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)
382
383 theList = self.__callbackDict.setdefault(weakCB, [])
384 assert self.__callbackDict.has_key(weakCB)
385 # add it only if we don't already have it
386 try:
387 weakTopicNode = WeakRef(topicNode)
388 theList.index(weakTopicNode)
389 except ValueError:
390 theList.append(weakTopicNode)
391 assert self.__callbackDict[weakCB].index(weakTopicNode) >= 0
392
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]
398
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)
405 if topic is None:
406 return self.__callbackDict.has_key(weakCB)
407 else:
408 topicPath = _tupleize(topic)
409 for weakNode in self.__callbackDict[weakCB]:
410 if topicPath == weakNode().getPathname():
411 return True
412 return False
413
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):
421 return
422
423 cbNodes = self.__callbackDict[weakCB]
424 if topicList is None:
425 for weakNode in cbNodes:
426 weakNode().removeCallable(listener)
427 del self.__callbackDict[weakCB]
428 return
429
430 for weakNode in cbNodes:
431 node = weakNode()
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())
437
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)
445 if node is not None:
446 weakCallables = node.clearCallables()
447 for callable in weakCallables:
448 weakNodes = self.__callbackDict[callable]
449 success = _removeItem(WeakRef(node), weakNodes)
450 assert success == True
451 if weakNodes == []:
452 del self.__callbackDict[callable]
453 elif onNoSuchTopic is not None:
454 onNoSuchTopic(topic)
455
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
462 node = self
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)
471 break
472 return deliveryCount
473
474 def numListeners(self):
475 """Return a pair (live, dead) with count of live and dead listeners in tree"""
476 dead, live = 0, 0
477 for cb in self.__callbackDict:
478 if cb() is None:
479 dead += 1
480 else:
481 live += 1
482 return live, dead
483
484 # clean up the callback dictionary after how many dead listeners
485 callbackDeadLimit = 10
486
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
497
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,):
503 return self
504
505 # not root, so traverse tree
506 node = self
507 path = ()
508 for topicItem in topic:
509 path += (topicItem,)
510 if topicItem == ALL_TOPICS:
511 raise ValueError, 'Topic tuple must not contain ""'
512 if make:
513 node = node.createSubtopic(topicItem, path)
514 elif node.hasSubtopic(topicItem):
515 node = node.getNode(topicItem)
516 else:
517 return None
518 # done
519 return node
520
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)
527
528 def __str__(self):
529 return 'all: %s' % _TopicTreeNode.__str__(self)
530
531
532# -----------------------------------------------------------------------------
d14a1e28 533
d1e05453
RD
534class Publisher:
535 """
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()).
540
541 The three important concepts for Publisher are:
542
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
547 are ok::
548
549 class Foo:
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
553
554 def func(a, b=''): pass
555
556 Foo foo
557 Publisher().subscribe(foo) # functor
558 Publisher().subscribe(foo.meth) # bound method
559 Publisher().subscribe(foo.meth2) # bound method
560 Publisher().subscribe(func) # function
561
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
564 the message.
565
b0429a40
RD
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
570 this topic::
d1e05453
RD
571
572 ('sports','baseball')
573
574 would receive messages for these topics::
575
576 ('sports', 'baseball') # because same
577 ('sports', 'baseball', 'highscores') # because more specific
578
579 but not these::
580
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
585
586 - message: this is an instance of Message, containing the topic for
587 which the message was sent, and any data the sender specified.
588
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()'.
593 """
594
595 __ALL_TOPICS_TPL = (ALL_TOPICS, )
596
597 def __init__(self):
598 self.__messageCount = 0
599 self.__deliveryCount = 0
600 self.__topicTree = _TopicTreeRoot()
d14a1e28
RD
601
602 #
603 # Public API
604 #
605
d1e05453
RD
606 def getDeliveryCount(self):
607 """How many listeners have received a message since beginning of run"""
608 return self.__deliveryCount
609
610 def getMessageCount(self):
611 """How many times sendMessage() was called since beginning of run"""
612 return self.__messageCount
613
614 def subscribe(self, listener, topic = ALL_TOPICS):
d14a1e28 615 """
d1e05453
RD
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).
619
b0429a40
RD
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
624 topic.
d1e05453
RD
625
626 :note: Calling
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.
d14a1e28 637 """
d1e05453
RD
638 self.validate(listener)
639
640 if topic is None:
641 raise TypeError, 'Topic must be either a word, tuple of '\
642 'words, or getStrAllTopics()'
643
644 self.__topicTree.addTopic(_tupleize(topic), listener)
645
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)
652
653 def validate(self, listener):
654 """Similar to isValid(), but raises a TypeError exception if not valid"""
655 # check callable
d14a1e28 656 if not callable(listener):
d1e05453
RD
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!'
663
664 # check that it takes the right number of parameters
665 min, d = _paramMinCount(listener)
666 if min > 1:
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!'
671
672 assert (min == 0 and d>0) or (min == 1)
673
674 def isValid(self, listener):
675 """Return true only if listener will be able to subscribe to Publisher."""
676 try:
677 self.validate(listener)
678 return True
679 except TypeError:
680 return False
681
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."""
691 if topics is None:
692 del self.__topicTree
693 self.__topicTree = _TopicTreeRoot()
694 return
695
696 # make sure every topics are in tuple form
b0429a40 697 if isinstance(topics, list):
d1e05453
RD
698 topicList = [_tupleize(x) for x in topics]
699 else:
700 topicList = [_tupleize(topics)]
701
702 # unsub every listener of topics
703 self.__topicTree.unsubAll(topicList, onNoSuchTopic)
704
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).
712
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).
d14a1e28 717 """
d1e05453
RD
718 self.validate(listener)
719 topicList = None
720 if topics is not None:
721 if isinstance(topics, list):
722 topicList = [_tupleize(x) for x in topics]
723 else:
724 topicList = [_tupleize(topics)]
725
726 self.__topicTree.unsubscribe(listener, topicList)
727
d14a1e28 728 def getAssociatedTopics(self, listener):
d1e05453
RD
729 """Return a list of topics the given listener is registered with.
730 Returns [] if listener never subscribed.
731
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::
737
738 associatedTopics = [('topic1',), ('topic2','subtopic2')]
d14a1e28 739 """
d1e05453
RD
740 return self.__topicTree.getTopics(listener)
741
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
753
754 # send to those who listen to all topics
755 self.__deliveryCount += \
756 self.__topicTree.sendMessage(aTopic, message, onTopicNeverCreated)
757
d14a1e28
RD
758 #
759 # Private methods
760 #
761
d14a1e28 762 def __call__(self):
d1e05453 763 """Allows for singleton"""
d14a1e28 764 return self
d1e05453
RD
765
766 def __str__(self):
767 return str(self.__topicTree)
d14a1e28
RD
768
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.
773#
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
777# employed.
778Publisher = Publisher()
779
780
781#---------------------------------------------------------------------------
782
783class Message:
784 """
785 A simple container object for the two components of
d1e05453 786 a message; the topic and the user data.
d14a1e28
RD
787 """
788 def __init__(self, topic, data):
789 self.topic = topic
790 self.data = data
791
792 def __str__(self):
793 return '[Topic: '+`self.topic`+', Data: '+`self.data`+']'
794
795
d14a1e28
RD
796#---------------------------------------------------------------------------
797
798
799#
800# Code for a simple command-line test
801#
d1e05453
RD
802def test():
803 def done(funcName):
804 print '----------- Done %s -----------' % funcName
805
806 def testParam():
807 def testFunc(a,b,c=1): pass
808 class Foo:
809 def testMeth(self,a,b): pass
810 def __call__(self, a): pass
811
812 foo = Foo()
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)
817
818 done('testParam')
819
820 testParam()
821 #------------------------
822
823 _NodeCallback.notified = 0
824 def testPreNotifyNode(self, dead):
825 _NodeCallback.notified += 1
826 print 'testPreNotifyNODE heard notification of', `dead`
827 _NodeCallback.preNotify = testPreNotifyNode
828
829 def testTreeNode():
830
831 class WS:
832 def __init__(self, s):
833 self.s = s
834 def __call__(self, msg):
835 print 'WS#', self.s, ' received msg ', msg
836 def __str__(self):
837 return self.s
838
839 def testPreNotifyRoot(dead):
840 print 'testPreNotifyROOT heard notification of', `dead`
841
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)
849
850 node.removeCallable(bid) # no-op
851 assert node.hasCallable(baz)
852 assert node.getCallables() == [boo,baz]
853
854 node.removeCallable(boo)
855 assert node.getCallables() == [baz]
856 assert node.hasCallable(baz)
857 assert not node.hasCallable(boo)
858
859 node.removeCallable(baz)
860 assert node.getCallables() == []
861 assert not node.hasCallable(baz)
862
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'))
871
872 print str(node)
873 assert str(node) == ' (st1: st1_cb1 st1_cb2 (st4: ) (st3: )) (st2: st2_cb )'
874
875 # verify send message, and that a dead listener does not get sent one
876 delivered = node2.sendMessage('hello')
877 assert delivered == 2
878 del cb1
879 delivered = node2.sendMessage('hello')
880 assert delivered == 1
881 assert _NodeCallback.notified == 1
882
883 done('testTreeNode')
884
885 testTreeNode()
886 #------------------------
887
888 def testValidate():
889 class Foo:
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
895 def badFun2(): pass
896 def badFun3(self, a, b): pass
897
898 server = Publisher()
899 foo = Foo()
900 server.validate(foo)
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)
907
908 done('testValidate')
909
910 testValidate()
911 #------------------------
912
d14a1e28
RD
913 class SimpleListener:
914 def __init__(self, number):
915 self.number = number
d1e05453
RD
916 def __call__(self, message = ''):
917 print 'Callable #%s got the message "%s"' %(self.number, message)
d14a1e28 918 def notify(self, message):
d1e05453
RD
919 print '%s.notify() got the message "%s"' %(self.number, message)
920 def __str__(self):
921 return "SimpleListener_%s" % self.number
922
923 def testSubscribe():
924 publisher = Publisher()
925
926 topic1 = 'politics'
927 topic2 = ('history','middle age')
928 topic3 = ('politics','UN')
929 topic4 = ('politics','NATO')
930 topic5 = ('politics','NATO','US')
931
932 lisnr1 = SimpleListener(1)
933 lisnr2 = SimpleListener(2)
934 def func(message, a=1):
935 print 'Func received message "%s"' % message
936 lisnr3 = func
937 lisnr4 = lambda x: 'Lambda received message "%s"' % x
938
939 assert not publisher.isSubscribed(lisnr1)
940 assert not publisher.isSubscribed(lisnr2)
941 assert not publisher.isSubscribed(lisnr3)
942 assert not publisher.isSubscribed(lisnr4)
943
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)
957
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
968
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)
985
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
991
992 publisher.unsubAll()
993 assert str(publisher) == 'all: '
994
995 done('testSubscribe')
996
997 testSubscribe()
998 #------------------------
999
1000 def testUnsubAll():
1001 publisher = Publisher()
1002
1003 topic1 = 'politics'
1004 topic2 = ('history','middle age')
1005 topic3 = ('politics','UN')
1006 topic4 = ('politics','NATO')
1007 topic5 = ('politics','NATO','US')
1008
1009 lisnr1 = SimpleListener(1)
1010 lisnr2 = SimpleListener(2)
1011 def func(message, a=1):
1012 print 'Func received message "%s"' % message
1013 lisnr3 = func
1014 lisnr4 = lambda x: 'Lambda received message "%s"' % x
1015
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)
1022
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
1026
1027 publisher.unsubAll(topic1)
1028 assert publisher.getAssociatedTopics(lisnr1) == [topic2]
1029 assert not publisher.isSubscribed(lisnr1, topic1)
1030
1031 publisher.unsubAll(topic2)
1032 print publisher
1033 assert publisher.getAssociatedTopics(lisnr1) == []
1034 assert publisher.getAssociatedTopics(lisnr3) == [topic5]
1035 assert not publisher.isSubscribed(lisnr1)
1036 assert publisher.isSubscribed(lisnr3, topic5)
1037
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
1045
1046 publisher.unsubAll()
1047 done('testUnsubAll')
1048
1049 testUnsubAll()
1050 #------------------------
1051
1052 def testSend():
1053 publisher = Publisher()
1054 called = []
1055
1056 class TestListener:
1057 def __init__(self, num):
1058 self.number = 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')
1065
1066 lisnr1 = TestListener(1)
1067 lisnr2 = TestListener(2)
1068 lisnr3 = funcListener
1069 lisnr4 = lambda x: called.append('lambda')
1070
1071 topic1 = 'politics'
1072 topic2 = 'history'
1073 topic3 = ('politics','UN')
1074 topic4 = ('politics','NATO','US')
1075 topic5 = ('politics','NATO')
1076
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)
1082
1083 print publisher
1084
1085 # setup ok, now test send/receipt
1086 publisher.sendMessage(topic1)
1087 assert called == ['lambda','TL1cb']
1088 called = []
1089 publisher.sendMessage(topic2)
1090 assert called == ['lambda','TL2cb','TL2m']
1091 called = []
1092 publisher.sendMessage(topic3)
1093 assert called == ['lambda','TL1cb']
1094 called = []
1095 publisher.sendMessage(topic4)
1096 assert called == ['lambda','TL1cb','func']
1097 called = []
1098 publisher.sendMessage(topic5)
1099 assert called == ['lambda','TL1cb']
1100 assert publisher.getDeliveryCount() == 12
1101 assert publisher.getMessageCount() == 5
1102
1103 # test weak referencing works:
1104 _NodeCallback.notified = 0
1105 del lisnr2
1106 called = []
1107 publisher.sendMessage(topic2)
1108 assert called == ['lambda']
1109 assert _NodeCallback.notified == 2
1110
1111 done('testSend')
1112
1113 testSend()
1114 assert _NodeCallback.notified == 5
1115
1116 def testDead():
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)
1124
1125 node.addCallable(lisnr1)
1126 node.addCallable(lisnr2)
1127 node.addCallable(lisnr3)
1128 node.addCallable(lisnr4)
1129
1130 print 'Deleting listeners first'
1131 _NodeCallback.notified = 0
1132 del lisnr1
1133 del lisnr2
1134 assert _NodeCallback.notified == 2
1135
1136 print 'Deleting node first'
1137 _NodeCallback.notified = 0
1138 del node
1139 del lisnr3
1140 del lisnr4
1141 assert _NodeCallback.notified == 0
1142
1143 lisnr1 = SimpleListener(1)
1144 lisnr2 = SimpleListener(2)
1145 lisnr3 = SimpleListener(3)
1146 lisnr4 = SimpleListener(4)
1147
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
1166
1167 print 'Deleting listeners first'
1168 _NodeCallback.notified = 0
1169 del lisnr1
1170 del lisnr2
1171 assert _NodeCallback.notified == 2
1172 print 'Deleting node first'
1173 _NodeCallback.notified = 0
1174 del node
1175 del lisnr3
1176 del lisnr4
1177 assert _NodeCallback.notified == 0
1178
1179 done('testDead')
1180
1181 testDead()
1182
1183 print 'Exiting tests'
d14a1e28 1184#---------------------------------------------------------------------------
d1e05453
RD
1185
1186if __name__ == '__main__':
1187 test()