~ubuntu-branches/debian/sid/phatch/sid

« back to all changes in this revision

Viewing changes to .pc/do_not_raise_string_exceptions.patch/phatch/other/pubsub.py

  • Committer: Package Import Robot
  • Author(s): Piotr Ożarowski
  • Date: 2010-05-27 20:17:37 UTC
  • Revision ID: package-import@ubuntu.com-20100527201737-awcn0u6ss2hdwtzi
Tags: 0.2.7-2
* Prepare for Python 2.6:
  - do_not_raise_string_exceptions.patch added
* Source package format changed to 3.0 (quilt)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright: (c) 2004 Oliver Schoenborn
 
2
# License: wxWidgets(based on LGPL,http://www.wxwidgets.org/about/newlicen.htm)
 
3
 
 
4
#---------------------------------------------------------------------------
 
5
"""
 
6
This module provides a publish-subscribe component that allows
 
7
listeners to subcribe to messages of a given topic. Contrary to the
 
8
original wxPython.lib.pubsub module (which it is based on), it uses 
 
9
weak referencing to the subscribers so the lifetime of subscribers 
 
10
is not affected by Publisher. Also, callable objects can be used in 
 
11
addition to functions and bound methods. See Publisher class docs for 
 
12
more details. 
 
13
 
 
14
Thanks to Robb Shecter and Robin Dunn for having provided 
 
15
the basis for this module (which now shares most of the concepts but
 
16
very little design or implementation with the original 
 
17
wxPython.lib.pubsub).
 
18
 
 
19
The publisher is a singleton instance of the PublisherClass class. You 
 
20
access the instance via the Publisher object available from the module::
 
21
 
 
22
    from wx.lib.pubsub import Publisher
 
23
    Publisher().subscribe(...)
 
24
    Publisher().sendMessage(...)
 
25
    ...
 
26
 
 
27
:Author:      Oliver Schoenborn
 
28
:Since:       Apr 2004
 
29
:Version:     $Id: pubsub.py,v 1.8 2006/06/11 00:12:59 RD Exp $
 
30
:Copyright:   \(c) 2004 Oliver Schoenborn
 
31
:License:     wxWidgets
 
32
"""
 
33
 
 
34
_implNotes = """
 
35
Implementation notes
 
36
--------------------
 
37
 
 
38
In class Publisher, I represent the topics-listener set as a tree
 
39
where each node is a topic, and contains a list of listeners of that
 
40
topic, and a dictionary of subtopics of that topic. When the Publisher
 
41
is told to send a message for a given topic, it traverses the tree
 
42
down to the topic for which a message is being generated, all
 
43
listeners on the way get sent the message.
 
44
 
 
45
Publisher currently uses a weak listener topic tree to store the
 
46
topics for each listener, and if a listener dies before being
 
47
unsubscribed, the tree is notified, and the tree eliminates the
 
48
listener from itself.
 
49
 
 
50
Ideally, _TopicTreeNode would be a generic _TreeNode with named
 
51
subnodes, and _TopicTreeRoot would be a generic _Tree with named
 
52
nodes, and Publisher would store listeners in each node and a topic
 
53
tuple would be converted to a path in the tree.  This would lead to a
 
54
much cleaner separation of concerns. But time is over, time to move on.
 
55
"""
 
56
#---------------------------------------------------------------------------
 
57
 
 
58
# for function and method parameter counting:
 
59
from types   import InstanceType
 
60
from inspect import getargspec, ismethod, isfunction
 
61
# for weakly bound methods:
 
62
from new     import instancemethod as InstanceMethod
 
63
from weakref import ref as WeakRef
 
64
 
 
65
# -----------------------------------------------------------------------------
 
66
 
 
67
def _isbound(method):
 
68
    """Return true if method is a bound method, false otherwise"""
 
69
    assert ismethod(method)
 
70
    return method.im_self is not None
 
71
 
 
72
 
 
73
def _paramMinCountFunc(function):
 
74
    """Given a function, return pair (min,d) where min is minimum # of
 
75
    args required, and d is number of default arguments."""
 
76
    assert isfunction(function)
 
77
    (args, va, kwa, dflt) = getargspec(function)
 
78
    lenDef = len(dflt or ())
 
79
    lenArgs = len(args or ())
 
80
    lenVA = int(va is not None)
 
81
    return (lenArgs - lenDef + lenVA, lenDef)
 
82
 
 
83
 
 
84
def _paramMinCount(callableObject):
 
85
    """
 
86
    Given a callable object (function, method or callable instance),
 
87
    return pair (min,d) where min is minimum # of args required, and d
 
88
    is number of default arguments. The 'self' parameter, in the case
 
89
    of methods, is not counted.
 
90
    """
 
91
    if type(callableObject) is InstanceType:
 
92
        min, d = _paramMinCountFunc(callableObject.__call__.im_func)
 
93
        return min-1, d
 
94
    elif ismethod(callableObject):
 
95
        min, d = _paramMinCountFunc(callableObject.im_func)
 
96
        return min-1, d
 
97
    elif isfunction(callableObject):
 
98
        return _paramMinCountFunc(callableObject)
 
99
    else:
 
100
        raise 'Cannot determine type of callable: '+repr(callableObject)
 
101
 
 
102
 
 
103
def _tupleize(items):
 
104
    """Convert items to tuple if not already one, 
 
105
    so items must be a list, tuple or non-sequence"""
 
106
    if isinstance(items, list):
 
107
        raise TypeError, 'Not allowed to tuple-ize a list'
 
108
    elif isinstance(items, (str, unicode)) and items.find('.') != -1:
 
109
        items = tuple(items.split('.'))
 
110
    elif not isinstance(items, tuple):
 
111
        items = (items,)
 
112
    return items
 
113
 
 
114
 
 
115
def _getCallableName(callable):
 
116
    """Get name for a callable, ie function, bound 
 
117
    method or callable instance"""
 
118
    if ismethod(callable):
 
119
        return '%s.%s ' % (callable.im_self, callable.im_func.func_name)
 
120
    elif isfunction(callable):
 
121
        return '%s ' % callable.__name__
 
122
    else:
 
123
        return '%s ' % callable
 
124
    
 
125
    
 
126
def _removeItem(item, fromList):
 
127
    """Attempt to remove item from fromList, return true 
 
128
    if successful, false otherwise."""
 
129
    try: 
 
130
        fromList.remove(item)
 
131
        return True
 
132
    except ValueError:
 
133
        return False
 
134
        
 
135
        
 
136
# -----------------------------------------------------------------------------
 
137
 
 
138
class _WeakMethod:
 
139
    """Represent a weak bound method, i.e. a method doesn't keep alive the 
 
140
    object that it is bound to. It uses WeakRef which, used on its own, 
 
141
    produces weak methods that are dead on creation, not very useful. 
 
142
    Typically, you will use the getRef() function instead of using
 
143
    this class directly. """
 
144
    
 
145
    def __init__(self, method, notifyDead = None):
 
146
        """The method must be bound. notifyDead will be called when 
 
147
        object that method is bound to dies. """
 
148
        assert ismethod(method)
 
