into a topic tuple.  For example: subscribing to "timer.clock.seconds"
is the same as subscribing to ("timer", "clock", "seconds").
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@37831 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
		
	
		
			
				
	
	
		
			1188 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1188 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
"""
 | 
						|
This module provides a publish-subscribe component that allows
 | 
						|
listeners to subcribe to messages of a given topic. Contrary to the
 | 
						|
original wxPython.lib.pubsub module, which it is based on, it uses 
 | 
						|
weak referencing to the subscribers so the subscribers are not kept
 | 
						|
alive by the Publisher. Also, callable objects can be used in addition
 | 
						|
to functions and bound methods. See Publisher class docs for more
 | 
						|
details. 
 | 
						|
 | 
						|
Thanks to Robb Shecter and Robin Dunn for having provided 
 | 
						|
the basis for this module (which now shares most of the concepts but
 | 
						|
very little design or implementation with the original 
 | 
						|
wxPython.lib.pubsub).
 | 
						|
 | 
						|
:Author:      Oliver Schoenborn
 | 
						|
:Since:       Apr 2004
 | 
						|
:Version:     $Id$
 | 
						|
:Copyright:   \(c) 2004 Oliver Schoenborn
 | 
						|
:License:     wxWidgets
 | 
						|
 | 
						|
Implementation notes
 | 
						|
--------------------
 | 
						|
 | 
						|
In class Publisher, I represent the topics-listener set as a tree
 | 
						|
where each node is a topic, and contains a list of listeners of that
 | 
						|
topic, and a dictionary of subtopics of that topic. When the Publisher
 | 
						|
is told to send a message for a given topic, it traverses the tree
 | 
						|
down to the topic for which a message is being generated, all
 | 
						|
listeners on the way get sent the message.
 | 
						|
 | 
						|
Publisher currently uses a weak listener topic tree to store the
 | 
						|
topics for each listener, and if a listener dies before being
 | 
						|
unsubscribed, the tree is notified, and the tree eliminates the
 | 
						|
listener from itself.
 | 
						|
 | 
						|
Ideally, _TopicTreeNode would be a generic _TreeNode with named
 | 
						|
subnodes, and _TopicTreeRoot would be a generic _Tree with named
 | 
						|
nodes, and Publisher would store listeners in each node and a topic
 | 
						|
tuple would be converted to a path in the tree.  This would lead to a
 | 
						|
much cleaner separation of concerns. But time is over, time to move on.
 | 
						|
 | 
						|
"""
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
 | 
						|
# for function and method parameter counting:
 | 
						|
from types   import InstanceType
 | 
						|
from inspect import getargspec, ismethod, isfunction
 | 
						|
# for weakly bound methods:
 | 
						|
from new     import instancemethod as InstanceMethod
 | 
						|
from weakref import ref as WeakRef
 | 
						|
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
def _isbound(method):
 | 
						|
    """Return true if method is a bound method, false otherwise"""
 | 
						|
    assert ismethod(method)
 | 
						|
    return method.im_self is not None
 | 
						|
 | 
						|
 | 
						|
def _paramMinCountFunc(function):
 | 
						|
    """Given a function, return pair (min,d) where min is minimum # of
 | 
						|
    args required, and d is number of default arguments."""
 | 
						|
    assert isfunction(function)
 | 
						|
    (args, va, kwa, dflt) = getargspec(function)
 | 
						|
    lenDef = len(dflt or ())
 | 
						|
    return (len(args or ()) - lenDef, lenDef)
 | 
						|
 | 
						|
 | 
						|
def _paramMinCount(callableObject):
 | 
						|
    """
 | 
						|
    Given a callable object (function, method or callable instance),
 | 
						|
    return pair (min,d) where min is minimum # of args required, and d
 | 
						|
    is number of default arguments. The 'self' parameter, in the case
 | 
						|
    of methods, is not counted.
 | 
						|
    """
 | 
						|
    if type(callableObject) is InstanceType:
 | 
						|
        min, d = _paramMinCountFunc(callableObject.__call__.im_func)
 | 
						|
        return min-1, d
 | 
						|
    elif ismethod(callableObject):
 | 
						|
        min, d = _paramMinCountFunc(callableObject.im_func)
 | 
						|
        return min-1, d
 | 
						|
    elif isfunction(callableObject):
 | 
						|
        return _paramMinCountFunc(callableObject)
 | 
						|
    else:
 | 
						|
        raise 'Cannot determine type of callable: '+repr(callableObject)
 | 
						|
 | 
						|
 | 
						|
def _tupleize(items):
 | 
						|
    """Convert items to tuple if not already one, 
 | 
						|
    so items must be a list, tuple or non-sequence"""
 | 
						|
    if isinstance(items, list):
 | 
						|
        raise TypeError, 'Not allowed to tuple-ize a list'
 | 
						|
    elif isinstance(items, (str, unicode)) and items.find('.') != -1:
 | 
						|
        items = tuple(items.split('.'))
 | 
						|
    elif not isinstance(items, tuple):
 | 
						|
        items = (items,)
 | 
						|
    return items
 | 
						|
 | 
						|
 | 
						|
def _getCallableName(callable):
 | 
						|
    """Get name for a callable, ie function, bound 
 | 
						|
    method or callable instance"""
 | 
						|
    if ismethod(callable):
 | 
						|
        return '%s.%s ' % (callable.im_self, callable.im_func.func_name)
 | 
						|
    elif isfunction(callable):
 | 
						|
        return '%s ' % callable.__name__
 | 
						|
    else:
 | 
						|
        return '%s ' % callable
 | 
						|
    
 | 
						|
    
 | 
						|
def _removeItem(item, fromList):
 | 
						|
    """Attempt to remove item from fromList, return true 
 | 
						|
    if successful, false otherwise."""
 | 
						|
    try: 
 | 
						|
        fromList.remove(item)
 | 
						|
        return True
 | 
						|
    except ValueError:
 | 
						|
        return False
 | 
						|
        
 | 
						|
        
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
class _WeakMethod:
 | 
						|
    """Represent a weak bound method, i.e. a method doesn't keep alive the 
 | 
						|
    object that it is bound to. It uses WeakRef which, used on its own, 
 | 
						|
    produces weak methods that are dead on creation, not very useful. 
 | 
						|
    Typically, you will use the getRef() function instead of using
 | 
						|
    this class directly. """
 | 
						|
    
 | 
						|
    def __init__(self, method, notifyDead = None):
 | 
						|
        """The method must be bound. notifyDead will be called when 
 | 
						|
        object that method is bound to dies. """
 | 
						|
        assert ismethod(method)
 | 
						|
        if method.im_self is None:
 | 
						|
            raise ValueError, "We need a bound method!"
 | 
						|
        if notifyDead is None:
 | 
						|
            self.objRef = WeakRef(method.im_self)
 | 
						|
        else:
 | 
						|
            self.objRef = WeakRef(method.im_self, notifyDead)
 | 
						|
        self.fun = method.im_func
 | 
						|
        self.cls = method.im_class
 | 
						|
        
 | 
						|
    def __call__(self):
 | 
						|
        """Returns a new.instancemethod if object for method still alive. 
 | 
						|
        Otherwise return None. Note that instancemethod causes a 
 | 
						|
        strong reference to object to be created, so shouldn't save 
 | 
						|
        the return value of this call. Note also that this __call__
 | 
						|
        is required only for compatibility with WeakRef.ref(), otherwise
 | 
						|
        there would be more efficient ways of providing this functionality."""
 | 
						|
        if self.objRef() is None:
 | 
						|
            return None
 | 
						|
        else:
 | 
						|
            return InstanceMethod(self.fun, self.objRef(), self.cls)
 | 
						|
        
 | 
						|
    def __cmp__(self, method2):
 | 
						|
        """Two _WeakMethod objects compare equal if they refer to the same method
 | 
						|
        of the same instance."""
 | 
						|
        return hash(self) - hash(method2)
 | 
						|
    
 | 
						|
    def __hash__(self):
 | 
						|
        """Hash must depend on WeakRef of object, and on method, so that
 | 
						|
        separate methods, bound to same object, can be distinguished. 
 | 
						|
        I'm not sure how robust this hash function is, any feedback 
 | 
						|
        welcome."""
 | 
						|
        return hash(self.fun)/2 + hash(self.objRef)/2
 | 
						|
    
 | 
						|
    def __repr__(self):
 | 
						|
        dead = ''
 | 
						|
        if self.objRef() is None: 
 | 
						|
            dead = '; DEAD'
 | 
						|
        obj = '<%s at %s%s>' % (self.__class__, id(self), dead)
 | 
						|
        return obj
 | 
						|
        
 | 
						|
    def refs(self, weakRef):
 | 
						|
        """Return true if we are storing same object referred to by weakRef."""
 | 
						|
        return self.objRef == weakRef
 | 
						|
 | 
						|
 | 
						|
