]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/pubsub.py
Add GetHDC back
[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()