149
        if method.im_self is None:
 
150
            raise ValueError, "We need a bound method!"
 
151
        if notifyDead is None:
 
152
            self.objRef = WeakRef(method.im_self)
 
153
        else:
 
154
            self.objRef = WeakRef(method.im_self, notifyDead)
 
155
        self.fun = method.im_func
 
156
        self.cls = method.im_class
 
157
        
 
158
    def __call__(self):
 
159
        """Returns a new.instancemethod if object for method still alive. 
 
160
        Otherwise return None. Note that instancemethod causes a 
 
161
        strong reference to object to be created, so shouldn't save 
 
162
        the return value of this call. Note also that this __call__
 
163
        is required only for compatibility with WeakRef.ref(), otherwise
 
164
        there would be more efficient ways of providing this functionality."""
 
165
        if self.objRef() is None:
 
166
            return None
 
167
        else:
 
168
            return InstanceMethod(self.fun, self.objRef(), self.cls)
 
169
        
 
170
    def __eq__(self, method2):
 
171
        """Two WeakMethod objects compare equal if they refer to the same method
 
172
        of the same instance. Thanks to Josiah Carlson for patch and clarifications
 
173
        on how dict uses eq/cmp and hashing. """
 
174
        if not isinstance(method2, _WeakMethod):
 
175
            return False 
 
176
        return      self.fun      is method2.fun \
 
177
                and self.objRef() is method2.objRef() \
 
178
                and self.objRef() is not None
 
179
    
 
180
    def __hash__(self):
 
181
        """Hash is an optimization for dict searches, it need not 
 
182
        return different numbers for every different object. Some objects
 
183
        are not hashable (eg objects of classes derived from dict) so no
 
184
        hash(objRef()) in there, and hash(self.cls) would only be useful
 
185
        in the rare case where instance method was rebound. """
 
186
        return hash(self.fun)
 
187
    
 
188
    def __repr__(self):
 
189
        dead = ''
 
190
        if self.objRef() is None: 
 
191
            dead = '; DEAD'
 
192
        obj = '<%s at %s%s>' % (self.__class__, id(self), dead)
 
193
        return obj
 
194
        
 
195
    def refs(self, weakRef):
 
196
        """Return true if we are storing same object referred to by weakRef."""
 
197
        return self.objRef == weakRef
 
198
 
 
199
 
 
200
def _getWeakRef(obj, notifyDead=None):
 
201
    """Get a weak reference to obj. If obj is a bound method, a _WeakMethod
 
202
    object, that behaves like a WeakRef, is returned, if it is
 
203
    anything else a WeakRef is returned. If obj is an unbound method,
 
204
    a ValueError will be raised."""
 
205
    if ismethod(obj):
 
206
        createRef = _WeakMethod
 
207
    else:
 
208
        createRef = WeakRef
 
209
        
 
210
    if notifyDead is None:
 
211
        return createRef(obj)
 
212
    else:
 
213
        return createRef(obj, notifyDead)
 
214
    
 
215
    
 
216
# -----------------------------------------------------------------------------
 
217
 
 
218
def getStrAllTopics():
 
219
    """Function to call if, for whatever reason, you need to know 
 
220
    explicitely what is the string to use to indicate 'all topics'."""
 
221
    return ''
 
222
 
 
223
 
 
224
# alias, easier to see where used
 
225
ALL_TOPICS = getStrAllTopics()
 
226
 
 
227
# -----------------------------------------------------------------------------
 
228
 
 
229
 
 
230
class _NodeCallback:
 
231
    """Encapsulate a weak reference to a method of a TopicTreeNode
 
232
    in such a way that the method can be called, if the node is 
 
233
    still alive, but the callback does not *keep* the node alive.
 
234
    Also, define two methods, preNotify() and noNotify(), which can 
 
235
    be redefined to something else, very useful for testing. 
 
236
    """
 
237
    
 
238
    def __init__(self, obj):
 
239
        self.objRef = _getWeakRef(obj)
 
240
        
 
241
    def __call__(self, weakCB):
 
242
        notify = self.objRef()
 
243
        if notify is not None: 
 
244
            self.preNotify(weakCB)
 
245
            notify(weakCB)
 
246
        else: 
 
247
            self.noNotify()
 
248
            
 
249
    def preNotify(self, dead):
 
250
        """'Gets called just before our callback (self.objRef) is called"""
 
251
        pass
 
252
        
 
253
    def noNotify(self):
 
254
        """Gets called if the TopicTreeNode for this callback is dead"""
 
255
        pass
 
256
 
 
257
 
 
258
class _TopicTreeNode:
 
259
    """A node in the topic tree. This contains a list of callables
 
260
    that are interested in the topic that this node is associated
 
261
    with, and contains a dictionary of subtopics, whose associated
 
262
    values are other _TopicTreeNodes. The topic of a node is not stored
 
263
    in the node, so that the tree can be implemented as a dictionary
 
264
    rather than a list, for ease of use (and, likely, performance).
 
265
    
 
266
    Note that it uses _NodeCallback to encapsulate a callback for 
 
267
    when a registered listener dies, possible thanks to WeakRef.
 
268
    Whenever this callback is called, the onDeadListener() function, 
 
269
    passed in at construction time, is called (unless it is None).
 
270
    """
 
271
    
 
272
    def __init__(self, topicPath, onDeadListenerWeakCB):
 
273
        self.__subtopics = {}
 
274
        self.__callables = []
 
275
        self.__topicPath = topicPath
 
276
        self.__onDeadListenerWeakCB = onDeadListenerWeakCB
 
277
        
 
278
    def getPathname(self): 
 
279
        """The complete node path to us, ie., the topic tuple that would lead to us"""
 
280
        return self.__topicPath
 
281
    
 
282
    def createSubtopic(self, subtopic, topicPath):
 
283
        """Create a child node for subtopic"""
 
284
        return self.__subtopics.setdefault(subtopic,
 
285
                    _TopicTreeNode(topicPath, self.__onDeadListenerWeakCB))
 
286
    
 
287
    def hasSubtopic(self, subtopic):
 
288
        """Return true only if topic string is one of subtopics of this node"""
 
289
        return self.__subtopics.has_key(subtopic)
 
290
    
 
291
    def getNode(self, subtopic):
 
292
        """Return ref to node associated with subtopic"""
 
293
        return self.__subtopics[subtopic]
 
294
    
 
295
    def addCallable(self, callable):
 
296
        """Add a callable to list of callables for this topic node"""
 
297
        try:
 
298
            id = self.__callables.index(_getWeakRef(callable))
 
299
            return self.__callables[id]
 
300
        except ValueError:
 
301
            wrCall = _getWeakRef(callable, _NodeCallback(self.__notifyDead))
 
302
            self.__callables.append(wrCall)
 
303
            return wrCall
 
304
            
 
305
    def getCallables(self):
 
306
        """Get callables associated with this topic node"""
 
307
        return [cb() for cb in self.__callables if cb() is not None]
 
308
    
 
309
    def hasCallable(self, callable):
 
310
        """Return true if callable in this node"""
 
311
        try: 
 
312
            self.__callables.index(_getWeakRef(callable))
 
