Esempio n. 1
0
    def __createTopic(self, nameTuple, desc, specGiven, parent=None):
        '''Actual topic creation step. Adds new Topic instance
        to topic map, and sends notification message (of topic 
        'pubsub.newTopic') about new topic having been created.'''
        if specGiven is None:
            specGiven = ArgSpecGiven()
        parentAI = None
        if parent:
            parentAI = parent._getListenerSpec()
        argsInfo = ArgsInfo(nameTuple, specGiven, parentAI)
        if (self.__treeConfig.raiseOnTopicUnspecified
            and not argsInfo.isComplete()):
            raise ListenerSpecIncomplete(nameTuple)

        newTopicObj = Topic(self.__treeConfig, nameTuple, desc,
                            argsInfo, parent = parent)
        # sanity checks:
        assert not self._topicsMap.has_key(newTopicObj.getName())
        if parent is self.__allTopics:
            assert len( newTopicObj.getNameTuple() ) == 1
        else:
            assert parent.getNameTuple() == newTopicObj.getNameTuple()[:-1]
        assert nameTuple == newTopicObj.getNameTuple()

        # store new object and notify of creation
        self._topicsMap[ newTopicObj.getName() ] = newTopicObj
        self.__treeConfig.notificationMgr.notifyNewTopic(
            newTopicObj, desc, specGiven.reqdArgs, specGiven.argsDocs)
        
        return newTopicObj
Esempio n. 2
0
    def __init__(self, topicMgr, nameTuple, description, parent=None,
                 argsSpec=None, reqdArgs=(), msgArgs=None, deadListenerCB=None):
        '''Specify the name, description, and parent of this Topic. Any remaining 
        keyword arguments (which will be put in msgArgs) describe the arguments that 
        a listener of this topic must support (i.e., the key is the argument name and
        the value is a documentation string explaining what the argument is for). 
        The reqdArgs is an optional list of names identifying which variables in 
        msgArgs keys are required arguments. E.g. 
        
            Topic(('a','b'), 'what is topic for', parentTopic, _reqdArgs=('c','d'), 
                c='what is c for', d='what is d for', e='what is e for')
            
        would create a Topic whose listeners would have to be of the form
        
            callable(c, d, e=...)
            
        ie 
            callable(c, d, e=...)
            callable(self, c, d, e=..., **kwargs) (method)
            
        would all be valid listeners but 
        
            callable(c, e=...) # error: required d is missing
            callable(c, d, e)  # error: e is optional
        
        would not be valid listeners of this topic. 
        
        The _useKwa is only used by the package to indicate whether the arguments are
        specified as part of __init__ (there is no other way since msgArgs cannot be None). 
        '''
        self.__validateName(nameTuple, parent is None)
        self.__tupleName = nameTuple

        self.__validator    = None
        self.__listeners    = []
        self.__deadListenerCB = deadListenerCB
        
        # specification: 
        self.__description  = None
        self.setDescription(description)
        getArgsSpec = topicMgr._getDefnProvider_().getSubSpec
        self.__msgArgs      = ArgsInfo(getArgsSpec, nameTuple, 
                                       parent, msgArgs, reqdArgs, argsSpec) 
        if self.__msgArgs.isComplete():
            self.__finalize()
        self.argsSpec = self.__msgArgs
        
        # now that we know the args are fine, we can link to parent
        self.__parentTopic = None
        if parent is None:
            assert self.isSendable()
        else:
            self.__parentTopic = weakref(parent)
            parent.__setSubtopic( self.getTailName(), self )
Esempio n. 3
0
 def _updateArgsSpec_(self, usingCallable, topicMgr):
     '''Update the argument spec of topic using given callable. '''
     assert self.__parentTopic is not None
     assert not self.argsSpecComplete()
     
     argsDocs, required = topicArgsFromCallable(usingCallable)
     getArgsSpec = topicMgr._getDefnProvider_().getSubSpec
     self.__msgArgs = ArgsInfo(getArgsSpec, self.getName(), self.__parentTopic(), 
                               argsDocs, required, ARGS_SPEC_ALL)
     self.argsSpec = self.__msgArgs
     # validate that our new spec agrees with complete children
     for child in self.getSubtopics():
         # get difference between child and our parent
         # this must contain our difference from our parent
         pass
     
     if self.__msgArgs.isComplete():
         self.__finalize()