def _getWeakRef(obj, notifyDead=None):
 | 
						|
    """Get a weak reference to obj. If obj is a bound method, a _WeakMethod
 | 
						|
    object, that behaves like a WeakRef, is returned, if it is
 | 
						|
    anything else a WeakRef is returned. If obj is an unbound method,
 | 
						|
    a ValueError will be raised."""
 | 
						|
    if ismethod(obj):
 | 
						|
        createRef = _WeakMethod
 | 
						|
    else:
 | 
						|
        createRef = WeakRef
 | 
						|
        
 | 
						|
    if notifyDead is None:
 | 
						|
        return createRef(obj)
 | 
						|
    else:
 | 
						|
        return createRef(obj, notifyDead)
 | 
						|
    
 | 
						|
    
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
def getStrAllTopics():
 | 
						|
    """Function to call if, for whatever reason, you need to know 
 | 
						|
    explicitely what is the string to use to indicate 'all topics'."""
 | 
						|
    return ''
 | 
						|
 | 
						|
 | 
						|
# alias, easier to see where used
 | 
						|
ALL_TOPICS = getStrAllTopics()
 | 
						|
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
 | 
						|
class _NodeCallback:
 | 
						|
    """Encapsulate a weak reference to a method of a TopicTreeNode
 | 
						|
    in such a way that the method can be called, if the node is 
 | 
						|
    still alive, but the callback does not *keep* the node alive.
 | 
						|
    Also, define two methods, preNotify() and noNotify(), which can 
 | 
						|
    be redefined to something else, very useful for testing. 
 | 
						|
    """
 | 
						|
    
 | 
						|
    def __init__(self, obj):
 | 
						|
        self.objRef = _getWeakRef(obj)
 | 
						|
        
 | 
						|
    def __call__(self, weakCB):
 | 
						|
        notify = self.objRef()
 | 
						|
        if notify is not None: 
 | 
						|
            self.preNotify(weakCB)
 | 
						|
            notify(weakCB)
 | 
						|
        else: 
 | 
						|
            self.noNotify()
 | 
						|
            
 | 
						|
    def preNotify(self, dead):
 | 
						|
        """'Gets called just before our callback (self.objRef) is called"""
 | 
						|
        pass
 | 
						|
        
 | 
						|
    def noNotify(self):
 | 
						|
        """Gets called if the TopicTreeNode for this callback is dead"""
 | 
						|
        pass
 | 
						|
 | 
						|
 | 
						|
class _TopicTreeNode:
 | 
						|
    """A node in the topic tree. This contains a list of callables
 | 
						|
    that are interested in the topic that this node is associated
 | 
						|
    with, and contains a dictionary of subtopics, whose associated
 | 
						|
    values are other _TopicTreeNodes. The topic of a node is not stored
 | 
						|
    in the node, so that the tree can be implemented as a dictionary
 | 
						|
    rather than a list, for ease of use (and, likely, performance).
 | 
						|
    
 | 
						|
    Note that it uses _NodeCallback to encapsulate a callback for 
 | 
						|
    when a registered listener dies, possible thanks to WeakRef.
 | 
						|
    Whenever this callback is called, the onDeadListener() function, 
 | 
						|
    passed in at construction time, is called (unless it is None).
 | 
						|
    """
 | 
						|
    
 | 
						|
    def __init__(self, topicPath, onDeadListenerWeakCB):
 | 
						|
        self.__subtopics = {}
 | 
						|
        self.__callables = []
 | 
						|
        self.__topicPath = topicPath
 | 
						|
        self.__onDeadListenerWeakCB = onDeadListenerWeakCB
 | 
						|
        
 | 
						|
    def getPathname(self): 
 | 
						|
        """The complete node path to us, ie., the topic tuple that would lead to us"""
 | 
						|
        return self.__topicPath
 | 
						|
    
 | 
						|
    def createSubtopic(self, subtopic, topicPath):
 | 
						|
        """Create a child node for subtopic"""
 | 
						|
        return self.__subtopics.setdefault(subtopic,
 | 
						|
                    _TopicTreeNode(topicPath, self.__onDeadListenerWeakCB))
 | 
						|
    
 | 
						|
    def hasSubtopic(self, subtopic):
 | 
						|
        """Return true only if topic string is one of subtopics of this node"""
 | 
						|
        return self.__subtopics.has_key(subtopic)
 | 
						|
    
 | 
						|
    def getNode(self, subtopic):
 | 
						|
        """Return ref to node associated with subtopic"""
 | 
						|
        return self.__subtopics[subtopic]
 | 
						|
    
 | 
						|
    def addCallable(self, callable):
 | 
						|
        """Add a callable to list of callables for this topic node"""
 | 
						|
        try:
 | 
						|
            id = self.__callables.index(_getWeakRef(callable))
 | 
						|
            return self.__callables[id]
 | 
						|
        except ValueError:
 | 
						|
            wrCall = _getWeakRef(callable, _NodeCallback(self.__notifyDead))
 | 
						|
            self.__callables.append(wrCall)
 | 
						|
            return wrCall
 | 
						|
            
 | 
						|
    def getCallables(self):
 | 
						|
        """Get callables associated with this topic node"""
 | 
						|
        return [cb() for cb in self.__callables if cb() is not None]
 | 
						|
    
 | 
						|
    def hasCallable(self, callable):
 | 
						|
        """Return true if callable in this node"""
 | 
						|
        try: 
 | 
						|
            self.__callables.index(_getWeakRef(callable))
 | 
						|
            return True
 | 
						|
        except ValueError:
 | 
						|
            return False
 | 
						|
    
 | 
						|
    def sendMessage(self, message):
 | 
						|
        """Send a message to our callables"""
 | 
						|
        deliveryCount = 0
 | 
						|
        for cb in self.__callables:
 | 
						|
            listener = cb()
 | 
						|
            if listener is not None:
 | 
						|
                listener(message)
 | 
						|
                deliveryCount += 1
 | 
						|
        return deliveryCount
 | 
						|
    
 | 
						|
    def removeCallable(self, callable):
 | 
						|
        """Remove weak callable from our node (and return True). 
 | 
						|
        Does nothing if not here (and returns False)."""
 | 
						|
        try: 
 | 
						|
            self.__callables.remove(_getWeakRef(callable))
 | 
						|
            return True
 | 
						|
        except ValueError:
 | 
						|
            return False
 | 
						|
        
 | 
						|
    def clearCallables(self):
 | 
						|
        """Abandon list of callables to caller. We no longer have 
 | 
						|
        any callables after this method is called."""
 | 
						|
        tmpList = [cb for cb in self.__callables if cb() is not None]
 | 
						|
        self.__callables = []
 | 
						|
        return tmpList
 | 
						|
        
 | 
						|
    def __notifyDead(self, dead):
 | 
						|
        """Gets called when a listener dies, thanks to WeakRef"""
 | 
						|
        #print 'TreeNODE', `self`, 'received death certificate for ', dead
 | 
						|
        self.__cleanupDead()
 | 
						|
        if self.__onDeadListenerWeakCB is not None:
 | 
						|
            cb = self.__onDeadListenerWeakCB()
 | 
						|
            if cb is not None: 
 | 
						|
                cb(dead)
 | 
						|
        
 | 
						|
    def __cleanupDead(self):
 | 
						|
        """Remove all dead objects from list of callables"""
 | 
						|
        self.__callables = [cb for cb in self.__callables if cb() is not None]
 | 
						|
        
 | 
						|
    def __str__(self):
 | 
						|
        """Print us in a not-so-friendly, but readable way, good for debugging."""
 | 
						|
        strVal = []
 | 
						|
        for callable in self.getCallables():
 | 
						|
            strVal.append(_getCallableName(callable))
 | 
						|
        for topic, node in self.__subtopics.iteritems():
 | 
						|
            strVal.append(' (%s: %s)' %(topic, node))
 | 
						|
        return ''.join(strVal)
 | 
						|
      
 | 
						|
      
 | 
						|