313
            return True
 
314
        except ValueError:
 
315
            return False
 
316
    
 
317
    def sendMessage(self, message):
 
318
        """Send a message to our callables"""
 
319
        deliveryCount = 0
 
320
        for cb in self.__callables:
 
321
            listener = cb()
 
322
            if listener is not None:
 
323
                listener(message)
 
324
                deliveryCount += 1
 
325
        return deliveryCount
 
326
    
 
327
    def removeCallable(self, callable):
 
328
        """Remove weak callable from our node (and return True). 
 
329
        Does nothing if not here (and returns False)."""
 
330
        try: 
 
331
            self.__callables.remove(_getWeakRef(callable))
 
332
            return True
 
333
        except ValueError:
 
334
            return False
 
335
        
 
336
    def clearCallables(self):
 
337
        """Abandon list of callables to caller. We no longer have 
 
338
        any callables after this method is called."""
 
339
        tmpList = [cb for cb in self.__callables if cb() is not None]
 
340
        self.__callables = []
 
341
        return tmpList
 
342
        
 
343
    def __notifyDead(self, dead):
 
344
        """Gets called when a listener dies, thanks to WeakRef"""
 
345
        #print 'TreeNODE', `self`, 'received death certificate for ', dead
 
346
        self.__cleanupDead()
 
347
        if self.__onDeadListenerWeakCB is not None:
 
348
            cb = self.__onDeadListenerWeakCB()
 
349
            if cb is not None: 
 
350
                cb(dead)
 
351
        
 
352
    def __cleanupDead(self):
 
353
        """Remove all dead objects from list of callables"""
 
354
        self.__callables = [cb for cb in self.__callables if cb() is not None]
 
355
        
 
356
    def __str__(self):
 
357
        """Print us in a not-so-friendly, but readable way, good for debugging."""
 
358
        strVal = []
 
359
        for callable in self.getCallables():
 
360
            strVal.append(_getCallableName(callable))
 
361
        for topic, node in self.__subtopics.iteritems():
 
362
            strVal.append(' (%s: %s)' %(topic, node))
 
363
        return ''.join(strVal)
 
364
      
 
365
      
 
366
class _TopicTreeRoot(_TopicTreeNode):
 
367
    """
 
368
    The root of the tree knows how to access other node of the 
 
369
    tree and is the gateway of the tree user to the tree nodes. 
 
370
    It can create topics, and and remove callbacks, etc. 
 
371
    
 
372
    For efficiency, it stores a dictionary of listener-topics, 
 
373
    so that unsubscribing a listener just requires finding the 
 
374
    topics associated to a listener, and finding the corresponding
 
375
    nodes of the tree. Without it, unsubscribing would require 
 
376
    that we search the whole tree for all nodes that contain 
 
377
    given listener. Since Publisher is a singleton, it will 
 
378
    contain all topics in the system so it is likely to be a large
 
379
    tree. However, it is possible that in some runs, unsubscribe()
 
380
    is called very little by the user, in which case most unsubscriptions
 
381
    are automatic, ie caused by the listeners dying. In this case, 
 
382
    a flag is set to indicate that the dictionary should be cleaned up
 
383
    at the next opportunity. This is not necessary, it is just an 
 
384
    optimization.
 
385
    """
 
386
    
 
387
    def __init__(self):
 
388
        self.__callbackDict  = {}
 
389
        self.__callbackDictCleanup = 0
 
390
        # all child nodes will call our __rootNotifyDead method
 
391
        # when one of their registered listeners dies 
 
392
        _TopicTreeNode.__init__(self, (ALL_TOPICS,), 
 
393
                                _getWeakRef(self.__rootNotifyDead))
 
394
        
 
395
    def addTopic(self, topic, listener):
 
396
        """Add topic to tree if doesnt exist, and add listener to topic node"""
 
397
        assert isinstance(topic, tuple)
 
398
        topicNode = self.__getTreeNode(topic, make=True)
 
399
        weakCB = topicNode.addCallable(listener)
 
400
        assert topicNode.hasCallable(listener)
 
401
 
 
402
        theList = self.__callbackDict.setdefault(weakCB, [])
 
403
        assert self.__callbackDict.has_key(weakCB)
 
404
        # add it only if we don't already have it
 
405
        try:
 
406
            weakTopicNode = WeakRef(topicNode)
 
407
            theList.index(weakTopicNode)
 
408
        except ValueError:
 
409
            theList.append(weakTopicNode)
 
410
        assert self.__callbackDict[weakCB].index(weakTopicNode) >= 0
 
411
        
 
412
    def getTopics(self, listener):
 
413
        """Return the list of topics for given listener"""
 
414
        weakNodes = self.__callbackDict.get(_getWeakRef(listener), [])
 
415
        return [weakNode().getPathname() for weakNode in weakNodes 
 
416
                if weakNode() is not None]
 
417
 
 
418
    def isSubscribed(self, listener, topic=None):
 
419
        """Return true if listener is registered for topic specified. 
 
420
        If no topic specified, return true if subscribed to something.
 
421
        Use topic=getStrAllTopics() to determine if a listener will receive 
 
422
        messages for all topics."""
 
423
        weakCB = _getWeakRef(listener)
 
424
        if topic is None: 
 
425
            return self.__callbackDict.has_key(weakCB)
 
426
        else:
 
427
            topicPath = _tupleize(topic)
 
428
            for weakNode in self.__callbackDict[weakCB]:
 
429
                if topicPath == weakNode().getPathname():
 
430
                    return True
 
431
            return False
 
432
            
 
433
    def unsubscribe(self, listener, topicList):
 
434
        """Remove listener from given list of topics. If topicList
 
435
        doesn't have any topics for which listener has subscribed,
 
436
        nothing happens."""
 
437
        weakCB = _getWeakRef(listener)
 
438
        if not self.__callbackDict.has_key(weakCB):
 
439
            return
 
440
        
 
441
        cbNodes = self.__callbackDict[weakCB] 
 
442
        if topicList is None:
 
443
            for weakNode in cbNodes:
 
444
                weakNode().removeCallable(listener)
 
445
            del self.__callbackDict[weakCB] 
 
446
            return
 
447
 
 
448
        for weakNode in cbNodes:
 
449
            node = weakNode()
 
450
            if node is not None and node.getPathname() in topicList:
 
451
                success = node.removeCallable(listener)
 
452
                assert success == True
 
453
                cbNodes.remove(weakNode)
 
454
                assert not self.isSubscribed(listener, node.getPathname())
 
455
 
 
456
    def unsubAll(self, topicList, onNoSuchTopic):
 
457
        """Unsubscribe all listeners registered for any topic in 
 
458
        topicList. If a topic in the list does not exist, and 
 
459
        onNoSuchTopic is not None, a call
 
460
        to onNoSuchTopic(topic) is done for that topic."""
 
461
        for topic in topicList:
 
462
            node = self.__getTreeNode(topic)
 
463
            if node is not None:
 
464
                weakCallables = node.clearCallables()
 
465
                for callable in weakCallables:
 
466
                    weakNodes = self.__callbackDict[callable]
 