Esempio n. 4
0
class Topic(PublisherMixin):
    '''
    Represent a message topic. This keeps track of which  
    call arguments (msgArgs) can be given as message data to subscribed
    listeners, it supports documentation of msgArgs and topic itself,
    and allows Python-like access to subtopics (e.g. A.B is subtopic
    B of topic A) and keeps track of listeners of topic. 
    '''
    
    UNDERSCORE = '_' # topic name can't start with this
    
    class InvalidName(ValueError):
        '''
        Raised when attempt to create a topic with name that is 
        not allowed (contains reserved characters etc).
        '''
        def __init__(self, name, reason):
            msg = 'Invalid topic name "%s": %s' % (name or '', reason)
            ValueError.__init__(self, )

    def __init__(self, topicMgr, nameTuple, description, parent=None,
                 argsSpec=None, reqdArgs=(), msgArgs=None, deadListenerCB=None):
        '''Specify the name, description, and parent of this Topic. Any remaining 
        keyword arguments (which will be put in msgArgs) describe the arguments that 
        a listener of this topic must support (i.e., the key is the argument name and
        the value is a documentation string explaining what the argument is for). 
        The reqdArgs is an optional list of names identifying which variables in 
        msgArgs keys are required arguments. E.g. 
        
            Topic(('a','b'), 'what is topic for', parentTopic, _reqdArgs=('c','d'), 
                c='what is c for', d='what is d for', e='what is e for')
            
        would create a Topic whose listeners would have to be of the form
        
            callable(c, d, e=...)
            
        ie 
            callable(c, d, e=...)
            callable(self, c, d, e=..., **kwargs) (method)
            
        would all be valid listeners but 
        
            callable(c, e=...) # error: required d is missing
            callable(c, d, e)  # error: e is optional
        
        would not be valid listeners of this topic. 
        
        The _useKwa is only used by the package to indicate whether the arguments are
        specified as part of __init__ (there is no other way since msgArgs cannot be None). 
        '''
        self.__validateName(nameTuple, parent is None)
        self.__tupleName = nameTuple

        self.__validator    = None
        self.__listeners    = []
        self.__deadListenerCB = deadListenerCB
        
        # specification: 
        self.__description  = None
        self.setDescription(description)
        getArgsSpec = topicMgr._getDefnProvider_().getSubSpec
        self.__msgArgs      = ArgsInfo(getArgsSpec, nameTuple, 
                                       parent, msgArgs, reqdArgs, argsSpec) 
        if self.__msgArgs.isComplete():
            self.__finalize()
        self.argsSpec = self.__msgArgs
        
        # now that we know the args are fine, we can link to parent
        self.__parentTopic = None
        if parent is None:
            assert self.isSendable()
        else:
            self.__parentTopic = weakref(parent)
            parent.__setSubtopic( self.getTailName(), self )
        
    def setDescription(self, desc):
        '''Set the 'docstring' of topic'''
        self.__description = desc or 'UNDOCUMENTED'
        
    def getDescription(self):
        '''Return the 'docstring' of topic'''
        return smartDedent(self.__description)
        
    def argsSpecComplete(self):
        '''Return true only if topic's spec is complete'''
        return self.__msgArgs.isComplete()
    
    def getArgs(self):
        '''Returns a triplet (reqdArgs, optArgs, isComplete) where reqdArgs
        is the names of required message arguments, optArgs same for optional
        arguments, and isComplete is same as would be returned from 
        self.argsSpecComplete().'''
        return (self.__msgArgs.allRequired, 
                self.__msgArgs.allOptional,
                self.__msgArgs.isComplete())
    
    def getArgDescriptions(self):
        '''Get a **copy** of the topic's kwargs given at construction time. 
        Returns None if args not described yet. '''
        if self.__parentTopic is None:
            return self.__msgArgs.subArgsDocs.copy()
        parentDescs = self.__parentTopic().getArgDescriptions()
        parentDescs.update( self.__msgArgs.subArgsDocs or {})
        return parentDescs
    
    def isSendable(self):
        '''Return true if messages can be sent for this topic'''
        return self.__validator is not None
    
    def getName(self):
        '''Return dotted form of full topic name'''
        return stringize(self.__tupleName)
        
    def getNameTuple(self):
        '''Return tuple form of full topic name'''
        return self.__tupleName
    
    def getTailName(self):
        '''Return the last part of the topic name (has no dots)'''
        name = self.__tupleName[-1]
        if name is ALL_TOPICS:
            return 'ALL_TOPICS'
        assert name.find('.') < 0
        return name
    
    def getParent(self):
        '''Get Topic object that is parent of self 
        (i.e. self is a subtopic of parent).'''
        if self.__parentTopic is None:
            return None
        return self.__parentTopic()

    def hasSubtopic(self, name=None):
        '''Return true only if name is a subtopic of self. If name not
        specified, return true only if self has at least one subtopic.'''
        if name is None:
            for attr in self.__dict__.values():
                if isinstance(attr, Topic):
                    return True
            return False
        
        elif hasattr(self, name):
            return isinstance(getattr(self, name), Topic)
        
        return False
        
    def getSubtopics(self):
        '''Get a list of Topic instances that are subtopics of self.'''
        st = []
        for attr in self.__dict__.values():
            if isinstance(attr, Topic):
                st.append(attr)
        return st
    
    def getNumListeners(self):
        '''Return number of listeners currently subscribed to topic. This is
        different from number of listeners that will get notified since more
        general topics up the topic tree may have listeners.'''
        return len(self.__listeners)

    def hasListener(self, listener):
        '''Return true if listener is subscribed to this topic.'''
        return listener in self.__listeners

    def hasListeners(self):
        '''Return true if there are any listeners subscribed to 
        this topic, false otherwise.'''
        return self.__listeners != []
    
    def getListeners(self):
        '''Get a **copy** of Listener objects for listeners 
        subscribed to this topic.'''
        return self.__listeners[:]
        
    def validate(self, listener):
        '''Same as self.isValid(listener) but raises ListenerInadequate
        instead of returning False. Returns nothing. '''
        if not self.isSendable():
            raise ListenerNotValidatable()
        return self.__validator.validate(listener)
    
    def isValid(self, listener):
        '''Return True only if listener can subscribe to messages of 
        this topic, otherwise returns False. Raises ListenerNotValidatable 
        if not self.isSendable().'''
        if not self.isSendable():
            raise ListenerNotValidatable()
        return self.__validator.isValid(listener)
        
    def __call__(self, subtopicName):
        '''Return the Topic object that represents the subtopic of given name'''
        return getattr(self, subtopicName)
    
    # Impementation API:
    
    def _updateArgsSpec_(self, usingCallable, topicMgr):
        '''Update the argument spec of topic using given callable. '''
        assert self.__parentTopic is not None
        assert not self.argsSpecComplete()
        
        argsDocs, required = topicArgsFromCallable(usingCallable)
        getArgsSpec = topicMgr._getDefnProvider_().getSubSpec
        self.__msgArgs = ArgsInfo(getArgsSpec, self.getName(), self.__parentTopic(), 
                                  argsDocs, required, ARGS_SPEC_ALL)
        self.argsSpec = self.__msgArgs
        # validate that our new spec agrees with complete children
        for child in self.getSubtopics():
            # get difference between child and our parent
            # this must contain our difference from our parent
            pass
        
        if self.__msgArgs.isComplete():
            self.__finalize()
            
    def _subscribe_(self, listener):
        '''This method must only be called from within pubsub, as 
        indicated by the surrounding underscores.'''
        # add to list if not already there:
        if listener in self.__listeners:
            assert self.isSendable()
            idx = self.__listeners.index(listener)
            return self.__listeners[idx], False
            
        else:
            if not self.isSendable():
                raise RuntimeError('Incomplete topic, can\'t register listeners')
            else:
                argsInfo = self.__validator.validate(listener)
                weakListener = Listener(
                    listener, argsInfo, onDead=self.__onDeadListener)
            self.__listeners.append(weakListener)
            return weakListener, True
            
    def _unsubscribe_(self, listener):
        try:
            idx = self.__listeners.index(listener)
        except ValueError:
            return None

        tmp = self.__listeners.pop(idx)
        tmp._unlinkFromTopic_()
        return tmp
        
    def _unsubscribeAllListeners_(self, filter=None):
        '''Clears list of subscribed listeners. If filter is given, it must 
        be a function that takes a listener and returns true if the listener
        should be unsubscribed. Returns the list of listeners that were 
        unsubscribed.'''
        index = 0
        unsubd = []
        for listener in self.__listeners[:] :
            if filter is None or filter(listener):
                listener._unlinkFromTopic_()
                assert listener is self.__listeners[index]
                del self.__listeners[index] 
                unsubd.append(listener)
            else:
                index += 1
            
        return unsubd
        
    def _undefineSelf_(self, topicsMap):
        if self.__parentTopic is not None:
            delattr(self.__parentTopic(), self.__tupleName[-1])
        self.__undefineSelf(topicsMap)

    def __finalize(self):
        '''Change the arguments of topic. They can be different from those set (if any) 
        at construction time, however any subscribed listeners must remain valid with 
        new args/required otherwise a ValueError exception is raised. '''            
        assert not self.isSendable()
        #assert self.__msgArgs.isFinal()

        # must make sure can adopt a validator
        required = self.__msgArgs.allRequired
        optional = self.__msgArgs.allOptional
        self.__validator = ListenerValidator(required, list(optional) )
        assert not self.__listeners 
    
    def __undefineSelf(self, topicsMap):
        '''Unsubscribe all our listeners, remove all subtopics from self,
        then detach from parent. '''
        #print 'Remove %s listeners (%s)' % (self.getName(), self.getNumListeners())
        self._unsubscribeAllListeners_()
        self.__parentTopic = None
        
        for subName, subObj in self.__dict__.items(): # COPY since modify!!
            if isinstance(subObj, Topic) and not subName.startswith('_'):
                #print 'Unlinking %s from parent' % subObj.getName()
                delattr(self, subName)
                subObj.__undefineSelf(topicsMap)
            
        del topicsMap[self.getName()]

    def __validateName(self, nameTuple, isRootTopic):
        '''Raise TopicNameInvalid if nameTuple not valid as topic name.'''
        if not nameTuple: 
            reason = 'name tuple must have at least one item!'
            raise TopicNameInvalid(None, reason)
        
        tailName = nameTuple[-1]
        if not tailName:
            reason = 'can\'t contain empty string or None'
            raise TopicNameInvalid(None, reason)
        if tailName.startswith(self.UNDERSCORE):
            reason = 'must not start with "%s"' % self.UNDERSCORE
            raise TopicNameInvalid(tailName, reason)
        if tailName == ALL_TOPICS and not isRootTopic:
            reason = 'only root topic can contain "%s"' % ALL_TOPICS
            raise TopicNameInvalid(tailName, reason)
        assert tailName != ALL_TOPICS or isRootTopic

    def __setSubtopic(self, attrName, topicObj):
        '''Link self to a Topic instance via self.attrName. Always succeeds.'''
        assert topicObj.__parentTopic() is self
        setattr(self, attrName, topicObj)
        
    def __onDeadListener(self, weakListener):
        '''One of our subscribed listeners has died, so remove it and notify others'''
        ll = self.__listeners.index(weakListener)
        listener = self.__listeners[ll]
        llID = str(listener)
        del self.__listeners[ll]
        self.__deadListenerCB(self, listener)

    def __str__(self):
        return "%s, %s" % (self.getName(), self.getNumListeners())