class _TopicTreeRoot(_TopicTreeNode):
 | 
						|
    """
 | 
						|
    The root of the tree knows how to access other node of the 
 | 
						|
    tree and is the gateway of the tree user to the tree nodes. 
 | 
						|
    It can create topics, and and remove callbacks, etc. 
 | 
						|
    
 | 
						|
    For efficiency, it stores a dictionary of listener-topics, 
 | 
						|
    so that unsubscribing a listener just requires finding the 
 | 
						|
    topics associated to a listener, and finding the corresponding
 | 
						|
    nodes of the tree. Without it, unsubscribing would require 
 | 
						|
    that we search the whole tree for all nodes that contain 
 | 
						|
    given listener. Since Publisher is a singleton, it will 
 | 
						|
    contain all topics in the system so it is likely to be a large
 | 
						|
    tree. However, it is possible that in some runs, unsubscribe()
 | 
						|
    is called very little by the user, in which case most unsubscriptions
 | 
						|
    are automatic, ie caused by the listeners dying. In this case, 
 | 
						|
    a flag is set to indicate that the dictionary should be cleaned up
 | 
						|
    at the next opportunity. This is not necessary, it is just an 
 | 
						|
    optimization.
 | 
						|
    """
 | 
						|
    
 | 
						|
    def __init__(self):
 | 
						|
        self.__callbackDict  = {}
 | 
						|
        self.__callbackDictCleanup = 0
 | 
						|
        # all child nodes will call our __rootNotifyDead method
 | 
						|
        # when one of their registered listeners dies 
 | 
						|
        _TopicTreeNode.__init__(self, (ALL_TOPICS,), 
 | 
						|
                                _getWeakRef(self.__rootNotifyDead))
 | 
						|
        
 | 
						|
    def addTopic(self, topic, listener):
 | 
						|
        """Add topic to tree if doesnt exist, and add listener to topic node"""
 | 
						|
        assert isinstance(topic, tuple)
 | 
						|
        topicNode = self.__getTreeNode(topic, make=True)
 | 
						|
        weakCB = topicNode.addCallable(listener)
 | 
						|
        assert topicNode.hasCallable(listener)
 | 
						|
 | 
						|
        theList = self.__callbackDict.setdefault(weakCB, [])
 | 
						|
        assert self.__callbackDict.has_key(weakCB)
 | 
						|
        # add it only if we don't already have it
 | 
						|
        try:
 | 
						|
            weakTopicNode = WeakRef(topicNode)
 | 
						|
            theList.index(weakTopicNode)
 | 
						|
        except ValueError:
 | 
						|
            theList.append(weakTopicNode)
 | 
						|
        assert self.__callbackDict[weakCB].index(weakTopicNode) >= 0
 | 
						|
        
 | 
						|
    def getTopics(self, listener):
 | 
						|
        """Return the list of topics for given listener"""
 | 
						|
        weakNodes = self.__callbackDict.get(_getWeakRef(listener), [])
 | 
						|
        return [weakNode().getPathname() for weakNode in weakNodes 
 | 
						|
                if weakNode() is not None]
 | 
						|
 | 
						|
    def isSubscribed(self, listener, topic=None):
 | 
						|
        """Return true if listener is registered for topic specified. 
 | 
						|
        If no topic specified, return true if subscribed to something.
 | 
						|
        Use topic=getStrAllTopics() to determine if a listener will receive 
 | 
						|
        messages for all topics."""
 | 
						|
        weakCB = _getWeakRef(listener)
 | 
						|
        if topic is None: 
 | 
						|
            return self.__callbackDict.has_key(weakCB)
 | 
						|
        else:
 | 
						|
            topicPath = _tupleize(topic)
 | 
						|
            for weakNode in self.__callbackDict[weakCB]:
 | 
						|
                if topicPath == weakNode().getPathname():
 | 
						|
                    return True
 | 
						|
            return False
 | 
						|
            
 | 
						|
    def unsubscribe(self, listener, topicList):
 | 
						|
        """Remove listener from given list of topics. If topicList
 | 
						|
        doesn't have any topics for which listener has subscribed,
 | 
						|
        the onNotSubscribed callback, if not None, will be called,
 | 
						|
        as onNotSubscribed(listener, topic)."""
 | 
						|
        weakCB = _getWeakRef(listener)
 | 
						|
        if not self.__callbackDict.has_key(weakCB):
 | 
						|
            return
 | 
						|
        
 | 
						|
        cbNodes = self.__callbackDict[weakCB] 
 | 
						|
        if topicList is None:
 | 
						|
            for weakNode in cbNodes:
 | 
						|
                weakNode().removeCallable(listener)
 | 
						|
            del self.__callbackDict[weakCB] 
 | 
						|
            return
 | 
						|
 | 
						|
        for weakNode in cbNodes:
 | 
						|
            node = weakNode()
 | 
						|
            if node is not None and node.getPathname() in topicList:
 | 
						|
                success = node.removeCallable(listener)
 | 
						|
                assert success == True
 | 
						|
                cbNodes.remove(weakNode)
 | 
						|
                assert not self.isSubscribed(listener, node.getPathname())
 | 
						|
 | 
						|
    def unsubAll(self, topicList, onNoSuchTopic):
 | 
						|
        """Unsubscribe all listeners registered for any topic in 
 | 
						|
        topicList. If a topic in the list does not exist, and 
 | 
						|
        onNoSuchTopic is not None, a call
 | 
						|
        to onNoSuchTopic(topic) is done for that topic."""
 | 
						|
        for topic in topicList:
 | 
						|
            node = self.__getTreeNode(topic)
 | 
						|
            if node is not None:
 | 
						|
                weakCallables = node.clearCallables()
 | 
						|
                for callable in weakCallables:
 | 
						|
                    weakNodes = self.__callbackDict[callable]
 | 
						|
                    success = _removeItem(WeakRef(node), weakNodes)
 | 
						|
                    assert success == True
 | 
						|
                    if weakNodes == []:
 | 
						|
                        del self.__callbackDict[callable]
 | 
						|
            elif onNoSuchTopic is not None: 
 | 
						|
                onNoSuchTopic(topic)
 | 
						|
            
 | 
						|
    def sendMessage(self, topic, message, onTopicNeverCreated):
 | 
						|
        """Send a message for given topic to all registered listeners. If 
 | 
						|
        topic doesn't exist, call onTopicNeverCreated(topic)."""
 | 
						|
        # send to the all-toipcs listeners
 | 
						|
        deliveryCount = _TopicTreeNode.sendMessage(self, message)
 | 
						|
        # send to those who listen to given topic or any of its supertopics
 | 
						|
        node = self
 | 
						|
        for topicItem in topic:
 | 
						|
            assert topicItem != ''
 | 
						|
            if node.hasSubtopic(topicItem):
 | 
						|
                node = node.getNode(topicItem)
 | 
						|
                deliveryCount += node.sendMessage(message)
 | 
						|
            else: # topic never created, don't bother continuing
 | 
						|
                if onTopicNeverCreated is not None:
 | 
						|
                    onTopicNeverCreated(aTopic)
 | 
						|
                break
 | 
						|
        return deliveryCount
 | 
						|
 | 
						|
    def numListeners(self):
 | 
						|
        """Return a pair (live, dead) with count of live and dead listeners in tree"""
 | 
						|
        dead, live = 0, 0
 | 
						|
        for cb in self.__callbackDict:
 | 
						|
            if cb() is None: 
 | 
						|
                dead += 1
 | 
						|
            else:
 | 
						|
                live += 1
 | 
						|
        return live, dead
 | 
						|
    
 | 
						|
    # clean up the callback dictionary after how many dead listeners
 | 
						|
    callbackDeadLimit = 10
 | 
						|
 | 
						|
    def __rootNotifyDead(self, dead):
 | 
						|
        #print 'TreeROOT received death certificate for ', dead
 | 
						|
        self.__callbackDictCleanup += 1
 | 
						|
        if self.__callbackDictCleanup > _TopicTreeRoot.callbackDeadLimit:
 | 
						|
            self.__callbackDictCleanup = 0
 | 
						|
            oldDict = self.__callbackDict
 | 
						|
            self.__callbackDict = {}
 | 
						|
            for weakCB, weakNodes in oldDict.iteritems():
 | 
						|
                if weakCB() is not None:
 | 
						|
                    self.__callbackDict[weakCB] = weakNodes
 | 
						|
        
 | 
						|
    def __getTreeNode(self, topic, make=False):
 | 
						|
        """Return the tree node for 'topic' from the topic tree. If it 
 | 
						|
        doesnt exist and make=True, create it first."""
 | 
						|
        # if the all-topics, give root; 
 | 
						|
        if topic == (ALL_TOPICS,):
 | 
						|
            return self
 | 
						|
            
 | 
						|
        # not root, so traverse tree
 | 
						|
        node = self
 | 
						|
        path = ()
 | 
						|
        for topicItem in topic:
 | 
						|
            path += (topicItem,)
 | 
						|
            if topicItem == ALL_TOPICS:
 | 
						|
                raise ValueError, 'Topic tuple must not contain ""'
 | 
						|
            if make: 
 | 
						|
                node = node.createSubtopic(topicItem, path)
 | 
						|
            elif node.hasSubtopic(topicItem):
 | 
						|
                node = node.getNode(topicItem)
 | 
						|
            else:
 | 
						|
                return None
 | 
						|
        # done
 | 
						|
        return node
 | 
						|
        
 | 
						|
    def printCallbacks(self):
 | 
						|
        strVal = ['Callbacks:\n']
 | 
						|
        for listener, weakTopicNodes in self.__callbackDict.iteritems():
 | 
						|
            topics = [topic() for topic in weakTopicNodes if topic() is not None]
 | 
						|
            strVal.append('  %s: %s\n' % (_getCallableName(listener()), topics))
 | 
						|
        return ''.join(strVal)
 | 
						|
        
 | 
						|
    def __str__(self):
 | 
						|
        return 'all: %s' % _TopicTreeNode.__str__(self)
 | 
						|
    
 | 
						|
    
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
class Publisher:
 | 
						|
    """
 | 
						|
    The publish/subscribe manager.  It keeps track of which listeners
 | 
						|
    are interested in which topics (see subscribe()), and sends a
 | 
						|
    Message for a given topic to listeners that have subscribed to
 | 
						|
    that topic, with optional user data (see sendMessage()).
 | 
						|
    
 | 
						|
    The three important concepts for Publisher are:
 | 
						|
        
 | 
						|
    - listener: a function, bound method or
 | 
						|
      callable object that can be called with only one parameter
 | 
						|
      (not counting 'self' in the case of methods). The parameter
 | 
						|
      will be a reference to a Message object. E.g., these listeners
 | 
						|
      are ok::
 | 
						|
          
 | 
						|
          class Foo:
 | 
						|
              def __call__(self, a, b=1): pass # can be called with only one arg
 | 
						|
              def meth(self,  a):         pass # takes only one arg
 | 
						|
              def meth2(self, a=2, b=''): pass # can be called with one arg
 | 
						|
        
 | 
						|
          def func(a, b=''): pass
 | 
						|
          
 | 
						|
          Foo foo
 | 
						|
          Publisher().subscribe(foo)           # functor
 | 
						|
          Publisher().subscribe(foo.meth)      # bound method
 | 
						|
          Publisher().subscribe(foo.meth2)     # bound method
 | 
						|
          Publisher().subscribe(func)          # function
 | 
						|
          
 | 
						|
      The three types of callables all have arguments that allow a call 
 | 
						|
      with only one argument. In every case, the parameter 'a' will contain
 | 
						|
      the message. 
 | 
						|
 | 
						|
    - topic: a single word, a tuple of words, or a string containing a
 | 
						|
      set of words separated by dots, for example: 'sports.baseball'.
 | 
						|
      A tuple or a dotted notation string denotes a hierarchy of
 | 
						|
      topics from most general to least. For example, a listener of
 | 
						|
      this topic::
 | 
						|
 | 
						|
          ('sports','baseball')
 | 
						|
 | 
						|
      would receive messages for these topics::
 | 
						|
 | 
						|
          ('sports', 'baseball')                 # because same
 | 
						|
          ('sports', 'baseball', 'highscores')   # because more specific
 | 
						|
 | 
						|
      but not these::
 | 
						|
 | 
						|
           'sports'      # because more general
 | 
						|
          ('sports',)    # because more general
 | 
						|
          () or ('')     # because only for those listening to 'all' topics
 | 
						|
          ('news')       # because different topic
 | 
						|
          
 | 
						|
    - message: this is an instance of Message, containing the topic for 
 | 
						|
      which the message was sent, and any data the sender specified. 
 | 
						|
      
 | 
						|
    :note: This class is visible to importers of pubsub only as a 
 | 
						|
    Singleton. I.e., every time you execute 'Publisher()', it's 
 | 
						|
    actually the same instance of publisher that is returned. So to 
 | 
						|
    use, just do 'Publisher().method()'.
 | 
						|
    """
 | 
						|
    
 | 
						|
    __ALL_TOPICS_TPL = (ALL_TOPICS, )
 | 
						|
    
 | 
						|
    def __init__(self):
 | 
						|
        self.__messageCount  = 0
 | 
						|
        self.__deliveryCount = 0
 | 
						|
        self.__topicTree     = _TopicTreeRoot()
 | 
						|
 | 
						|
    #
 | 
						|
    # Public API
 | 
						|
    #
 | 
						|
 | 
						|
    def getDeliveryCount(self):
 | 
						|
        """How many listeners have received a message since beginning of run"""
 | 
						|
        return self.__deliveryCount
 | 
						|
    
 | 
						|
    def getMessageCount(self):
 | 
						|
        """How many times sendMessage() was called since beginning of run"""
 | 
						|
        return self.__messageCount
 | 
						|
    
 | 
						|
    def subscribe(self, listener, topic = ALL_TOPICS):
 | 
						|
        """
 | 
						|
        Subscribe listener for given topic. If topic is not specified,
 | 
						|
        listener will be subscribed for all topics (that listener will 
 | 
						|
        receive a Message for any topic for which a message is generated). 
 | 
						|
        
 | 
						|
        This method may be called multiple times for one listener,
 | 
						|
        registering it with many topics.  It can also be invoked many
 | 
						|
        times for a particular topic, each time with a different
 | 
						|
        listener.  See the class doc for requirements on listener and
 | 
						|
        topic.
 | 
						|
 | 
						|
        :note: Calling 
 | 
						|
        this method for the same listener, with two topics in the same 
 | 
						|
        branch of the topic hierarchy, will cause the listener to be
 | 
						|
        notified twice when a message for the deepest topic is sent. E.g.
 | 
						|
        subscribe(listener, 't1') and then subscribe(listener, ('t1','t2'))
 | 
						|
        means that when calling sendMessage('t1'), listener gets one message,
 | 
						|
        but when calling sendMessage(('t1','t2')), listener gets message 
 | 
						|
        twice. This effect could be eliminated but it would not be safe to 
 | 
						|
        do so: how do we know what topic to give the listener? Answer appears
 | 
						|
        trivial at first but is far from obvious. It is best to rely on the
 | 
						|
        user to be careful about who registers for what topics. 
 | 
						|
        """
 | 
						|
        self.validate(listener)
 | 
						|
 | 
						|
        if topic is None: 
 | 
						|
            raise TypeError, 'Topic must be either a word, tuple of '\
 | 
						|
                             'words, or getStrAllTopics()'
 | 
						|
            
 | 
						|
        self.__topicTree.addTopic(_tupleize(topic), listener)
 | 
						|
 | 
						|
    def isSubscribed(self, listener, topic=None):
 | 
						|
        """Return true if listener has subscribed to topic specified. 
 | 
						|
        If no topic specified, return true if subscribed to something.
 | 
						|
        Use getStrAllTopics() to determine if a listener will receive 
 | 
						|
        messages for all topics."""
 | 
						|
        return self.__topicTree.isSubscribed(listener, topic)
 | 
						|
            
 | 
						|
    def validate(self, listener):
 | 
						|
        """Similar to isValid(), but raises a TypeError exception if not valid"""
 | 
						|
        # check callable
 | 
						|
        if not callable(listener):
 | 
						|
            raise TypeError, 'Listener '+`listener`+' must be a '\
 | 
						|
                             'function, bound method or instance.'
 | 
						|
        # ok, callable, but if method, is it bound:
 | 
						|
        elif ismethod(listener) and not _isbound(listener):
 | 
						|
            raise TypeError, 'Listener '+`listener`+\
 | 
						|
                             ' is a method but it is unbound!'
 | 
						|
                             
 | 
						|
        # check that it takes the right number of parameters
 | 
						|
        min, d = _paramMinCount(listener)
 | 
						|
        if min > 1:
 | 
						|
            raise TypeError, 'Listener '+`listener`+" can't"\
 | 
						|
                             ' require more than one parameter!'
 | 
						|
        if min <= 0 and d == 0:
 | 
						|
            raise TypeError, 'Listener '+`listener`+' lacking arguments!'
 | 
						|
                             
 | 
						|
        assert (min == 0 and d>0) or (min == 1)
 | 
						|
 | 
						|
    def isValid(self, listener):
 | 
						|
        """Return true only if listener will be able to subscribe to Publisher."""
 | 
						|
        try: 
 | 
						|
            self.validate(listener)
 | 
						|
            return True
 | 
						|
        except TypeError:
 | 
						|
            return False
 | 
						|
 | 
						|
    def unsubAll(self, topics=None, onNoSuchTopic=None):
 | 
						|
        """Unsubscribe all listeners subscribed for topics. Topics can 
 | 
						|
        be a single topic (string or tuple) or a list of topics (ie 
 | 
						|
        list containing strings and/or tuples). If topics is not 
 | 
						|
        specified, all listeners for all topics will be unsubscribed, 
 | 
						|
        ie. the Publisher singleton will have no topics and no listeners
 | 
						|
        left. If topics was specified and is not found among contained
 | 
						|
        topics, the onNoSuchTopic, if specified, will be called, with 
 | 
						|
        the name of the topic."""
 | 
						|
        if topics is None: 
 | 
						|
            del self.__topicTree
 | 
						|
            self.__topicTree = _TopicTreeRoot()
 | 
						|
            return
 | 
						|
        
 | 
						|
        # make sure every topics are in tuple form
 | 
						|
        if isinstance(topics, list):
 | 
						|
            topicList = [_tupleize(x) for x in topics]
 | 
						|
        else:
 | 
						|
            topicList = [_tupleize(topics)]
 | 
						|
            
 | 
						|
        # unsub every listener of topics
 | 
						|
        self.__topicTree.unsubAll(topicList, onNoSuchTopic)
 | 
						|
            
 | 
						|
    def unsubscribe(self, listener, topics=None):
 | 
						|
        """Unsubscribe listener. If topics not specified, listener is
 | 
						|
        completely unsubscribed. Otherwise, it is unsubscribed only 
 | 
						|
        for the topic (the usual tuple) or list of topics (ie a list
 | 
						|
        of tuples) specified. In this case, if listener is not actually
 | 
						|
        subscribed for (one of) the topics, the optional onNotSubscribed
 | 
						|
        callback will be called, as onNotSubscribed(listener, missingTopic).
 | 
						|
        
 | 
						|
        Note that if listener subscribed for two topics (a,b) and (a,c), 
 | 
						|
        then unsubscribing for topic (a) will do nothing. You must 
 | 
						|
        use getAssociatedTopics(listener) and give unsubscribe() the returned 
 | 
						|
        list (or a subset thereof).
 | 
						|
        """
 | 
						|
        self.validate(listener)
 | 
						|
        topicList = None
 | 
						|
        if topics is not None:
 | 
						|
            if isinstance(topics, list):
 | 
						|
                topicList = [_tupleize(x) for x in topics]
 | 
						|
            else:
 | 
						|
                topicList = [_tupleize(topics)]
 | 
						|
            
 | 
						|
        self.__topicTree.unsubscribe(listener, topicList)
 | 
						|
        
 | 
						|
    def getAssociatedTopics(self, listener):
 | 
						|
        """Return a list of topics the given listener is registered with. 
 | 
						|
        Returns [] if listener never subscribed.
 | 
						|
        
 | 
						|
        :attention: when using the return of this method to compare to 
 | 
						|
        expected list of topics, remember that topics that are not in the
 | 
						|
        form of a tuple appear as a one-tuple in the return. E.g. if you 
 | 
						|
        have subscribed a listener to 'topic1' and ('topic2','subtopic2'), 
 | 
						|
        this method returns::
 | 
						|
            
 | 
						|
            associatedTopics = [('topic1',), ('topic2','subtopic2')]
 | 
						|
        """
 | 
						|
        return self.__topicTree.getTopics(listener)
 | 
						|
    
 | 
						|
    def sendMessage(self, topic=ALL_TOPICS, data=None, onTopicNeverCreated=None):
 | 
						|
        """Send a message for given topic, with optional data, to
 | 
						|
        subscribed listeners. If topic is not specified, only the
 | 
						|
        listeners that are interested in all topics will receive
 | 
						|
        message. The onTopicNeverCreated is an optional callback of
 | 
						|
        your choice that will be called if the topic given was never
 | 
						|
        created (i.e. it, or one of its subtopics, was never
 | 
						|
        subscribed to). The callback must be of the form f(a)."""
 | 
						|
        aTopic  = _tupleize(topic)
 | 
						|
        message = Message(aTopic, data)
 | 
						|
        self.__messageCount += 1
 | 
						|
        
 | 
						|
        # send to those who listen to all topics
 | 
						|
        self.__deliveryCount += \
 | 
						|
            self.__topicTree.sendMessage(aTopic, message, onTopicNeverCreated)
 | 
						|
        
 | 
						|
    #
 | 
						|
    # Private methods
 | 
						|
    #
 | 
						|
 | 
						|
    def __call__(self):
 | 
						|
        """Allows for singleton"""
 | 
						|
        return self
 | 
						|
    
 | 
						|
    def __str__(self):
 | 
						|
        return str(self.__topicTree)
 | 
						|
 | 
						|