467
                    success = _removeItem(WeakRef(node), weakNodes)
 
468
                    assert success == True
 
469
                    if weakNodes == []:
 
470
                        del self.__callbackDict[callable]
 
471
            elif onNoSuchTopic is not None: 
 
472
                onNoSuchTopic(topic)
 
473
            
 
474
    def sendMessage(self, topic, message, onTopicNeverCreated):
 
475
        """Send a message for given topic to all registered listeners. If 
 
476
        topic doesn't exist, call onTopicNeverCreated(topic)."""
 
477
        # send to the all-toipcs listeners
 
478
        deliveryCount = _TopicTreeNode.sendMessage(self, message)
 
479
        # send to those who listen to given topic or any of its supertopics
 
480
        node = self
 
481
        for topicItem in topic:
 
482
            assert topicItem != ''
 
483
            if node.hasSubtopic(topicItem):
 
484
                node = node.getNode(topicItem)
 
485
                deliveryCount += node.sendMessage(message)
 
486
            else: # topic never created, don't bother continuing
 
487
                if onTopicNeverCreated is not None:
 
488
                    onTopicNeverCreated(topic)
 
489
                break
 
490
        return deliveryCount
 
491
 
 
492
    def numListeners(self):
 
493
        """Return a pair (live, dead) with count of live and dead listeners in tree"""
 
494
        dead, live = 0, 0
 
495
        for cb in self.__callbackDict:
 
496
            if cb() is None: 
 
497
                dead += 1
 
498
            else:
 
499
                live += 1
 
500
        return live, dead
 
501
    
 
502
    # clean up the callback dictionary after how many dead listeners
 
503
    callbackDeadLimit = 10
 
504
 
 
505
    def __rootNotifyDead(self, dead):
 
506
        #print 'TreeROOT received death certificate for ', dead
 
507
        self.__callbackDictCleanup += 1
 
508
        if self.__callbackDictCleanup > _TopicTreeRoot.callbackDeadLimit:
 
509
            self.__callbackDictCleanup = 0
 
510
            oldDict = self.__callbackDict
 
511
            self.__callbackDict = {}
 
512
            for weakCB, weakNodes in oldDict.iteritems():
 
513
                if weakCB() is not None:
 
514
                    self.__callbackDict[weakCB] = weakNodes
 
515
        
 
516
    def __getTreeNode(self, topic, make=False):
 
517
        """Return the tree node for 'topic' from the topic tree. If it 
 
518
        doesnt exist and make=True, create it first."""
 
519
        # if the all-topics, give root; 
 
520
        if topic == (ALL_TOPICS,):
 
521
            return self
 
522
            
 
523
        # not root, so traverse tree
 
524
        node = self
 
525
        path = ()
 
526
        for topicItem in topic:
 
527
            path += (topicItem,)
 
528
            if topicItem == ALL_TOPICS:
 
529
                raise ValueError, 'Topic tuple must not contain ""'
 
530
            if make: 
 
531
                node = node.createSubtopic(topicItem, path)
 
532
            elif node.hasSubtopic(topicItem):
 
533
                node = node.getNode(topicItem)
 
534
            else:
 
535
                return None
 
536
        # done
 
537
        return node
 
538
        
 
539
    def printCallbacks(self):
 
540
        strVal = ['Callbacks:\n']
 
541
        for listener, weakTopicNodes in self.__callbackDict.iteritems():
 
542
            topics = [topic() for topic in weakTopicNodes if topic() is not None]
 
543
            strVal.append('  %s: %s\n' % (_getCallableName(listener()), topics))
 
544
        return ''.join(strVal)
 
545
        
 
546
    def __str__(self):
 
547
        return 'all: %s' % _TopicTreeNode.__str__(self)
 
548
    
 
549
    
 
550
# -----------------------------------------------------------------------------
 
551
 
 
552
class _SingletonKey: pass
 
553
 
 
554
class PublisherClass:
 
555
    """
 
556
    The publish/subscribe manager.  It keeps track of which listeners
 
557
    are interested in which topics (see subscribe()), and sends a
 
558
    Message for a given topic to listeners that have subscribed to
 
559
    that topic, with optional user data (see sendMessage()).
 
560
    
 
561
    The three important concepts for Publisher are:
 
562
        
 
563
    - listener: a function, bound method or
 
564
      callable object that can be called with one parameter
 
565
      (not counting 'self' in the case of methods). The parameter
 
566
      will be a reference to a Message object. E.g., these listeners
 
567
      are ok::
 
568
          
 
569
          class Foo:
 
570
              def __call__(self, a, b=1): pass # can be called with only one arg
 
571
              def meth(self,  a):         pass # takes only one arg
 
572
              def meth2(self, a=2, b=''): pass # can be called with one arg
 
573
        
 
574
          def func(a, b=''): pass
 
575
          
 
576
          Foo foo
 
577
          Publisher().subscribe(foo)           # functor
 
578
          Publisher().subscribe(foo.meth)      # bound method
 
579
          Publisher().subscribe(foo.meth2)     # bound method
 
580
          Publisher().subscribe(func)          # function
 
581
          
 
582
      The three types of callables all have arguments that allow a call 
 
583
      with only one argument. In every case, the parameter 'a' will contain
 
584
      the message. 
 
585
 
 
586
    - topic: a single word, a tuple of words, or a string containing a
 
587
      set of words separated by dots, for example: 'sports.baseball'.
 
588
      A tuple or a dotted notation string denotes a hierarchy of
 
589
      topics from most general to least. For example, a listener of
 
590
      this topic::
 
591
 
 
592
          ('sports','baseball')
 
593
 
 
594
      would receive messages for these topics::
 
595
 
 
596
          ('sports', 'baseball')                 # because same
 
597
          ('sports', 'baseball', 'highscores')   # because more specific
 
598
 
 
599
      but not these::
 
600
 
 
601
           'sports'      # because more general
 
602
          ('sports',)    # because more general
 
603
          () or ('')     # because only for those listening to 'all' topics
 
604
          ('news')       # because different topic
 
605
          
 
606
    - message: this is an instance of Message, containing the topic for 
 
607
      which the message was sent, and any data the sender specified. 
 
608
      
 
609
    :note: This class is visible to importers of pubsub only as a
 
610
           Singleton. I.e., every time you execute 'Publisher()', it's
 
611
           actually the same instance of PublisherClass that is
 
612
           returned. So to use, just do'Publisher().method()'.
 
613
        
 
614
    """
 
615
    
 
616
    __ALL_TOPICS_TPL = (ALL_TOPICS, )
 
617
    
 
618
    def __init__(self, singletonKey):
 
619
        """Construct a Publisher. This can only be done by the pubsub 
 
620
        module. You just use pubsub.Publisher()."""
 
621
        if not isinstance(singletonKey, _SingletonKey):
 
622
            raise invalid_argument("Use Publisher() to get access to singleton")
 
623
        self.__messageCount  = 0
 
624
        self.__deliveryCount = 0
 
625
        self.__topicTree     = _TopicTreeRoot()
 
626
 
 
627
    #
 
628
    # Public API
 
629
    #
 
