Changeset 355

Show
Ignore:
Timestamp:
10/11/06 21:11:30 (2 years ago)
Author:
athomas
Message:

cly:

  • Added a configurable traversal limit.
  • Added a valid(context) callback method.
  • Changed Action so there is no longer pydoc magic going on. Must explicitly pass doc as first arg.
  • Updated unit tests.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • cly/trunk/cly/__init__.py

    r352 r355  
    6969    The default is for the node to match the name of the Node: 
    7070    >>> a = Node('Something', name='something') 
    71     >>> a.pattern.pattern == a.name 
     71    >>> a.pattern == a.name 
    7272    True 
    7373 
     
    7777    pattern = None 
    7878    separator = r'\s+|\s*$' 
     79    # Ordering within a group 
    7980    order = 0 
    8081    group = 0 
    8182    match_candidates = False 
     83    match_help = False 
     84    traversals = 1 
    8285 
    8386    def __init__(self, doc, pattern=None, name=None, separator=None, **options): 
    8487        """Construct a new CLY grammar node. 
    8588         
    86         `doc` can be either a help string or a tuple of (command, help), for 
    87         specifying the command text to match. If command is not provided, the 
    88         Node name is used. 
     89        `doc` can be either a help string, a tuple of (command, help), or a 
     90        lambda returning either of these, for specifying the command text to 
     91        match. If command is not provided, the Node name is used. 
    8992 
    9093        `pattern` is a regular expression for matching user input.""" 
     
    144147                          key=lambda i: (i.group, i.order, i.name)) 
    145148        for child in children: 
    146             yield child 
     149                yield child 
    147150 
    148151    def children(self, context, follow=False): 
    149152        """ Iterate over child nodes, optionally following branches. """ 
     153        def follow_branch(child): 
     154            branches = list(child.follow(context)) 
     155            if branches: 
     156                for branch in branches: 
     157                    yield branch 
     158                    follow_branch(branch) 
     159            else: 
     160                yield child 
     161 
    150162        for child in self: 
    151             if follow: 
    152                 followed = child.branch(context) 
    153                 while followed is not None: 
    154                     child = followed 
    155                     followed = followed.branch(context) 
    156             yield child 
     163            if child.valid(context): 
     164                if follow: 
     165                    for branch in follow_branch(child): 
     166                        if child.valid(context): 
     167                            yield branch 
     168                else: 
     169                    yield child 
     170 
     171    def follow(self, context): 
     172        """ Return alternative Nodes to traverse. """ 
     173        return [] 
    157174 
    158175    def chosen(self, context, match): 
    159176        """ This node was chosen by the parser. """ 
     177        context.traverse(self) 
    160178 
    161179    def next(self, context): 
     
    173191        Must include separator in determining whether a match was 
    174192        successful. """ 
     193        if not self.valid(context): 
     194            return None 
    175195        match = self._pattern.match(context.command, context.cursor) 
    176196        if match: 
     
    236256        [('one', 'One')] 
    237257        """ 
    238         if isinstance(self.doc, (tuple, list)): 
    239             yield self.doc 
     258        doc = self.doc 
     259        while callable(doc): 
     260            doc = doc(context) 
     261        if isinstance(doc, (tuple, list)): 
     262            yield doc 
    240263        else: 
    241264            if self.name == self.pattern: 
    242                 yield (self.name, self.doc) 
     265                yield (self.name, doc) 
    243266            else: 
    244                 yield ('<%s>' % self.name, self.doc) 
     267                yield ('<%s>' % self.name, doc) 
    245268 
    246269    def candidates(self, context, text): 
     
    256279            if key[0] != '<' and key.startswith(text): 
    257280                yield key + ' ' 
    258  
    259     def branch(self, context): 
    260         """ Return an alternative Node to traverse, or None. """ 
    261         return None 
    262281 
    263282    def find(self, path): 
     
    281300        raise InvalidNodePath(posixpath.join(self.path(), path.strip('/'))) 
    282301 
     302    def valid(self, context): 
     303        """ Is this node valid in the given context? """ 
     304        # Node is invalid if traversed more than self.traversals times 
     305        return context.traversed(self) < self.traversals 
     306 
    283307    def __repr__(self): 
    284308        return '<%s:%s>' % (self.__class__.__name__, self.path() or '<root>') 
     
    288312    """ An alias for another node. 
    289313     
    290     An Alias overrides the branch() method to return the aliased Node. 
     314    An Alias overrides the follow() method to return aliased Nodes. Globs are 
     315    supported. 
    291316 
    292317    >>> top = Node('Top', name='top', one=Node('One'), two=Node('Two', 
     
    296321    """ 
    297322 
     323    pattern = '' 
    298324    alias = property(lambda self: posixpath.normpath( 
    299325                     posixpath.join(self.path(), self._alias))) 
     
    303329        self._alias = alias 
    304330 
    305     def branch(self, context): 
    306         return self.parser.find(self.alias) 
     331    def follow(self, context): 
     332        try: 
     333            yield self.parser.find(self.alias) 
     334        except InvalidNodePath: 
     335            from fnmatch import fnmatch 
     336            start = self.parser.find(posixpath.dirname(self.alias)) 
     337            match = posixpath.basename(self.alias) 
     338            for child in start.children(context, True): 
     339                if fnmatch(child.name, match): 
     340                    yield child 
    307341 
    308342    def __repr__(self): 
     
    311345 
    312346class Action(Node): 
    313     """ Action node, matches EOL. If a callable is provided as the first 
    314     argument, it is used as the execute member, and its docstring used as the 
    315     help for this node. Otherwise the `callback` keyword arg will be used as 
    316     the callable. If `with_context` is true, the context object will be passed 
    317     as the first argument. 
     347    """ Action node, matches EOL. The `callback` arg will be used as the 
     348    callable. If `with_context` is true, the context object will be passed as 
     349    the first argument. 
    318350     
    319351    >>> def write_text(): 
    320     ...     \"\"\"Write some text\"\"\" 
    321352    ...     print "some text" 
    322     >>> grammar = Grammar(action=Action(write_text)) 
     353    >>> grammar = Grammar(action=Action("Write some text", write_text)) 
    323354    >>> node = grammar.find('action') 
    324355    >>> node.doc 
     
    333364    with_context = False 
    334365 
    335     def __init__(self, thing, *args, **kwargs): 
    336         if callable(thing): 
    337             doc = pydoc.getdoc(thing) or thing.__name__.replace('_', ' ').title() 
    338             doc = doc.splitlines()[0] 
    339             Node.__init__(self, doc, callback=thing, *args, **kwargs) 
    340         else: 
    341             Node.__init__(self, thing, *args, **kwargs) 
     366    def __init__(self, doc, callback, *args, **kwargs): 
     367        Node.__init__(self, doc, callback=callback, *args, **kwargs) 
    342368 
    343369    def help(self, context): 
     
    358384        raise UnexpectedEOL(None)  
    359385 
     386    def chosen(self, context, match): 
     387        # We don't "traverse" Action nodes, because they are always terminal, 
     388        # and if we do they get excluded from help. 
     389        pass 
     390 
    360391 
    361392class Validator(Node): 
     
    366397    accumulate = False 
    367398 
     399    def valid(self, context): 
     400        if not self.accumulate and self.name in context.vars: 
     401            return False 
     402        if self.accumulate and isinstance(self.accumulate, int) and \ 
     403                len(context.vars.get(self.name, [])) >= self.accumulate: 
     404            return False 
     405        return Node.valid(self, context) 
     406         
    368407    def chosen(self, context, match): 
    369408        try: 
     
    396435        self.help = [] 
    397436        self.node = node 
     437 
     438        def add_help(node): 
     439            node_help = sorted(node.help(context)) 
     440            for help in node_help: 
     441                self.help.append((node.group, node.order, help[0], help[1])) 
     442 
    398443        for child in node.children(context, follow=True): 
    399             child_help = sorted(child.help(context)) 
    400             for help in child_help: 
    401                 self.help.append((child.group, child.order, help[0], help[1])) 
     444            add_help(child) 
     445 
    402446        self.help.sort() 
    403447 
     
    405449        """ Iterate over each (key, help) help pair. 
    406450 
    407         >>> help = Help(None, Grammar(one=Node('One'), 
     451        >>> context = Context(None, None) 
     452        >>> help = Help(context, Grammar(one=Node('One'), 
    408453        ...     two=Node(('<two>', 'Two'), group=2))) 
    409454        >>> list(help) 
     
    418463 
    419464        >>> import sys 
    420         >>> help = Help(None, Grammar(one=Node('One'), 
     465        >>> context = Context(None, None) 
     466        >>> help = Help(context, Grammar(one=Node('One'), 
    421467        ...     two=Node(('<two>', 'Two'), group=2))) 
    422468        >>> help.format(sys.stdout) 
     
    447493        self.user = user 
    448494        self.vars = {} 
     495        self._traversed = {} 
    449496        self.trail = [] 
    450497 
     
    470517        self.cursor += distance 
    471518 
    472     def candidates(self, text): 
     519    def candidates(self, text=''): 
    473520        """ Return potential candidates from children of last successfully 
    474521        parsed node. """ 
     
    481528        node. """ 
    482529        return Help(self, self.last_node) 
     530 
     531    def traverse(self, node): 
     532        """ Mark node as traversed in this context. """ 
     533        path = node.path() 
     534        self._traversed.setdefault(path, 0) 
     535        self._traversed[path] += 1 
     536 
     537    def traversed(self, node): 
     538        """ How many times has node been traversed in this context? """ 
     539        return self._traversed.get(node.path, 0) 
    483540 
    484541    def __repr__(self): 
     
    518575         
    519576        >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three', 
    520         ...                 action=Action(lambda: "foo bar"))))) 
     577        ...                 action=Action('Do stuff', lambda: "foo bar"))))) 
    521578        >>> context = parser.parse('two three')  
    522579        >>> context 
     
    538595                    continue 
    539596                submatch = subnode.match(context) 
    540                 if submatch
     597                if submatch is not None
    541598                    return parse(subnode, submatch) 
    542599            else: 
     
    551608 
    552609        >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three', 
    553         ...                 action=Action(lambda: "foo bar"))))) 
     610        ...                 action=Action('Do stuff', lambda: "foo bar"))))) 
    554611        >>> parser.execute('two three')  
    555612        'foo bar' 
  • cly/trunk/cly/interactive.py

    r352 r355  
    55import cly.rlext 
    66 
     7 
     8def print_error(*message): 
     9    if sys.stderr.isatty(): 
     10        print >>sys.stderr, '\033[31m\033[1m%s\033[0m' % ' '.join(message) 
     11    else: 
     12        print >>sys.stderr, ' '.join(message) 
     13     
    714 
    815class Interact(object): 
     
    2431 
    2532        Interact.__parser = parser 
    26         self.application = application 
    27         self.prompt = prompt  
    28         self.user_context = user_context 
    29         self.history_file = history_file 
    30         self.history_length = history_length 
    31         self.completion_delimiters = completion_delimiters 
     33        Interact.prompt = prompt  
     34        Interact.application = application 
     35        Interact.user_context = user_context 
     36        Interact.history_file = history_file 
     37        Interact.history_length = history_length 
     38        Interact.completion_delimiters = completion_delimiters 
    3239 
    3340        try: 
     
    6673                context.execute() 
    6774            except cly.ParseError, e: 
    68                 if sys.stderr.isatty(): 
    69                     print >>sys.stderr, '\033[31m\033[1merror: %s\033[0m' % str(e) 
    70                 else: 
    71                     print >>sys.stderr, 'error: %s' % str(e) 
     75                print_error('error:', str(e)) 
    7276            return context 
    7377 
     
    125129            context = Interact.__parser.parse(command) 
    126130            if context.remaining.strip(): 
    127                 cly.rlext.cursor(context.cursor) 
     131                print 
     132                candidates = [help[0] for help in context.help()] 
     133                text = '%s^ invalid token (candidates are %s)' % (' ' * (context.cursor + len(Interact.prompt)), ', '.join(candidates)) 
     134                print_error(text) 
     135                cly.rlext.force_redisplay() 
     136                return 
    128137            help = context.help() 
    129138            print 
  • cly/trunk/cly/validators.py

    r352 r355  
    4343    'http://www.example.com/test/;test?a=10&b=10#fragment' 
    4444    """ 
    45     pattern = r"""(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?/{0,2}[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?""" 
     45    pattern = r"""(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?/{0,2}[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?""" 
    4646 
    4747    def __init__(self, doc, scheme='', allow_fragments=1, *argl, **argd): 
     
    5959    >>> from cly import * 
    6060    >>> parser = Parser(Grammar(foo=LDAPDN('Foo'))) 
    61     >>> parser.parse('cn=Manager, dc=example, dc=com').vars['foo'] 
    62     'cn=Manager, dc=example, dc=com' 
    63     """ 
    64     pattern = r'(\w+=\w+)\s*(?:,\s*(\w+=\w+))*' 
     61    >>> parser.parse('cn=Manager,dc=example,dc=com').vars['foo'] 
     62    'cn=Manager,dc=example,dc=com' 
     63    """ 
     64    pattern = r'(\w+=\w+)(?:,(\w+=\w+))*' 
    6565 
    6666