# Create an instance with the same name as the class, effectivly
 | 
						|
# hiding the class object so it can't be instantiated any more.  From
 | 
						|
# this point forward any calls to Publisher() will invoke the __call__
 | 
						|
# of this instance which just returns itself.
 | 
						|
#
 | 
						|
# The only flaw with this approach is that you can't derive a new
 | 
						|
# class from Publisher without jumping through hoops.  If this ever
 | 
						|
# becomes an issue then a new Singleton implementaion will need to be
 | 
						|
# employed.
 | 
						|
Publisher = Publisher()
 | 
						|
 | 
						|
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
 | 
						|
class Message:
 | 
						|
    """
 | 
						|
    A simple container object for the two components of
 | 
						|
    a message; the topic and the user data.
 | 
						|
    """
 | 
						|
    def __init__(self, topic, data):
 | 
						|
        self.topic = topic
 | 
						|
        self.data  = data
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return '[Topic: '+`self.topic`+',  Data: '+`self.data`+']'
 | 
						|
 | 
						|
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
 | 
						|
 | 
						|
#
 | 
						|
# Code for a simple command-line test
 | 
						|
#
 | 
						|
def test():
 | 
						|
    def done(funcName):
 | 
						|
        print '----------- Done %s -----------' % funcName
 | 
						|
        
 | 
						|
    def testParam():
 | 
						|
        def testFunc(a,b,c=1): pass
 | 
						|
        class Foo:
 | 
						|
            def testMeth(self,a,b): pass
 | 
						|
            def __call__(self, a): pass
 | 
						|
            
 | 
						|
        foo = Foo()
 | 
						|
        assert _paramMinCount(testFunc)==(2,1)
 | 
						|
        assert _paramMinCount(Foo.testMeth)==(2,0)
 | 
						|
        assert _paramMinCount(foo.testMeth)==(2,0)
 | 
						|
        assert _paramMinCount(foo)==(1,0)
 | 
						|
    
 | 
						|
        done('testParam')
 | 
						|
 | 
						|
    testParam()
 | 
						|
    #------------------------
 | 
						|
 | 
						|
    _NodeCallback.notified = 0
 | 
						|
    def testPreNotifyNode(self, dead):
 | 
						|
        _NodeCallback.notified += 1
 | 
						|
        print 'testPreNotifyNODE heard notification of', `dead`
 | 
						|
    _NodeCallback.preNotify = testPreNotifyNode
 | 
						|
    
 | 
						|
    def testTreeNode():
 | 
						|
 | 
						|
        class WS:
 | 
						|
            def __init__(self, s):
 | 
						|
                self.s = s
 | 
						|
            def __call__(self, msg):
 | 
						|
                print 'WS#', self.s, ' received msg ', msg
 | 
						|
            def __str__(self):
 | 
						|
                return self.s
 | 
						|
            
 | 
						|
        def testPreNotifyRoot(dead):
 | 
						|
            print 'testPreNotifyROOT heard notification of', `dead`
 | 
						|
    
 | 
						|
        node = _TopicTreeNode((ALL_TOPICS,), WeakRef(testPreNotifyRoot))
 | 
						|
        boo, baz, bid = WS('boo'), WS('baz'), WS('bid')
 | 
						|
        node.addCallable(boo)
 | 
						|
        node.addCallable(baz)
 | 
						|
        node.addCallable(boo)
 | 
						|
        assert node.getCallables() == [boo,baz]
 | 
						|
        assert node.hasCallable(boo)
 | 
						|
        
 | 
						|
        node.removeCallable(bid) # no-op
 | 
						|
        assert node.hasCallable(baz)
 | 
						|
        assert node.getCallables() == [boo,baz]
 | 
						|
        
 | 
						|
        node.removeCallable(boo)
 | 
						|
        assert node.getCallables() == [baz]
 | 
						|
        assert node.hasCallable(baz)
 | 
						|
        assert not node.hasCallable(boo)
 | 
						|
        
 | 
						|
        node.removeCallable(baz)
 | 
						|
        assert node.getCallables() == []
 | 
						|
        assert not node.hasCallable(baz)
 | 
						|
 | 
						|
        node2 = node.createSubtopic('st1', ('st1',))
 | 
						|
        node3 = node.createSubtopic('st2', ('st2',))
 | 
						|
        cb1, cb2, cb = WS('st1_cb1'), WS('st1_cb2'), WS('st2_cb')
 | 
						|
        node2.addCallable(cb1)
 | 
						|
        node2.addCallable(cb2)
 | 
						|
        node3.addCallable(cb)
 | 
						|
        node2.createSubtopic('st3', ('st1','st3'))
 | 
						|
        node2.createSubtopic('st4', ('st1','st4'))
 | 
						|
       
 | 
						|
        print str(node)
 | 
						|
        assert str(node) == ' (st1: st1_cb1 st1_cb2  (st4: ) (st3: )) (st2: st2_cb )'
 | 
						|
    
 | 
						|
        # verify send message, and that a dead listener does not get sent one
 | 
						|
        delivered = node2.sendMessage('hello')
 | 
						|
        assert delivered == 2
 | 
						|
        del cb1
 | 
						|
        delivered = node2.sendMessage('hello')
 | 
						|
        assert delivered == 1
 | 
						|
        assert _NodeCallback.notified == 1
 | 
						|
        
 | 
						|
        done('testTreeNode')
 | 
						|
        
 | 
						|
    testTreeNode()
 | 
						|
    #------------------------
 | 
						|
    
 | 
						|
    def testValidate():
 | 
						|
        class Foo:
 | 
						|
            def __call__(self, a):   pass
 | 
						|
            def fun(self, b):        pass
 | 
						|
            def fun2(self, b=1):     pass
 | 
						|
            def fun3(self, a, b=2):  pass
 | 
						|
            def badFun(self):        pass
 | 
						|
            def badFun2():           pass
 | 
						|
            def badFun3(self, a, b): pass
 | 
						|
            
 | 
						|
        server = Publisher()
 | 
						|
        foo = Foo()
 | 
						|
        server.validate(foo)
 | 
						|
        server.validate(foo.fun)
 | 
						|
        server.validate(foo.fun2)
 | 
						|
        server.validate(foo.fun3)
 | 
						|
        assert not server.isValid(foo.badFun)
 | 
						|
        assert not server.isValid(foo.badFun2)
 | 
						|
        assert not server.isValid(foo.badFun3)
 | 
						|
    
 | 
						|
        done('testValidate')
 | 
						|
 | 
						|
    testValidate()
 | 
						|
    #------------------------
 | 
						|
    
 | 
						|
    class SimpleListener:
 | 
						|
        def __init__(self, number):
 | 
						|
            self.number = number
 | 
						|
        def __call__(self, message = ''): 
 | 
						|
            print 'Callable #%s got the message "%s"' %(self.number, message)
 | 
						|
        def notify(self, message):
 | 
						|
            print '%s.notify() got the message "%s"' %(self.number, message)
 | 
						|
        def __str__(self):
 | 
						|
            return "SimpleListener_%s" % self.number
 | 
						|
 | 
						|
    def testSubscribe():
 | 
						|
        publisher = Publisher()
 | 
						|
        
 | 
						|
        topic1 = 'politics'
 | 
						|
        topic2 = ('history','middle age')
 | 
						|
        topic3 = ('politics','UN')
 | 
						|
        topic4 = ('politics','NATO')
 | 
						|
        topic5 = ('politics','NATO','US')
 | 
						|
        
 | 
						|
        lisnr1 = SimpleListener(1)
 | 
						|
        lisnr2 = SimpleListener(2)
 | 
						|
        def func(message, a=1): 
 | 
						|
            print 'Func received message "%s"' % message
 | 
						|
        lisnr3 = func
 | 
						|
        lisnr4 = lambda x: 'Lambda received message "%s"' % x
 | 
						|
 | 
						|
        assert not publisher.isSubscribed(lisnr1)
 | 
						|
        assert not publisher.isSubscribed(lisnr2)
 | 
						|
        assert not publisher.isSubscribed(lisnr3)
 | 
						|
        assert not publisher.isSubscribed(lisnr4)
 | 
						|
        
 | 
						|
        publisher.subscribe(lisnr1, topic1)
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,)]
 | 
						|
        publisher.subscribe(lisnr1, topic2)
 | 
						|
        publisher.subscribe(lisnr1, topic1) # do it again, should be no-op
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2]
 | 
						|
        publisher.subscribe(lisnr2.notify, topic3)
 | 
						|
        assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3]
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2]
 | 
						|
        publisher.subscribe(lisnr3, topic5)
 | 
						|
        assert publisher.getAssociatedTopics(lisnr3) == [topic5]
 | 
						|
        assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3]
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2]
 | 
						|
        publisher.subscribe(lisnr4)
 | 
						|
        
 | 
						|
        print "Publisher tree: ", publisher
 | 
						|
        assert publisher.isSubscribed(lisnr1)
 | 
						|
        assert publisher.isSubscribed(lisnr1, topic1)
 | 
						|
        assert publisher.isSubscribed(lisnr1, topic2)
 | 
						|
        assert publisher.isSubscribed(lisnr2.notify)
 | 
						|
        assert publisher.isSubscribed(lisnr3, topic5)
 | 
						|
        assert publisher.isSubscribed(lisnr4, ALL_TOPICS)
 | 
						|
        expectTopicTree = 'all: <lambda>  (politics: SimpleListener_1  (UN: SimpleListener_2.notify ) (NATO:  (US: func ))) (history:  (middle age: SimpleListener_1 ))'
 | 
						|
        print "Publisher tree: ", publisher
 | 
						|
        assert str(publisher) == expectTopicTree
 | 
						|
        
 | 
						|
        publisher.unsubscribe(lisnr1, 'booboo') # should do nothing
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2]
 | 
						|
        assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3]
 | 
						|
        assert publisher.getAssociatedTopics(lisnr3) == [topic5]
 | 
						|
        publisher.unsubscribe(lisnr1, topic1)
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == [topic2]
 | 
						|
        assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3]
 | 
						|
        assert publisher.getAssociatedTopics(lisnr3) == [topic5]
 | 
						|
        publisher.unsubscribe(lisnr1, topic2)
 | 
						|
        publisher.unsubscribe(lisnr1, topic2)
 | 
						|
        publisher.unsubscribe(lisnr2.notify, topic3)
 | 
						|
        publisher.unsubscribe(lisnr3, topic5)
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == []
 | 
						|
        assert publisher.getAssociatedTopics(lisnr2.notify) == []
 | 
						|
        assert publisher.getAssociatedTopics(lisnr3) == []
 | 
						|
        publisher.unsubscribe(lisnr4)
 | 
						|
        
 | 
						|
        expectTopicTree = 'all:  (politics:  (UN: ) (NATO:  (US: ))) (history:  (middle age: ))'
 | 
						|
        print "Publisher tree: ", publisher
 | 
						|
        assert str(publisher) == expectTopicTree
 | 
						|
        assert publisher.getDeliveryCount() == 0
 | 
						|
        assert publisher.getMessageCount() == 0
 | 
						|
        
 | 
						|
        publisher.unsubAll()
 | 
						|
        assert str(publisher) == 'all: '
 | 
						|
        
 | 
						|
        done('testSubscribe')
 | 
						|
    
 | 
						|
    testSubscribe()
 | 
						|
    #------------------------
 | 
						|
    
 | 
						|
    def testUnsubAll():
 | 
						|
        publisher = Publisher()
 | 
						|
        
 | 
						|
        topic1 = 'politics'
 | 
						|
        topic2 = ('history','middle age')
 | 
						|
        topic3 = ('politics','UN')
 | 
						|
        topic4 = ('politics','NATO')
 | 
						|
        topic5 = ('politics','NATO','US')
 | 
						|
        
 | 
						|
        lisnr1 = SimpleListener(1)
 | 
						|
        lisnr2 = SimpleListener(2)
 | 
						|
        def func(message, a=1): 
 | 
						|
            print 'Func received message "%s"' % message
 | 
						|
        lisnr3 = func
 | 
						|
        lisnr4 = lambda x: 'Lambda received message "%s"' % x
 | 
						|
 | 
						|
        publisher.subscribe(lisnr1, topic1)
 | 
						|
        publisher.subscribe(lisnr1, topic2)
 | 
						|
        publisher.subscribe(lisnr2.notify, topic3)
 | 
						|
        publisher.subscribe(lisnr3, topic2)
 | 
						|
        publisher.subscribe(lisnr3, topic5)
 | 
						|
        publisher.subscribe(lisnr4)
 | 
						|
        
 | 
						|
        expectTopicTree = 'all: <lambda>  (politics: SimpleListener_1  (UN: SimpleListener_2.notify ) (NATO:  (US: func ))) (history:  (middle age: SimpleListener_1 func ))'
 | 
						|
        print "Publisher tree: ", publisher
 | 
						|
        assert str(publisher) == expectTopicTree
 | 
						|
    
 | 
						|
        publisher.unsubAll(topic1)
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == [topic2]
 | 
						|
        assert not publisher.isSubscribed(lisnr1, topic1)
 | 
						|
        
 | 
						|
        publisher.unsubAll(topic2)
 | 
						|
        print publisher
 | 
						|
        assert publisher.getAssociatedTopics(lisnr1) == []
 | 
						|
        assert publisher.getAssociatedTopics(lisnr3) == [topic5]
 | 
						|
        assert not publisher.isSubscribed(lisnr1)
 | 
						|
        assert publisher.isSubscribed(lisnr3, topic5)
 | 
						|
        
 | 
						|
        #print "Publisher tree: ", publisher
 | 
						|
        expectTopicTree = 'all: <lambda>  (politics:  (UN: SimpleListener_2.notify ) (NATO:  (US: func ))) (history:  (middle age: ))'
 | 
						|
        assert str(publisher) == expectTopicTree
 | 
						|
        publisher.unsubAll(ALL_TOPICS)
 | 
						|
        #print "Publisher tree: ", publisher
 | 
						|
        expectTopicTree = 'all:  (politics:  (UN: SimpleListener_2.notify ) (NATO:  (US: func ))) (history:  (middle age: ))'
 | 
						|
        assert str(publisher) == expectTopicTree
 | 
						|
        
 | 
						|
        publisher.unsubAll()
 | 
						|
        done('testUnsubAll')
 | 
						|
    
 | 
						|
    testUnsubAll()
 | 
						|
    #------------------------
 | 
						|
    
 | 
						|
    def testSend():
 | 
						|
        publisher = Publisher()
 | 
						|
        called = []
 | 
						|
        
 | 
						|
        class TestListener:
 | 
						|
            def __init__(self, num):
 | 
						|
                self.number = num
 | 
						|
            def __call__(self, b): 
 | 
						|
                called.append( 'TL%scb' % self.number )
 | 
						|
            def notify(self, b):
 | 
						|
                called.append( 'TL%sm' % self.number )
 | 
						|
        def funcListener(b):
 | 
						|
            called.append('func')
 | 
						|
            
 | 
						|
        lisnr1 = TestListener(1)
 | 
						|
        lisnr2 = TestListener(2)
 | 
						|
        lisnr3 = funcListener
 | 
						|
        lisnr4 = lambda x: called.append('lambda')
 | 
						|
 | 
						|
        topic1 = 'politics'
 | 
						|
        topic2 = 'history'
 | 
						|
        topic3 = ('politics','UN')
 | 
						|
        topic4 = ('politics','NATO','US')
 | 
						|
        topic5 = ('politics','NATO')
 | 
						|
        
 | 
						|
        publisher.subscribe(lisnr1, topic1)
 | 
						|
        publisher.subscribe(lisnr2, topic2)
 | 
						|
        publisher.subscribe(lisnr2.notify, topic2)
 | 
						|
        publisher.subscribe(lisnr3, topic4)
 | 
						|
        publisher.subscribe(lisnr4)
 | 
						|
        
 | 
						|
        print publisher
 | 
						|
        
 | 
						|
        # setup ok, now test send/receipt
 | 
						|
        publisher.sendMessage(topic1)
 | 
						|
        assert called == ['lambda','TL1cb']
 | 
						|
        called = []
 | 
						|
        publisher.sendMessage(topic2)
 | 
						|
        assert called == ['lambda','TL2cb','TL2m']
 | 
						|
        called = []
 | 
						|
        publisher.sendMessage(topic3)
 | 
						|
        assert called == ['lambda','TL1cb']
 | 
						|
        called = []
 | 
						|
        publisher.sendMessage(topic4)
 | 
						|
        assert called == ['lambda','TL1cb','func']
 | 
						|
        called = []
 | 
						|
        publisher.sendMessage(topic5)
 | 
						|
        assert called == ['lambda','TL1cb']
 | 
						|
        assert publisher.getDeliveryCount() == 12
 | 
						|
        assert publisher.getMessageCount() == 5
 | 
						|
    
 | 
						|
        # test weak referencing works:
 | 
						|
        _NodeCallback.notified = 0
 | 
						|
        del lisnr2
 | 
						|
        called = []
 | 
						|
        publisher.sendMessage(topic2)
 | 
						|
        assert called == ['lambda']
 | 
						|
        assert _NodeCallback.notified == 2
 | 
						|
        
 | 
						|
        done('testSend')
 | 
						|
        
 | 
						|
    testSend()
 | 
						|
    assert _NodeCallback.notified == 5
 | 
						|
    
 | 
						|
    def testDead():
 | 
						|
        # verify if weak references work as expected
 | 
						|
        print '------ Starting testDead ----------'
 | 
						|
        node = _TopicTreeNode('t1', None)
 | 
						|
        lisnr1 = SimpleListener(1)
 | 
						|
        lisnr2 = SimpleListener(2)
 | 
						|
        lisnr3 = SimpleListener(3)
 | 
						|
        lisnr4 = SimpleListener(4)
 | 
						|
 | 
						|
        node.addCallable(lisnr1)
 | 
						|
        node.addCallable(lisnr2)
 | 
						|
        node.addCallable(lisnr3)
 | 
						|
        node.addCallable(lisnr4)
 | 
						|
        
 | 
						|
        print 'Deleting listeners first'
 | 
						|
        _NodeCallback.notified = 0
 | 
						|
        del lisnr1
 | 
						|
        del lisnr2
 | 
						|
        assert _NodeCallback.notified == 2
 | 
						|
        
 | 
						|
        print 'Deleting node first'
 | 
						|
        _NodeCallback.notified = 0
 | 
						|
        del node
 | 
						|
        del lisnr3
 | 
						|
        del lisnr4
 | 
						|
        assert _NodeCallback.notified == 0
 | 
						|
        
 | 
						|
        lisnr1 = SimpleListener(1)
 | 
						|
        lisnr2 = SimpleListener(2)
 | 
						|
        lisnr3 = SimpleListener(3)
 | 
						|
        lisnr4 = SimpleListener(4)
 | 
						|
        
 | 
						|
        # try same with root of tree
 | 
						|
        node = _TopicTreeRoot()
 | 
						|
        node.addTopic(('',), lisnr1)
 | 
						|
        node.addTopic(('',), lisnr2)
 | 
						|
        node.addTopic(('',), lisnr3)
 | 
						|
        node.addTopic(('',), lisnr4)
 | 
						|
        # add objects that will die immediately to see if cleanup occurs
 | 
						|
        # this must be done visually as it is a low-level detail
 | 
						|
        _NodeCallback.notified = 0
 | 
						|
        _TopicTreeRoot.callbackDeadLimit = 3
 | 
						|
        node.addTopic(('',), SimpleListener(5))
 | 
						|
        node.addTopic(('',), SimpleListener(6))
 | 
						|
        node.addTopic(('',), SimpleListener(7))
 | 
						|
        print node.numListeners()
 | 
						|
        assert node.numListeners() == (4, 3)
 | 
						|
        node.addTopic(('',), SimpleListener(8))
 | 
						|
        assert node.numListeners() == (4, 0)
 | 
						|
        assert _NodeCallback.notified == 4
 | 
						|
        
 | 
						|
        print 'Deleting listeners first'
 | 
						|
        _NodeCallback.notified = 0
 | 
						|
        del lisnr1
 | 
						|
        del lisnr2
 | 
						|
        assert _NodeCallback.notified == 2
 | 
						|
        print 'Deleting node first'
 | 
						|
        _NodeCallback.notified = 0
 | 
						|
        del node
 | 
						|
        del lisnr3
 | 
						|
        del lisnr4
 | 
						|
        assert _NodeCallback.notified == 0
 | 
						|
        
 | 
						|
        done('testDead')
 | 
						|
    
 | 
						|
    testDead()
 | 
						|
    
 | 
						|
    print 'Exiting tests'
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    test()
 |