630
 
 
631
    def getDeliveryCount(self):
 
632
        """How many listeners have received a message since beginning of run"""
 
633
        return self.__deliveryCount
 
634
    
 
635
    def getMessageCount(self):
 
636
        """How many times sendMessage() was called since beginning of run"""
 
637
        return self.__messageCount
 
638
    
 
639
    def subscribe(self, listener, topic = ALL_TOPICS):
 
640
        """
 
641
        Subscribe listener for given topic. If topic is not specified,
 
642
        listener will be subscribed for all topics (that listener will 
 
643
        receive a Message for any topic for which a message is generated). 
 
644
        
 
645
        This method may be called multiple times for one listener,
 
646
        registering it with many topics.  It can also be invoked many
 
647
        times for a particular topic, each time with a different
 
648
        listener.  See the class doc for requirements on listener and
 
649
        topic.
 
650
 
 
651
        :note: The listener is held by Publisher() only by *weak*
 
652
               reference.  This means you must ensure you have at
 
653
               least one strong reference to listener, otherwise it
 
654
               will be DOA ("dead on arrival"). This is particularly
 
655
               easy to forget when wrapping a listener method in a
 
656
               proxy object (e.g. to bind some of its parameters),
 
657
               e.g.::
 
658
        
 
659
                  class Foo: 
 
660
                      def listener(self, event): pass
 
661
                  class Wrapper:
 
662
                      def __init__(self, fun): self.fun = fun
 
663
                      def __call__(self, *args): self.fun(*args)
 
664
                  foo = Foo()
 
665
                  Publisher().subscribe( Wrapper(foo.listener) ) # whoops: DOA!
 
666
                  wrapper = Wrapper(foo.listener)
 
667
                  Publisher().subscribe(wrapper) # good!
 
668
        
 
669
        :note: Calling this method for the same listener, with two
 
670
               topics in the same branch of the topic hierarchy, will
 
671
               cause the listener to be notified twice when a message
 
672
               for the deepest topic is sent. E.g.
 
673
               subscribe(listener, 't1') and then subscribe(listener,
 
674
               ('t1','t2')) means that when calling sendMessage('t1'),
 
675
               listener gets one message, but when calling
 
676
               sendMessage(('t1','t2')), listener gets message twice.
 
677
        
 
678
        """
 
679
        self.validate(listener)
 
680
 
 
681
        if topic is None: 
 
682
            raise TypeError, 'Topic must be either a word, tuple of '\
 
683
                             'words, or getStrAllTopics()'
 
684
            
 
685
        self.__topicTree.addTopic(_tupleize(topic), listener)
 
686
 
 
687
    def isSubscribed(self, listener, topic=None):
 
688
        """Return true if listener has subscribed to topic specified. 
 
689
        If no topic specified, return true if subscribed to something.
 
690
        Use topic=getStrAllTopics() to determine if a listener will receive 
 
691
        messages for all topics."""
 
692
        return self.__topicTree.isSubscribed(listener, topic)
 
693
            
 
694
    def validate(self, listener):
 
695
        """Similar to isValid(), but raises a TypeError exception if not valid"""
 
696
        # check callable
 
697
        if not callable(listener):
 
698
            raise TypeError, 'Listener '+`listener`+' must be a '\
 
699
                             'function, bound method or instance.'
 
700
        # ok, callable, but if method, is it bound:
 
701
        elif ismethod(listener) and not _isbound(listener):
 
702
            raise TypeError, 'Listener '+`listener`+\
 
703
                             ' is a method but it is unbound!'
 
704
                             
 
705
        # check that it takes the right number of parameters
 
706
        min, d = _paramMinCount(listener)
 
707
        if min > 1:
 
708
            raise TypeError, 'Listener '+`listener`+" can't"\
 
709
                             ' require more than one parameter!'
 
710
        if min <= 0 and d == 0:
 
711
            raise TypeError, 'Listener '+`listener`+' lacking arguments!'
 
712
                             
 
713
        assert (min == 0 and d>0) or (min == 1)
 
714
 
 
715
    def isValid(self, listener):
 
716
        """Return true only if listener will be able to subscribe to 
 
717
        Publisher."""
 
718
        try: 
 
719
            self.validate(listener)
 
720
            return True
 
721
        except TypeError:
 
722
            return False
 
723
 
 
724
    def unsubAll(self, topics=None, onNoSuchTopic=None):
 
725
        """Unsubscribe all listeners subscribed for topics. Topics can 
 
726
        be a single topic (string or tuple) or a list of topics (ie 
 
727
        list containing strings and/or tuples). If topics is not 
 
728
        specified, all listeners for all topics will be unsubscribed, 
 
729
        ie. the Publisher singleton will have no topics and no listeners
 
730
        left. If onNoSuchTopic is given, it will be called as 
 
731
        onNoSuchTopic(topic) for each topic that is unknown.
 
732
        """
 
733
        if topics is None: 
 
734
            del self.__topicTree
 
735
            self.__topicTree = _TopicTreeRoot()
 
736
            return
 
737
        
 
738
        # make sure every topics are in tuple form
 
739
        if isinstance(topics, list):
 
740
            topicList = [_tupleize(x) for x in topics]
 
741
        else:
 
742
            topicList = [_tupleize(topics)]
 
743
            
 
744
        # unsub every listener of topics
 
745
        self.__topicTree.unsubAll(topicList, onNoSuchTopic)
 
746
            
 
747
    def unsubscribe(self, listener, topics=None):
 
748
        """Unsubscribe listener. If topics not specified, listener is
 
749
        completely unsubscribed. Otherwise, it is unsubscribed only 
 
750
        for the topic (the usual tuple) or list of topics (ie a list
 
751
        of tuples) specified. Nothing happens if listener is not actually
 
752
        subscribed to any of the topics.
 
753
        
 
754
        Note that if listener subscribed for two topics (a,b) and (a,c), 
 
755
        then unsubscribing for topic (a) will do nothing. You must 
 
756
        use getAssociatedTopics(listener) and give unsubscribe() the returned 
 
757
        list (or a subset thereof).
 
758
        """
 
759
        self.validate(listener)
 
760
        topicList = None
 
761
        if topics is not None:
 
762
            if isinstance(topics, list):
 
763
                topicList = [_tupleize(x) for x in topics]
 
764
            else:
 
765
                topicList = [_tupleize(topics)]
 
766
            
 
767
        self.__topicTree.unsubscribe(listener, topicList)
 
768
        
 
769
    def getAssociatedTopics(self, listener):
 
770
        """Return a list of topics the given listener is registered with. 
 
771
        Returns [] if listener never subscribed.
 
772
        
 
773
        :attention: when using the return of this method to compare to
 
774
                expected list of topics, remember that topics that are
 
775
                not in the form of a tuple appear as a one-tuple in
 
776
                the return. E.g. if you have subscribed a listener to
 
777
                'topic1' and ('topic2','subtopic2'), this method
 
778
                returns::
 
779
            
 
780
                associatedTopics = [('topic1',), ('topic2','subtopic2')]
 
781
        """
 
782
        return self.__topicTree.getTopics(listener)
 
783
    
 
784
    def sendMessage(self, topic=ALL_TOPICS, data=None, onTopicNeverCreated=None):
 
785
        """Send a message for given topic, with optional data, to
 
786
        subscribed listeners. If topic is not specified, only the
 
787
        listeners that are interested in all topics will receive message. 
 
788
        The onTopicNeverCreated is an optional callback of your choice that 
 
789
        will be called if the topic given was never created (i.e. it, or 
 
790
        one of its subtopics, was never subscribed to by any listener). 
 
791
        It will be called as onTopicNeverCreated(topic)."""
 
792
        aTopic  = _tupleize(topic)
 
793
        message = Message(aTopic, data)
 
794
        self.__messageCount += 1
 
795
        
 
796
        # send to those who listen to all topics
 
797
        self.__deliveryCount += \
 
798
            self.__topicTree.sendMessage(aTopic, message, onTopicNeverCreated)
 
799
        
 
800
    #
 
801
    # Private methods
 
802
    #
 
803
 
 
804
    def __call__(self):
 
805
        """Allows for singleton"""
 
806
        return self
 
807
    
 
808
    def __str__(self):
 
809
        return str(self.__topicTree)
 
810
 
 
811
# Create the Publisher singleton. We prevent users from (inadvertently)
 
812
# instantiating more than one object, by requiring a key that is 
 
813
# accessible only to module.  From
 
814
# this point forward any calls to Publisher() will invoke the __call__
 
815
# of this instance which just returns itself.
 
816
#
 
817
# The only flaw with this approach is that you can't derive a new
 
818
# class from Publisher without jumping through hoops.  If this ever
 
819
# becomes an issue then a new Singleton implementaion will need to be
 
820
# employed.
 
821
_key = _SingletonKey()
 
822
Publisher = PublisherClass(_key)
 
823
 
 
824
 
 
825
#---------------------------------------------------------------------------
 
826
 
 
827
class Message:
 
828
    """
 
829
    A simple container object for the two components of a message: the 
 
830
    topic and the user data. An instance of Message is given to your 
 
831
    listener when called by Publisher().sendMessage(topic) (if your
 
832
    listener callback was registered for that topic).
 
833
    """
 
834
    def __init__(self, topic, data):
 
835
        self.topic = topic
 
836
        self.data  = data
 
837
 
 
838
    def __str__(self):
 
839
        return '[Topic: '+`self.topic`+',  Data: '+`self.data`+']'
 
840
 
 
841
 
 
842
#---------------------------------------------------------------------------
 
843
 
 
844
 
 
845
#
 
846
# Code for a simple command-line test
 
847
#
 
848
def test():
 
849
    def done(funcName):
 
850
        print '----------- Done %s -----------' % funcName
 
851
        
 
852
    def testParam():
 
853
        def testFunc00(): pass
 
854
        def testFunc21(a,b,c=1): pass
 
855
        def testFuncA(*args): pass
 
856
        def testFuncAK(*args,**kwds): pass
 
857
        def testFuncK(**kwds): pass
 
858
        
 
859
        class Foo:
 
860
            def testMeth(self,a,b): pass
 
861
            def __call__(self, a): pass
 
862
        class Foo2:
 
863
            def __call__(self, *args): pass
 
864
            
 
865
        assert _paramMinCount(testFunc00)==(0,0)
 
866
        assert _paramMinCount(testFunc21)==(2,1)
 
867
        assert _paramMinCount(testFuncA) ==(1,0)
 
868
        assert _paramMinCount(testFuncAK)==(1,0)
 
869
        assert _paramMinCount(testFuncK) ==(0,0)
 
870
        foo = Foo()
 
871
        assert _paramMinCount(Foo.testMeth)==(2,0)
 
872
        assert _paramMinCount(foo.testMeth)==(2,0)
 
873
        assert _paramMinCount(foo)==(1,0)
 
874
        assert _paramMinCount(Foo2())==(1,0)
 
875
    
 
876
        done('testParam')
 
877
 
 
878
    testParam()
 
879
    #------------------------
 
880
 
 
881
    _NodeCallback.notified = 0
 
882
    def testPreNotifyNode(self, dead):
 
883
        _NodeCallback.notified += 1
 
884
        print 'testPreNotifyNODE heard notification of', `dead`
 
885
    _NodeCallback.preNotify = testPreNotifyNode
 
886
    
 
887
    def testTreeNode():
 
888
 
 
889
        class WS:
 
890
            def __init__(self, s):
 
891
                self.s = s
 
892
            def __call__(self, msg):
 
893
                print 'WS#', self.s, ' received msg ', msg
 
894
            def __str__(self):
 
895
                return self.s
 
896
            
 
897
        def testPreNotifyRoot(dead):
 
898
            print 'testPreNotifyROOT heard notification of', `dead`
 
899
    
 
900
        node = _TopicTreeNode((ALL_TOPICS,), WeakRef(testPreNotifyRoot))
 
901
        boo, baz, bid = WS('boo'), WS('baz'), WS('bid')
 
902
        node.addCallable(boo)
 
903
        node.addCallable(baz)
 
904
        node.addCallable(boo)
 
905
        assert node.getCallables() == [boo,baz]
 
906
        assert node.hasCallable(boo)
 
907
        
 
908
        node.removeCallable(bid) # no-op
 
909
        assert node.hasCallable(baz)
 
910
        assert node.getCallables() == [boo,baz]
 
911
        
 
912
        node.removeCallable(boo)
 
913
        assert node.getCallables() == [baz]
 
914
        assert node.hasCallable(baz)
 
915
        assert not node.hasCallable(boo)
 
916
        
 
917
        node.removeCallable(baz)
 
918
        assert node.getCallables() == []
 
919
        assert not node.hasCallable(baz)
 
920
 
 
921
        node2 = node.createSubtopic('st1', ('st1',))
 
922
        node3 = node.createSubtopic('st2', ('st2',))
 
923
        cb1, cb2, cb = WS('st1_cb1'), WS('st1_cb2'), WS('st2_cb')
 
924
        node2.addCallable(cb1)
 
925
        node2.addCallable(cb2)
 
926
        node3.addCallable(cb)
 
927
        node2.createSubtopic('st3', ('st1','st3'))
 
928
        node2.createSubtopic('st4', ('st1','st4'))
 
929
       
 
930
        print str(node)
 
931
        assert str(node) == ' (st1: st1_cb1 st1_cb2  (st4: ) (st3: )) (st2: st2_cb )'
 
932
    
 
933
        # verify send message, and that a dead listener does not get sent one
 
934
        delivered = node2.sendMessage('hello')
 
935
        assert delivered == 2
 
936
        del cb1
 
937
        delivered = node2.sendMessage('hello')
 
938
        assert delivered == 1
 
939
        assert _NodeCallback.notified == 1
 
940
        
 
941
        done('testTreeNode')
 
942
        
 
943
    testTreeNode()
 
944
    #------------------------
 
945
    
 
946
    def testValidate():
 
947
        class Foo:
 
948
            def __call__(self, a):   pass
 
949
            def fun(self, b):        pass
 
950
            def fun2(self, b=1):     pass
 
951
            def fun3(self, a, b=2):  pass
 
952
            def badFun(self):        pass
 
953
            def badFun2():           pass
 
954
            def badFun3(self, a, b): pass
 
955
            
 
956
        server = Publisher()
 
957
        foo = Foo()
 
958
        server.validate(foo)
 
959
        server.validate(foo.fun)
 
960
        server.validate(foo.fun2)
 
961
        server.validate(foo.fun3)
 
962
        assert not server.isValid(foo.badFun)
 
963
        assert not server.isValid(foo.badFun2)
 
964
        assert not server.isValid(foo.badFun3)
 
965
    
 
966
        done('testValidate')
 
967
 
 
968
    testValidate()
 
969
    #------------------------
 
970
    
 
971
    class SimpleListener:
 
972
        def __init__(self, number):
 
973
            self.number = number
 
974
        def __call__(self, message = ''): 
 
975
            print 'Callable #%s got the message "%s"' %(self.number, message)
 
976
        def notify(self, message):
 
977
            print '%s.notify() got the message "%s"' %(self.number, message)
 
978
        def __str__(self):
 
979
            return "SimpleListener_%s" % self.number
 
980
 
 
981
    def testSubscribe():
 
982
        publisher = Publisher()
 
983
        
 
984
        topic1 = 'politics'
 
985
        topic2 = ('history','middle age')
 
986
        topic3 = ('politics','UN')
 
987
        topic4 = ('politics','NATO')
 
988
        topic5 = ('politics','NATO','US')
 
989
        
 
990
        lisnr1 = SimpleListener(1)
 
991
        lisnr2 = SimpleListener(2)
 
992
        def func(message, a=1): 
 
993
            print 'Func received message "%s"' % message
 
994
        lisnr3 = func
 
995
        lisnr4 = lambda x: 'Lambda received message "%s"' % x
 
996
 
 
997
        assert not publisher.isSubscribed(lisnr1)
 
998
        assert not publisher.isSubscribed(lisnr2)
 
999
        assert not publisher.isSubscribed(lisnr3)
 
1000
        assert not publisher.isSubscribed(lisnr4)
 
1001
        
 
1002
        publisher.subscribe(lisnr1, topic1)
 
1003
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,)]
 
1004
        publisher.subscribe(lisnr1, topic2)
 
1005
        publisher.subscribe(lisnr1, topic1) # do it again, should be no-op
 
1006
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2]
 
1007
        publisher.subscribe(lisnr2.notify, topic3)
 
1008
        assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3]
 
1009
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2]
 
1010
        publisher.subscribe(lisnr3, topic5)
 
1011
        assert publisher.getAssociatedTopics(lisnr3) == [topic5]
 
1012
        assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3]
 
1013
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2]
 
1014
        publisher.subscribe(lisnr4)
 
1015
        
 
1016
        print "Publisher tree: ", publisher
 
1017
        assert publisher.isSubscribed(lisnr1)
 
1018
        assert publisher.isSubscribed(lisnr1, topic1)
 
1019
        assert publisher.isSubscribed(lisnr1, topic2)
 
1020
        assert publisher.isSubscribed(lisnr2.notify)
 
1021
        assert publisher.isSubscribed(lisnr3, topic5)
 
1022
        assert publisher.isSubscribed(lisnr4, ALL_TOPICS)
 
1023
        expectTopicTree = 'all: <lambda>  (politics: SimpleListener_1  (UN: SimpleListener_2.notify ) (NATO:  (US: func ))) (history:  (middle age: SimpleListener_1 ))'
 
1024
        print "Publisher tree: ", publisher
 
1025
        assert str(publisher) == expectTopicTree
 
1026
        
 
1027
        publisher.unsubscribe(lisnr1, 'booboo') # should do nothing
 
1028
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2]
 
1029
        assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3]
 
1030
        assert publisher.getAssociatedTopics(lisnr3) == [topic5]
 
1031
        publisher.unsubscribe(lisnr1, topic1)
 
1032
        assert publisher.getAssociatedTopics(lisnr1) == [topic2]
 
1033
        assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3]
 
1034
        assert publisher.getAssociatedTopics(lisnr3) == [topic5]
 
1035
        publisher.unsubscribe(lisnr1, topic2)
 
1036
        publisher.unsubscribe(lisnr1, topic2)
 
1037
        publisher.unsubscribe(lisnr2.notify, topic3)
 
1038
        publisher.unsubscribe(lisnr3, topic5)
 
1039
        assert publisher.getAssociatedTopics(lisnr1) == []
 
1040
        assert publisher.getAssociatedTopics(lisnr2.notify) == []
 
1041
        assert publisher.getAssociatedTopics(lisnr3) == []
 
1042
        publisher.unsubscribe(lisnr4)
 
1043
        
 
1044
        expectTopicTree = 'all:  (politics:  (UN: ) (NATO:  (US: ))) (history:  (middle age: ))'
 
1045
        print "Publisher tree: ", publisher
 
1046
        assert str(publisher) == expectTopicTree
 
1047
        assert publisher.getDeliveryCount() == 0
 
1048
        assert publisher.getMessageCount() == 0
 
1049
        
 
1050
        publisher.unsubAll()
 
1051
        assert str(publisher) == 'all: '
 
1052
        
 
1053
        done('testSubscribe')
 
1054
    
 
1055
    testSubscribe()
 
1056
    #------------------------
 
1057
    
 
1058
    def testUnsubAll():
 
1059
        publisher = Publisher()
 
1060
        
 
1061
        topic1 = 'politics'
 
1062
        topic2 = ('history','middle age')
 
1063
        topic3 = ('politics','UN')
 
1064
        topic4 = ('politics','NATO')
 
1065
        topic5 = ('politics','NATO','US')
 
1066
        
 
1067
        lisnr1 = SimpleListener(1)
 
1068
        lisnr2 = SimpleListener(2)
 
1069
        def func(message, a=1): 
 
1070
            print 'Func received message "%s"' % message
 
1071
        lisnr3 = func
 
1072
        lisnr4 = lambda x: 'Lambda received message "%s"' % x
 
1073
 
 
1074
        publisher.subscribe(lisnr1, topic1)
 
1075
        publisher.subscribe(lisnr1, topic2)
 
1076
        publisher.subscribe(lisnr2.notify, topic3)
 
1077
        publisher.subscribe(lisnr3, topic2)
 
1078
        publisher.subscribe(lisnr3, topic5)
 
1079
        publisher.subscribe(lisnr4)
 
1080
        
 
1081
        expectTopicTree = 'all: <lambda>  (politics: SimpleListener_1  (UN: SimpleListener_2.notify ) (NATO:  (US: func ))) (history:  (middle age: SimpleListener_1 func ))'
 
1082
        print "Publisher tree: ", publisher
 
1083
        assert str(publisher) == expectTopicTree
 
1084
    
 
1085
        publisher.unsubAll(topic1)
 
1086
        assert publisher.getAssociatedTopics(lisnr1) == [topic2]
 
1087
        assert not publisher.isSubscribed(lisnr1, topic1)
 
1088
        
 
1089
        publisher.unsubAll(topic2)
 
1090
        print publisher
 
1091
        assert publisher.getAssociatedTopics(lisnr1) == []
 
1092
        assert publisher.getAssociatedTopics(lisnr3) == [topic5]
 
1093
        assert not publisher.isSubscribed(lisnr1)
 
1094
        assert publisher.isSubscribed(lisnr3, topic5)
 
1095
        
 
1096
        #print "Publisher tree: ", publisher
 
1097
        expectTopicTree = 'all: <lambda>  (politics:  (UN: SimpleListener_2.notify ) (NATO:  (US: func ))) (history:  (middle age: ))'
 
1098
        assert str(publisher) == expectTopicTree
 
1099
        publisher.unsubAll(ALL_TOPICS)
 
1100
        #print "Publisher tree: ", publisher
 
1101
        expectTopicTree = 'all:  (politics:  (UN: SimpleListener_2.notify ) (NATO:  (US: func ))) (history:  (middle age: ))'
 
1102
        assert str(publisher) == expectTopicTree
 
1103
        
 
1104
        publisher.unsubAll()
 
1105
        done('testUnsubAll')
 
1106
    
 
1107
    testUnsubAll()
 
1108
    #------------------------
 
1109
    
 
1110
    def testSend():
 
1111
        publisher = Publisher()
 
1112
        called = []
 
1113
        
 
1114
        class TestListener:
 
1115
            def __init__(self, num):
 
1116
                self.number = num
 
1117
            def __call__(self, b): 
 
1118
                called.append( 'TL%scb' % self.number )
 
1119
            def notify(self, b):
 
1120
                called.append( 'TL%sm' % self.number )
 
1121
        def funcListener(b):
 
1122
            called.append('func')
 
1123
            
 
1124
        lisnr1 = TestListener(1)
 
1125
        lisnr2 = TestListener(2)
 
1126
        lisnr3 = funcListener
 
1127
        lisnr4 = lambda x: called.append('lambda')
 
1128
 
 
1129
        topic1 = 'politics'
 
1130
        topic2 = 'history'
 
1131
        topic3 = ('politics','UN')
 
1132
        topic4 = ('politics','NATO','US')
 
1133
        topic5 = ('politics','NATO')
 
1134
        
 
1135
        publisher.subscribe(lisnr1, topic1)
 
1136
        publisher.subscribe(lisnr2, topic2)
 
1137
        publisher.subscribe(lisnr2.notify, topic2)
 
1138
        publisher.subscribe(lisnr3, topic4)
 
1139
        publisher.subscribe(lisnr4)
 
1140
        
 
1141
        print publisher
 
1142
        
 
1143
        # setup ok, now test send/receipt
 
1144
        publisher.sendMessage(topic1)
 
1145
        assert called == ['lambda','TL1cb']
 
1146
        called = []
 
1147
        publisher.sendMessage(topic2)
 
1148
        assert called == ['lambda','TL2cb','TL2m']
 
1149
        called = []
 
1150
        publisher.sendMessage(topic3)
 
1151
        assert called == ['lambda','TL1cb']
 
1152
        called = []
 
1153
        publisher.sendMessage(topic4)
 
1154
        assert called == ['lambda','TL1cb','func']
 
1155
        called = []
 
1156
        publisher.sendMessage(topic5)
 
1157
        assert called == ['lambda','TL1cb']
 
1158
        assert publisher.getDeliveryCount() == 12
 
1159
        assert publisher.getMessageCount() == 5
 
1160
    
 
1161
        # test weak referencing works:
 
1162
        _NodeCallback.notified = 0
 
1163
        del lisnr2
 
1164
        called = []
 
1165
        publisher.sendMessage(topic2)
 
1166
        assert called == ['lambda']
 
1167
        assert _NodeCallback.notified == 2
 
1168
        
 
1169
        done('testSend')
 
1170
        
 
1171
    testSend()
 
1172
    assert _NodeCallback.notified == 5
 
1173
    
 
1174
    def testDead():
 
1175
        # verify if weak references work as expected
 
1176
        print '------ Starting testDead ----------'
 
1177
        node = _TopicTreeNode('t1', None)
 
1178
        lisnr1 = SimpleListener(1)
 
1179
        lisnr2 = SimpleListener(2)
 
1180
        lisnr3 = SimpleListener(3)
 
1181
        lisnr4 = SimpleListener(4)
 
1182
 
 
1183
        node.addCallable(lisnr1)
 
1184
        node.addCallable(lisnr2)
 
1185
        node.addCallable(lisnr3)
 
1186
        node.addCallable(lisnr4)
 
1187
        
 
1188
        print 'Deleting listeners first'
 
1189
        _NodeCallback.notified = 0
 
1190
        del lisnr1
 
1191
        del lisnr2
 
1192
        assert _NodeCallback.notified == 2
 
1193
        
 
1194
        print 'Deleting node first'
 
1195
        _NodeCallback.notified = 0
 
1196
        del node
 
1197
        del lisnr3
 
1198
        del lisnr4
 
1199
        assert _NodeCallback.notified == 0
 
1200
        
 
1201
        lisnr1 = SimpleListener(1)
 
1202
        lisnr2 = SimpleListener(2)
 
1203
        lisnr3 = SimpleListener(3)
 
1204
        lisnr4 = SimpleListener(4)
 
1205
        
 
1206
        # try same with root of tree
 
1207
        node = _TopicTreeRoot()
 
1208
        node.addTopic(('',), lisnr1)
 
1209
        node.addTopic(('',), lisnr2)
 
1210
        node.addTopic(('',), lisnr3)
 
1211
        node.addTopic(('',), lisnr4)
 
1212
        # add objects that will die immediately to see if cleanup occurs
 
1213
        # this must be done visually as it is a low-level detail
 
1214
        _NodeCallback.notified = 0
 
1215
        _TopicTreeRoot.callbackDeadLimit = 3
 
1216
        node.addTopic(('',), SimpleListener(5))
 
1217
        node.addTopic(('',), SimpleListener(6))
 
1218
        node.addTopic(('',), SimpleListener(7))
 
1219
        print node.numListeners()
 
1220
        assert node.numListeners() == (4, 3)
 
1221
        node.addTopic(('',), SimpleListener(8))
 
1222
        assert node.numListeners() == (4, 0)
 
1223
        assert _NodeCallback.notified == 4
 
1224
        
 
1225
        print 'Deleting listeners first'
 
1226
        _NodeCallback.notified = 0
 
1227
        del lisnr1
 
1228
        del lisnr2
 
1229
        assert _NodeCallback.notified == 2
 
1230
        print 'Deleting node first'
 
1231
        _NodeCallback.notified = 0
 
1232
        del node
 
1233
        del lisnr3
 
1234
        del lisnr4
 
1235
        assert _NodeCallback.notified == 0
 
1236
        
 
1237
        done('testDead')
 
1238
    
 
1239
    testDead()
 
1240
    
 
1241
    print 'Exiting tests'
 
1242
#---------------------------------------------------------------------------
 
1243
 
 
1244
if __name__ == '__main__':
 
1245
    test()