Changeset 556

Show
Ignore:
Timestamp:
11/07/08 00:46:59 (2 years ago)
Author:
athomas
Message:

- Many docstring updates.
- Added CIDR node. Automatically set match_candidates=True if candidates is

overridden.

- Add label support via the label=... attribute.
- Converted interactive "exceptions" argument to a callback.
- Added Absolute Time?, Relative Time? and Timezone node types.
- Added Key Value? node type (k=v).
- Removed defined() and added any() and all() to XML context locals.
- Re-added Group() node. This has one purpose: to set the group for all

children.

- Removed Apply().
- "group" attribute is now a property that will use the parent group if not

explicitly set.

- Delegate XML attribute casting and aliasing to Nodes.
- Update COPYING dates.
- Cleaned up some todos, removed some tabs.
- Renamed "Conditional" to "If", re-added "Apply" implemented on "Masquerade".
- Add support for labels to Node.find().

Location:
cly/trunk
Files:
1 added
10 modified

Legend:

Unmodified
Added
Removed
  • cly/trunk/.todo

    r552 r556  
    77        cmd.Cmd emulation (extended though) 
    88    </note> 
    9     <note priority="low" time="1178150358"> 
     9    <note priority="low" time="1178150358" done="1215652461"> 
    1010        Add XPath support: eg. grammar.find_all('//action') 
     11        <comment> 
     12            Unnecessary. 
     13        </comment> 
    1114    </note> 
    12     <note priority="medium" time="1179508609"> 
     15    <note priority="medium" time="1179508609" done="1215652470"> 
    1316        console.textwrap() has issues 
     17        <comment> 
     18            Fixed a while back. 
     19        </comment> 
    1420    </note> 
    1521    <note priority="medium" time="1179509575" done="1179988905"> 
     
    2228        Clarify/clean-up how with_context/with_user_context/user_context is used. 
    2329    </note> 
     30    <note priority="medium" time="1214634243"> 
     31        Add grammar introspection from classes. 
     32    </note> 
    2433</todo> 
  • cly/trunk/COPYING

    r413 r556  
    1 Copyright (C) 2006-2007 Alec Thomas 
     1Copyright (C) 2006-2008 Alec Thomas 
    22All rights reserved. 
    33 
  • cly/trunk/cly/__init__.py

    r513 r556  
    88 
    99"""CLY is a Python module for simplifying the creation of interactive shells. 
    10 Kind of like the builtin ``cmd`` module on steroids. 
     10Kind of like the builtin `cmd <http://docs.python.org/lib/module-cmd.html>`_ 
     11module on steroids. 
    1112 
    1213It has the following features: 
    1314 
    14   - Tab completion of all commands. 
     15  - Automatic tab completion of all commands:: 
    1516 
    16   - Contextual help. 
     17      cly> s<TAB><TAB> 
     18      show status 
    1719 
    18   - Extensible grammar - you can define your own commands with full dynamic 
    19     completion, contextual help, and so on. 
     20  - Contextual help:: 
    2021 
    21   - Simple. Grammars are constructed from objects using a convenient 
    22     ''function-call'' syntax. 
     22      cly> <?> 
     23      show    Show information. 
     24      status  Display status summary. 
     25 
     26      login   Authenticate. 
     27 
     28      quit    Quit. 
     29 
     30  - Extensible grammar - define your own commands with full dynamic completion, 
     31    contextual help, and so on. 
     32 
     33  - :class:`XML grammar <cly.builder.XMLGrammar>` for building clean MVC style command line interfaces. 
     34 
     35  - Simple. Grammars are constructed from objects using a simple *functional* 
     36    style. 
     37 
     38  - Multiple grammars can be merged both statically and dynamically. 
    2339 
    2440  - Flexible command grouping and ordering. 
     
    2743    independently of the readline-based shell. This allows CLY's parser to 
    2844    be used in other environments (think "web-based shell" ;)) 
    29  
    30   - Lots of other cool stuff. 
    3145""" 
    3246 
  • cly/trunk/cly/_rlext.c

    r528 r556  
    66 * you should have received as part of this distribution. 
    77 * 
    8  * 
    9 */ 
     8 * vim: ts=4 sts=4 sw=4 et 
     9 */ 
    1010#include "Python.h" 
    1111#include <readline/readline.h> 
     
    9898 
    9999static struct PyMethodDef methods[] = { 
    100         {(char*)"bind_key", bind_key, METH_VARARGS, doc_bind_key}, 
    101         {(char*)"force_redisplay", force_redisplay, METH_NOARGS, doc_force_redisplay}, 
    102         {(char*)"cursor", cursor, METH_VARARGS, doc_cursor}, 
    103         {NULL, NULL, 0, NULL}, 
     100    {(char*)"bind_key", bind_key, METH_VARARGS, doc_bind_key}, 
     101    {(char*)"force_redisplay", force_redisplay, METH_NOARGS, doc_force_redisplay}, 
     102    {(char*)"cursor", cursor, METH_VARARGS, doc_cursor}, 
     103    {NULL, NULL, 0, NULL}, 
    104104}; 
    105105 
     
    107107init_rlext(void) 
    108108{ 
    109         Py_InitModule((char*)"_rlext", methods); 
     109    Py_InitModule((char*)"_rlext", methods); 
    110110} 
  • cly/trunk/cly/builder.py

    r552 r556  
    1111 
    1212 
     13import datetime 
    1314import os 
    1415import posixpath 
     
    1819from xml.dom import minidom 
    1920from inspect import isclass, getargspec 
     21from cly.extra import cull_candidates 
    2022from cly.exceptions import * 
    2123from cly.parser import Context 
    2224 
     25try: 
     26    import pytz 
     27except ImportError: 
     28    pytz = None 
     29 
    2330 
    2431__all__ = [ 
    25     'Node', 'Masquerade', 'Alias', 'Conditional', 'Apply', 'Action', 
     32    'Node', 'Masquerade', 'Set', 'Alias', 'Group', 'If', 'Apply', 'Action', 
    2633    'Variable', 'Grammar', 'XMLGrammar', 'Help', 'LazyHelp', 'Word', 'Keyword', 
    2734    'String', 'URI', 'LDAPDN', 'Integer', 'Float', 'IP', 'Hostname', 'Host', 
    28     'EMail', 'File', 'Boolean', 
     35    'EMail', 'File', 'Boolean', 'KeyValue', 'AbsoluteTime', 'RelativeTime', 
     36    'Timezone', 
    2937    ] 
    3038__docformat__ = 'restructuredtext en' 
     
    3442    """The base class for all grammar nodes. 
    3543 
    36     :param help: 
    37         string or callable returning a list of (key, help) tuples 
    38         A help string or a callable returning an iterable of (key, help) 
    39         pairs. There is a useful class called Help which can be used for 
    40         this purpose. 
    41  
    42     :param name: 
    43         The name of the node. If ommitted the key used by the parent Node 
    44         is used. The node name also defines the node path: 
    45  
    46         >>> Node(name='something') 
    47         <Node:/something> 
     44    Any :class:`Node` instances passed to the constructor will become children of 
     45    this node in the grammar hierarchy. :class:`Node`\ s as keyword arguments 
     46    will be named after their keyword, while positional arguments will be 
     47    provided auto-generated names. This is generally only useful for "control" 
     48    nodes, such as :class:`Alias`. 
     49 
     50    Supported keyword arguments: 
     51 
     52        :help: 
     53            string or callable returning a list of (key, help) tuples A help 
     54            string or a callable returning an iterable of (key, help) pairs. 
     55            There is a useful class called Help which can be used for this 
     56            purpose. 
     57 
     58        :name: 
     59            The name of the node. If ommitted the keyword argument name used in 
     60            the parent Node is used. The node name also defines the node path 
     61            and the default pattern to match against if not explicitly provided: 
     62 
     63            >>> Node(name='something') 
     64            <Node:/something> 
    4865 
    4966    The following constructor arguments are also class variables, and as 
     
    5168    you find yourself using a particular pattern repeatedly. 
    5269 
    53     :param pattern: 
    54         The regular expression used to match user input. If not provided, 
    55         the node name is used: 
    56  
    57         >>> a = Node(name='something') 
    58         >>> a.pattern == a.name 
    59         True 
    60  
    61     :param separator: 
    62         A regular expression used to match the text separating this node 
    63         and the next. 
    64  
    65     :param group: 
    66         Nodes can be grouped together to provide visual cues. Groups are 
    67         ordered ascending numerically. 
    68  
    69     :param order: 
    70         Within a group, nodes are normally ordered alphabetically. This can 
    71         be overridden by setting this to a value other than 0. 
    72  
    73     :param match_candidates: 
    74         The candidates() method returns a list of words that match at the 
    75         current token, which are then used for completion, but can also be 
    76         used to constrain the allowed matches if match_candidates=True. 
    77         Useful for situations where you have a general regex pattern (eg. a 
    78         pattern matching files) but a known set of matches at this point (eg. 
    79         files in the current directory). 
    80  
    81     :param traversals: 
    82         The number of times this node can match in any parse context. Alias 
    83         nodes allow for multiple traversal. 
    84  
    85         If ``traversals=0`` the node will match an infinite number of times. 
     70        :pattern: 
     71            The regular expression used to match user input. If not provided, 
     72            the node name is used: 
     73 
     74            >>> a = Node(name='something') 
     75            >>> a.pattern == a.name 
     76            True 
     77 
     78        :separator: 
     79            A regular expression used to match the text separating this node 
     80            and the next. 
     81 
     82        :group: 
     83            Nodes with the same group value will be collated visually. 
     84            Generally a number, but can also be a string or any other 
     85            comparable object. 
     86 
     87        :order: 
     88            Within a group, nodes are normally ordered alphabetically. This can 
     89            be overridden by setting this to a value other than 0. 
     90 
     91        :match_candidates: 
     92            Modifies the behaviour of the parser when matching completion 
     93            candidates. 
     94 
     95            The :meth:`candidates` method returns a list of words that match at the 
     96            current token, which are then used for completion.  If 
     97            ``match_candidates=True`` the allowed input will be explicitly 
     98            constrainted to just these candidates. 
     99 
     100            ``match_candidates`` will be set automatically if 
     101            :meth:`candidates` is provided. 
     102 
     103            Useful for situations where you have a general regex pattern (eg. a 
     104            pattern matching files) but a known set of matches at this point 
     105            (eg.  files in the current directory). 
     106 
     107        :traversals: 
     108            The number of times this node can match in any parse context. 
     109            :class:`Alias` nodes allow for multiple traversal. 
     110 
     111            If ``traversals=0`` the node will match an infinite number of times. 
     112 
     113        :label: 
     114            Specify the global label for this node. This can be used by the 
     115            :class:`Alias` to refer to nodes by label rather than path. 
    86116    """ 
    87117    pattern = None 
    88118    separator = r'\s+|\s*$' 
    89119    order = 0 
    90     group = 0 
    91120    match_candidates = False 
    92121    traversals = 1 
     122    label = None 
    93123 
    94124    def __init__(self, *anonymous, **kwargs): 
    95125        self._children = {} 
     126        self._group = None 
    96127        help = kwargs.pop('help', '') 
    97128        if isinstance(help, basestring): 
     
    105136        if 'separator' in kwargs: 
    106137            self.separator = kwargs.pop('separator') 
     138        if 'candidates' in kwargs: 
     139            self.candidates = kwargs.pop('candidates') 
     140            self.match_candidates = True 
    107141        if self.pattern is not None: 
    108142            self._pattern = re.compile(self.pattern) 
     
    116150        self(*anonymous, **kwargs) 
    117151 
     152    def _get_group(self): 
     153        if self._group is None and self.parent: 
     154            return self.parent.group 
     155        return self._group or 0 
     156 
     157    def _set_group(self, group): 
     158        self._group = group 
     159 
     160    group = property(lambda self: self._get_group(), 
     161                     lambda self, value: self._set_group(value)) 
     162 
    118163    def _set_name(self, name): 
    119         """Set the name of this node. If the Node does not have an existing 
    120         matching pattern associated with it, a pattern will be created using 
    121         the name.""" 
     164        """Set the name of this node. 
     165 
     166        If the Node does not have an existing matching pattern associated with 
     167        it, a pattern will be created using the name. 
     168        """ 
    122169        self._name = name 
    123170        if isinstance(name, basestring) and self.pattern is None: 
     
    130177                    lambda self, name: self._set_name(name)) 
    131178 
     179 
    132180    def __call__(self, *anonymous, **options): 
    133181        """Update or add options and child nodes. 
     
    140188        >>> top(subnode=Node()) 
    141189        <Node:/top> 
    142         >>> top.find('subnode') 
     190        >>> top.find('/subnode') 
    143191        <Node:/top/subnode> 
    144192        """ 
     
    249297        """Iterate over child nodes, optionally follow()ing branches. 
    250298 
    251         >>> from cly.parser import Context 
    252         >>> tree = Node(two=Node(three=Node(), 
    253         ...                      four=Node()), 
    254         ...                      five=Alias(target='../two/*')) 
    255         >>> context = Context(None, None) 
    256         >>> list(tree.children(context)) 
     299        >>> from cly import * 
     300        >>> grammar = Grammar(two=Node(three=Node(), 
     301        ...                            four=Node()), 
     302        ...                            five=Alias(target='../two/*')) 
     303        >>> parser = Parser(grammar) 
     304        >>> context = Context(parser, None) 
     305        >>> list(grammar.children(context)) 
    257306        [<Alias:/five for /two/*>, <Node:/two>] 
    258         >>> list(tree.children(context, follow=True)) 
     307        >>> list(grammar.children(context, follow=True)) 
    259308        [<Node:/two/four>, <Node:/two/three>, <Node:/two>] 
    260309        """ 
     
    327376        >>> grammar.depth() 
    328377        0 
    329         >>> grammar.find('two').depth() 
     378        >>> grammar.find('/two').depth() 
    330379        1 
    331380        """ 
     
    337386 
    338387        >>> grammar = Grammar(one=Node(), two=Node()) 
    339         >>> grammar.find('two').path() 
     388        >>> grammar.find('/two').path() 
    340389        '/two' 
    341390        """ 
     
    352401        default is to use the content of self.help(). 
    353402 
     403        Arguments: 
     404 
     405            :text: Text entered so far. 
     406 
    354407        >>> grammar = Grammar(one=Node(), two=Node()) 
    355         >>> list(grammar.find('one').candidates(None, 'o')) 
     408        >>> list(grammar.find('/one').candidates(None, 'o')) 
    356409        ['one '] 
    357         >>> list(grammar.find('one').candidates(None, 't')) 
     410        >>> list(grammar.find('/one').candidates(None, 't')) 
    358411        [] 
    359412        """ 
     
    364417    def find(self, path): 
    365418        """Find a Node by path rooted at this node. 
     419 
     420        Arguments: 
     421 
     422            :path: "Path" to the node, or a label. 
    366423 
    367424        >>> top = Node(name='top', one=Node(), 
     
    374431        InvalidNodePath: /top/one/bar 
    375432        """ 
     433        if self.label == path: 
     434            return self 
    376435        components = filter(None, path.split('/')) 
    377436        if not components: 
    378437            return self 
    379438        for child in self: 
    380             if child.name == components[0]: 
    381                 return child.find('/'.join(components[1:])) 
    382         raise InvalidNodePath(posixpath.join(self.path(), path.strip('/'))) 
     439            if not path.startswith('/'): 
     440                return child.find(path) 
     441            elif child.name == components[0]: 
     442                return child.find('/' + '/'.join(components[1:])) 
     443        if path.startswith('/'): 
     444            raise InvalidNodePath(posixpath.join(self.path(), path.strip('/'))) 
     445        else: 
     446            raise InvalidNodePath(path) 
    383447 
    384448    def valid(self, context): 
     
    400464        will be preserved and the merging nodes children merged. 
    401465 
    402         :param node: Node to merge into this. 
     466        Arguments: 
     467 
     468            :node: Node to merge into this. 
    403469        """ 
    404470        self.__anonymous_children += node.__anonymous_children 
     
    412478        return '<%s:%s>' % (self.__class__.__name__, self.path() or '<root>') 
    413479 
    414  
    415 class Apply(Node): 
    416     """Apply settings to all ancestor nodes. 
    417  
    418     Terminates application of settings on any deeper Apply node. 
    419  
    420     Before applying settings: 
    421  
    422     >>> top = Node(one=Node(), two=Node(three=Node())) 
    423     >>> [node.traversals for node in top.walk()] 
    424     [1, 1, 1, 1] 
    425  
    426     And after applying settings: 
    427  
    428     >>> apply = Apply(traversals=0)(top) 
    429     >>> [node.traversals for node in top.walk()] 
    430     [0, 0, 0, 0] 
    431     """ 
    432     pattern = '' 
    433  
    434     def __init__(self, **apply): 
    435         self._apply = apply 
    436         Node.__init__(self, help=object.__repr__(self)) 
    437  
    438     def __call__(self, *anonymous, **kwargs): 
    439         result = Node.__call__(self, *anonymous, **kwargs) 
    440  
    441         def stop_on_ancestors(node): 
    442             return node is self or not isinstance(node, Apply) 
    443  
    444         for child in self.walk(predicate=stop_on_ancestors): 
    445             if child is self: 
    446                 Node.__call__(self, **self._apply) 
    447             else: 
    448                 child(**self._apply) 
    449  
    450         return result 
    451  
    452     def valid(self, context): 
    453         return True 
    454  
    455     def follow(self, context): 
    456         for child in self: 
    457             yield child 
     480    @classmethod 
     481    def xml_attribute_casts(cls): 
     482        """Define functions for casting attributes to their correct Python type. 
     483 
     484        Parent classes are automatically merged, so upcalling is unnecessary. 
     485 
     486        :returns: A dictionary of name, function mappings. 
     487        """ 
     488        return { 
     489            'traversals': int, 'group': _xml_group_type, 
     490            'order': int, 'match_candidates': _xml_boolean_type, 
     491            'with_context': _xml_boolean_type, 
     492            } 
     493 
     494    @classmethod 
     495    def xml_attribute_aliases(cls): 
     496        """Define attribute aliases for this node. 
     497 
     498        Parent classes are automatically merged, so upcalling is unnecessary. 
     499 
     500        :returns: Mapping of old to new keys. 
     501        """ 
     502        return {'if': 'valid'} 
    458503 
    459504 
     
    462507 
    463508    Masquerade is a general-purpose tool for dynamically inserting nodes into 
    464     the grammar. Implementations should override the ``masqueraded()`` method. 
     509    the grammar at the current location. Implementations should override the 
     510    ``masqueraded()`` method. 
    465511 
    466512    Use cases: 
     
    494540        """Return a sequence of all masqueraded nodes. 
    495541 
    496         :param context: Parse context. 
    497         :returns: Sequence of Nodes. 
    498         """ 
    499         raise NotImplementedError 
     542        Masquerades as child nodes by default. 
     543        """ 
     544        return super(Masquerade, self).children(context, follow=True) 
    500545 
    501546    def selected(self, context, match): 
     
    518563 
    519564 
     565class Set(Masquerade): 
     566    """Set variables in a branch.""" 
     567    def __init__(self, vars='', unset='', **kwargs): 
     568        self.vars = eval('dict(%s)' % vars) 
     569        self.unset = unset 
     570        super(Set, self).__init__(**kwargs) 
     571 
     572    def follow(self, context): 
     573        context.vars.update(self.vars) 
     574        return super(Set, self).follow(context) 
     575 
     576 
     577class Group(Masquerade): 
     578    """Group subnodes under a single group ID. 
     579 
     580    Arguments: 
     581        :id: Group ID. 
     582    """ 
     583    def __init__(self, id=None, *args, **kwargs): 
     584        super(Group, self).__init__(group=id, *args, **kwargs) 
     585 
     586    @classmethod 
     587    def xml_attribute_aliases(cls): 
     588        return {'id': _xml_group_type} 
     589 
     590 
    520591class Alias(Masquerade): 
    521592    """An alias for another node, or set of nodes. 
     
    524595    are supported. 
    525596 
    526     Constructor arguments: 
    527  
    528     :param alias: 
    529         Relative or absolute path to the aliased node. If the alias contains 
    530         glob characters (``*`` or ``?``) all matching nodes are aliased. 
     597    Arguments: 
     598 
     599        :target: 
     600            Relative or absolute path to the aliased node. If the alias contains 
     601            glob characters (``*`` or ``?``) all matching nodes are aliased. 
    531602 
    532603    >>> from cly.parser import Parser, Context 
     
    537608    >>> alias 
    538609    <Alias:/four for /one> 
    539     >>> context = Context(None, None) 
     610    >>> context = Context(parser, None) 
    540611    >>> list(alias.follow(context)) 
    541612    [<Node:/one>] 
     
    557628    def masqueraded(self, context): 
    558629        """Return an iterable of all aliased nodes.""" 
     630        # Find label path, if any 
     631        if '/' in self._target: 
     632            label, path = self._target.split('/', 1) 
     633        else: 
     634            label, path = self._target, '' 
     635        if label in context.parser.labels: 
     636            node = context.parser.labels[label] 
     637            target = posixpath.normpath(posixpath.join(node.path(), path)) 
     638        else: 
     639            target = self.target 
     640 
    559641        root = self 
    560642        while root.parent: 
    561643            root = root.parent 
    562644        try: 
    563             yield root.find(self.target) 
     645            yield root.find(target) 
    564646        except InvalidNodePath: 
    565647            from fnmatch import fnmatch 
    566             start = root.find(posixpath.dirname(self.target)) 
    567             match = posixpath.basename(self.target) 
    568             for child in start.children(context, True): 
     648            start = root.find(posixpath.dirname(target)) 
     649            match = posixpath.basename(target) 
     650            for child in start.children(context, follow=True): 
    569651                if fnmatch(child.name, match): 
    570652                    yield child 
    571653 
    572654    def _get_target(self): 
    573         """Absolute path to the aliased node.""" 
     655        """Absolute (normalised) path to the aliased node.""" 
    574656        return posixpath.normpath(posixpath.join(self.path(), self._target)) 
    575657 
     
    581663 
    582664 
    583 class Conditional(Masquerade): 
     665class If(Masquerade): 
    584666    """A set of conditional nodes. 
    585667 
    586     A node that masquerades as others, based on a condition function. 
    587  
    588     :param condition: A callable with the signature ``condition(context)``. 
    589                       Returns True if masqueraded nodes are accessible. 
    590  
    591     All other arguments are passed through to the default ``Node`` constructor. 
     668    A node that masquerades as its children, if a condition is true. 
     669 
     670    Arguments: 
     671 
     672        :test: A callable with the signature ``test(context)``. 
     673               Returns ``True`` if masqueraded nodes are accessible. 
     674 
     675    All other arguments are passed through to the default :class:`Node` constructor. 
    592676 
    593677    >>> from cly.parser import Parser 
    594678    >>> active = False 
    595     >>> parser = Parser(Grammar(Conditional(lambda c: active, one=Node()))) 
     679    >>> parser = Parser(Grammar(If(lambda c: active, one=Node()))) 
    596680    >>> parser.parse('one') 
    597681    <Context command:'one' remaining:'one'> 
     
    601685    """ 
    602686 
    603     def __init__(self, condition, *args, **kwargs): 
    604         kwargs['condition'] = condition 
    605         super(Conditional, self).__init__(*args, **kwargs) 
     687    def __init__(self, test, *args, **kwargs): 
     688        kwargs['test'] = test 
     689        super(If, self).__init__(*args, **kwargs) 
    606690 
    607691    def masqueraded(self, context): 
    608         if not self.condition(context): 
     692        if not self.test(context): 
    609693            return [] 
    610         return Node.children(self, context, follow=True) 
     694        return super(If, self).masqueraded(context) 
     695 
     696    def test(self, context): 
     697        raise NotImplementedError 
     698 
     699 
     700class Apply(Masquerade): 
     701    """Apply settings to all ancestor nodes. 
     702 
     703    Terminates application of settings on any deeper Apply node. 
     704 
     705    Before applying settings: 
     706 
     707    >>> top = Node(one=Node(), two=Node(three=Node())) 
     708    >>> [node.traversals for node in top.walk()] 
     709    [1, 1, 1, 1] 
     710 
     711    And after applying settings: 
     712 
     713    >>> apply = Apply(traversals=0)(top) 
     714    >>> [node.traversals for node in top.walk()] 
     715    [0, 0, 0, 0] 
     716    """ 
     717    def __init__(self, **apply): 
     718        self._apply = apply 
     719        super(Apply, self).__init__() 
     720 
     721    def __call__(self, *anonymous, **kwargs): 
     722        result = Node.__call__(self, *anonymous, **kwargs) 
     723 
     724        def stop_on_ancestors(node): 
     725            return node is self or not isinstance(node, Apply) 
     726 
     727        for child in self.walk(predicate=stop_on_ancestors): 
     728            if child is self: 
     729                Node.__call__(self, **self._apply) 
     730            else: 
     731                child(**self._apply) 
     732 
     733        return result 
     734 
    611735 
    612736 
    613737class Action(Node): 
    614     """Action node, matches EOL. The ``callback`` arg will be used as the 
    615     callable. 
    616  
    617     :param callback: Callback to execute when the action is chosen. 
    618     :attr with_context: If True, passes the current parse Context as the first 
    619                         argument. 
     738    """Matches EOL and executes ``callback``. 
     739 
     740    Arguments: 
     741 
     742        :callback: Callback to execute when the action is chosen. 
     743 
     744    Attributes: 
     745 
     746        .. attribute:: with_context 
     747 
     748            If True, passes the current parse :class:`~cly.parser.Context` as the 
     749            first argument. 
    620750 
    621751    >>> from cly.parser import Parser, Context 
     
    625755    >>> parser = Parser(grammar) 
    626756    >>> context = Context(parser, 'foo bar') 
    627     >>> node = grammar.find('action') 
     757    >>> node = grammar.find('/action') 
    628758    >>> node.help(None) 
    629759    (('<eol>', ''),) 
     
    632762    """ 
    633763    pattern = '$' 
    634     group = 9999 
    635764    with_context = None 
    636765 
    637766    def __init__(self, callback, *anonymous, **kwargs): 
    638767        help = kwargs.pop('help', '') 
     768        kwargs.setdefault('group', 9999) 
    639769        if isinstance(help, basestring): 
    640770            help_string = help 
     
    655785        # and if we do they get excluded from help. 
    656786        pass 
     787 
     788    @classmethod 
     789    def xml_attribute_aliases(cls): 
     790        return {'exec': 'callback'} 
    657791 
    658792 
     
    733867class Grammar(Node): 
    734868    """The root node for a grammar.""" 
    735     pattern = '' 
     869    pattern = '^' 
     870 
    736871    def __init__(self, *anonymous, **kwargs): 
    737872        Node.__init__(self, help='<root>', *anonymous, **kwargs) 
     
    740875        """Null-op for empty lines.""" 
    741876 
    742     def from_xml(cls, xml, extra_nodes=None, **extra_locals): 
    743         """Build a CLY Grammar from XML. 
    744  
    745         :param xml: 
    746             XML source as a string. 
    747         :param extra_nodes: 
    748             Dictionary of Node subclasses. 
    749         :param extra_locals: 
    750             Valid locals() when evaluating XML grammar node attributes. 
    751  
    752         Returns a new Grammar object. 
    753         """ 
    754  
    755         warnings.warn('Grammar.from_xml() has been deprecated. Use ' 
    756                       'builder.XMLGrammar, but note changes in semantics.', 
    757                       DeprecationWarning, stacklevel=2) 
    758  
    759         try: 
    760             dom = minidom.parseString(xml) 
    761         except Exception, e: 
    762             raise XMLParseError(str(e)) 
    763  
    764         extra_nodes = extra_nodes or [] 
    765  
    766         def boolean(v): 
    767             return v in ('True', 'true', '1', 'yes') 
    768  
    769         def evaluate(v): 
    770             def lazy_evaluator(*args, **kwargs): 
    771                 return eval(v, extra_locals)(*args, **kwargs) 
    772             return lazy_evaluator 
    773  
    774         arg_types = { 
    775             'traversals': int, 
    776             'group': int, 
    777             'order': int, 
    778             'match_candidates': boolean, 
    779         } 
    780  
    781         arg_types.update(dict.fromkeys( 
    782             'children follow selected next match advance visible ' \ 
    783             'terminal depth path candidates find valid callback'.split(), 
    784             evaluate 
    785             )) 
    786  
    787         node_classes = [globals()[i] for i in __all__] 
    788         node_types = dict([(v.__name__.lower(), v) 
    789                           for v in chain(node_classes, extra_nodes) 
    790                           if isclass(v) and issubclass(v, Node)]) 
    791  
    792         def parse(parent, xnode): 
    793             if not xnode: 
    794                 return 
    795  
    796             if xnode.nodeType == minidom.Node.ELEMENT_NODE: 
    797                 cls = node_types.get(xnode.localName.lower()) 
    798                 if not cls: 
    799                     raise XMLParseError('Invalid node type "%s"' % 
    800                                         xnode.localName.lower()) 
    801  
    802                 attributes = dict([(str(k), v) for k, v 
    803                                    in xnode.attributes.items()]) 
    804  
    805                 for k, v in attributes.items(): 
    806                     if k.startswith('eval:'): 
    807                         attributes.pop(k) 
    808                         k = k[5:] 
    809                         v = eval(v, extra_locals, {}) 
    810                     if k == 'group': 
    811                         try: 
    812                             v = arg_types.get(k, str)(v) 
    813                         except ValueError: 
    814                             pass 
    815                     else: 
    816                         v = arg_types.get(k, str)(v) 
    817                     attributes[k] = v 
    818  
    819                 name = attributes.pop('name', None) 
    820                 try: 
    821                     node = cls(**attributes) 
    822                 except Exception, e: 
    823                     e.args = ('Node construction of %s failed: %s' % (cls, e),) 
    824                     raise 
    825                 if name: 
    826                     path = parent.path() + '/' + name 
    827                     parent(**{str(name): node}) 
    828                 else: 
    829                     parent(node) 
    830             else: 
    831                 node = parent 
    832  
    833             parse(node, xnode.firstChild) 
    834             parse(parent, xnode.nextSibling) 
    835  
    836         grammar = Grammar() 
    837         if dom.firstChild.localName != 'grammar': 
    838             raise XMLParseError('Invalid root element "%s", expected "grammar"' 
    839                                 % dom.firstChild.localName) 
    840         parse(grammar, dom.firstChild.firstChild) 
    841         return grammar 
    842  
    843     from_xml = classmethod(from_xml) 
    844  
    845  
    846 def _xml_lazy_attr_evaluator(attr, positional_args=None): 
    847     """Return a callable that lazily evaluates an expression. 
    848  
    849     :param attr: Python expression to evaluate as a string. 
    850     :param positional_args: List of positional argument names to map to 
    851                             locals. 
    852     """ 
    853     # Extract positional arguments from function object 
    854     positional_args = positional_args or [] 
    855     def xml_attr_evaluator(*args, **kwargs): 
    856         locals = dict(kwargs) 
    857  
    858         # Convert positional args into locals 
    859         if args: 
    860             if not positional_args or len(positional_args) < len(args): 
    861                 raise XMLParseError( 
    862                     'Lazily evaluated XML attribute "%s" called with unknown ' 
    863                     'positional arguments. This is not supported.' % attr 
    864                     ) 
    865             locals.update(zip(positional_args[:len(args)], args)) 
    866  
    867         if 'context' in locals: 
    868             context = locals.pop('context') 
    869             data = context.data 
    870             if isinstance(data, dict): 
    871                 locals.update(data) 
    872             vars = context.vars 
    873         else: 
    874             data = {} 
    875             vars = {} 
    876         locals.update(vars) 
    877         locals['v'] = vars 
    878         locals['a'] = args 
    879         locals['kw'] = kwargs 
    880         locals['d'] = data 
    881         locals['defined'] = lambda v: v in locals 
    882         try: 
    883             return eval(attr, locals) 
    884         except Exception, e: 
    885             e.args = e.args + ('(while parsing "%s")' % attr,) 
    886             raise 
    887     return xml_attr_evaluator 
    888  
    889  
    890 def _xml_boolean_type(value): 
    891     """Converter for boolean attributes.""" 
    892     return str(value).lower() in ('true', '1', 'yes') 
    893  
    894  
    895 def _xml_group_type(value): 
    896     """Parse a group="n" attribute.""" 
    897     try: 
    898         return int(value) 
    899     except ValueError: 
    900         return str(value) 
    901  
    902877 
    903878class XMLGrammar(Grammar): 
    904879    """A Grammar that builds its structure from an XML file. 
    905880 
    906     The XML grammar is a simple mapping from element names to Node subclasses 
    907     and element attributes to constructor arguments. Unlike when building 
    908     a grammar from Node objects, the `name` must be explicitly provided. 
     881    The XML grammar is a simple mapping from element names to :class:`Node` 
     882    subclasses, and element attributes to constructor arguments. Unlike when 
     883    building a grammar from :class:`Node` objects, the ``name`` must be 
     884    explicitly provided. 
     885 
     886    Arguments: 
     887        :file: Filename or file-like object to load XML grammar from. 
     888        :extra_nodes: A sequence of extra :class:`Node` subclasses to make 
     889                      available as elements. 
    909890 
    910891    eg. 
    911892 
    912     .. code-block:: python 
     893    .. code-block:: xml 
    913894 
    914895        <word name="abc" valid="'var' in v" pattern=r"[abc]+"/> 
     
    923904        ) 
    924905 
    925     Attributes that are methods on the Node will be evaluated as expressions. 
    926     All variables from the parse Context, "data" dictionary, and keyword 
    927     arguments (in that order) are available as locals to the evaluated 
    928     expression, as well as some additional variables: 
    929  
    930         v 
    931             All variables from the parse Context. 
    932         d 
    933             The "data" dictionary. 
    934         a 
    935             Any positional arguments. 
    936         kw 
    937             Any keyword arguments. 
    938  
    939         defined(key) 
    940             Returns true if key is a valid symbol. 
     906    Attributes that are methods on the :class:`Node` will be evaluated as expressions. 
     907    All variables from the parse :class:`~cly.parser.Context`, "data" 
     908    dictionary, and keyword arguments (in that order) are available as locals 
     909    to the evaluated expression, as well as some additional variables: 
     910 
     911        :v: All variables from the parse Context. 
     912        :d: The "data" dictionary. 
     913        :a: Any positional arguments. 
     914        :kw: Any keyword arguments. 
     915        :all(*keys): Returns true if all keys are valid symbols. 
     916        :any(*keys): Returns true if any keys are valiid symbols. 
    941917 
    942918    v in particular is useful for passing all collected arguments to 
    943919    functions. 
    944920 
    945     Some other conveniences exist to make life easier when using the XML form: 
    946  
    947       - Node aliases: var -> variable, int -> integer, str -> string 
    948       - Attribute aliases: if -> valid, exec -> callback 
     921    Some convenient aliases also exist to make life easier when using XML 
     922    grammars. 
     923 
     924    Node aliases: 
     925 
     926        :var: variable 
     927        :int: integer 
     928        :str: string 
     929 
     930    Attribute aliases: 
     931 
     932        :if: valid 
     933        :exec: callback 
    949934 
    950935    eg. 
     
    977962    """ 
    978963 
    979     # Mapping of Node attributes to types. 
    980     default_attr_type_map = { 
    981         'traversals': int, 
    982         'group': _xml_group_type, 
    983         'order': int, 
    984         'match_candidates': _xml_boolean_type, 
    985         'with_context': _xml_boolean_type, 
    986         } 
    987  
    988964    node_aliases = { 
    989965        'var': 'variable', 
     
    992968        } 
    993969 
    994     attr_aliases = { 
    995         Node: {'if': 'valid'}, 
    996         Action: {'exec': 'callback'}, 
    997         } 
    998  
    999     def __init__(self, file, extra_nodes=None, attr_type_map=None): 
     970    def __init__(self, file, extra_nodes=None): 
    1000971        super(XMLGrammar, self).__init__() 
    1001972 
     
    1008979        self.node_map.update([(k, self.node_map[v]) 
    1009980                              for k, v in self.node_aliases.items()]) 
    1010  
    1011         self.attr_type_map = dict(self.default_attr_type_map) 
    1012         self.attr_type_map.update(attr_type_map or {}) 
    1013981 
    1014982        if dom.firstChild.localName != 'grammar': 
     
    10551023 
    10561024    def parse_attributes(self, cls, xnode): 
    1057         # Parse attributes 
    1058         def lookup_key(k): 
    1059             for base in cls.mro(): 
    1060                 lookup = self.attr_aliases.get(base, {}).get(k) 
    1061                 if lookup is not None: 
    1062                     return self.attr_aliases[base].get(k, k) 
    1063             return k 
    1064         attributes = dict([(lookup_key(str(k)), v) for k, v 
     1025        # Delegate to node classes for attribute aliases and type conversion 
     1026        # callbacks. 
     1027        aliases = {} 
     1028        attr_type_map = {} 
     1029        def call_and_merge(method_name, d): 
     1030            for c in reversed(cls.mro()): 
     1031                if method_name in c.__dict__: 
     1032                    d.update(getattr(c, method_name)()) 
     1033        call_and_merge('xml_attribute_aliases', aliases) 
     1034        call_and_merge('xml_attribute_casts', attr_type_map) 
     1035 
     1036        attributes = dict([(str(aliases.get(k, k)), v) for k, v 
    10651037                           in xnode.attributes.items()]) 
    10661038 
    10671039        for k, v in attributes.items(): 
    10681040            # Do type-map conversion 
    1069             if k in self.attr_type_map: 
    1070                 v = self.attr_type_map[k](v) 
     1041            if k in attr_type_map: 
     1042                v = attr_type_map[k](v) 
    10711043            # Is the destination Node attribute callable? Do a lazy eval. 
    10721044            elif callable(getattr(cls, k, None)): 
     
    10891061 
    10901062 
     1063def _xml_lazy_attr_evaluator(attr, positional_args=None): 
     1064    """Return a callable that lazily evaluates an expression. 
     1065 
     1066    Arguments: 
     1067        :attr: Python expression to evaluate as a string. 
     1068        :positional_args: List of positional argument names to map to locals. 
     1069    """ 
     1070    # Extract positional arguments from function object 
     1071    positional_args = positional_args or [] 
     1072    def xml_attr_evaluator(*args, **kwargs): 
     1073        def all(*v): 
     1074            """Return true if all arguments are defined symbols.""" 
     1075            for i in v: 
     1076                if i not in locals: 
     1077                    return False 
     1078            return True 
     1079 
     1080        def any(*v): 
     1081            """Return true if any arguments are defined symbols.""" 
     1082            for i in v: 
     1083                if i in locals: 
     1084                    return True 
     1085            return False 
     1086 
     1087        locals = dict(kwargs) 
     1088 
     1089        # Convert positional args into locals 
     1090        if args: 
     1091            if not positional_args or len(positional_args) < len(args): 
     1092                raise XMLParseError( 
     1093                    'Lazily evaluated XML attribute "%s" called with unknown ' 
     1094                    'positional arguments. This is not supported.' % attr 
     1095                    ) 
     1096            locals.update(zip(positional_args[:len(args)], args)) 
     1097 
     1098        if 'context' in locals: 
     1099            context = locals.pop('context') 
     1100            data = context.data 
     1101            if isinstance(data, dict): 
     1102                locals.update(data) 
     1103            vars = context.vars 
     1104        else: 
     1105            data = {} 
     1106            vars = {} 
     1107        locals.update(vars) 
     1108        locals['v'] = vars 
     1109        locals['a'] = args 
     1110        locals['kw'] = kwargs 
     1111        locals['d'] = data 
     1112        locals['all'] = all 
     1113        locals['any'] = any 
     1114        try: 
     1115            return eval(attr, locals) 
     1116        except Exception, e: 
     1117            e.args = e.args + ('while parsing "%s"' % attr,) 
     1118            raise 
     1119    return xml_attr_evaluator 
     1120 
     1121 
     1122def _xml_boolean_type(value): 
     1123    """Converter for boolean attributes.""" 
     1124    return str(value).lower() in ('true', '1', 'yes') 
     1125 
     1126 
     1127def _xml_group_type(value): 
     1128    """Parse a group="n" attribute.""" 
     1129    try: 
     1130        return int(value) 
     1131    except ValueError: 
     1132        return value 
     1133 
     1134 
    10911135class Help(object): 
    10921136    """A callable object representing help for a Node. 
     
    10941138    Returns an iterable of pairs in the form (key, help). 
    10951139 
    1096     Constructor arguments 
    1097  
    1098     :param doc: 
    1099         An iterable of two element tuples in the form ``(key, help)``. 
    1100  
    1101         >>> h = Help([('a', 'b'), ('b', 'c')]) 
    1102         >>> [i for i in h(None)] 
    1103         [('a', 'b'), ('b', 'c')] 
     1140    Arguments: 
     1141 
     1142        :doc: 
     1143            An iterable of two element tuples in the form ``(key, help)``. 
     1144 
     1145    >>> h = Help([('a', 'b'), ('b', 'c')]) 
     1146    >>> [i for i in h(None)] 
     1147    [('a', 'b'), ('b', 'c')] 
    11041148    """ 
    11051149    def __init__(self, doc): 
     
    11131157    @staticmethod 
    11141158    def pair(name, help): 
    1115         """Create a Help object from a single (name, help) pair. 
     1159        """Create a Help object from a single ``(name, help)`` pair. 
    11161160 
    11171161        >>> h = Help.pair('a', 'b') 
     
    11581202    '123' 
    11591203    """ 
    1160     pattern = r'(?i)[A-Z_]\w+' 
     1204    pattern = r'(?i)[A-Z_]\w*' 
    11611205 
    11621206 
     
    11761220 
    11771221 
     1222class KeyValue(Variable): 
     1223    """Match and store a key value pair.""" 
     1224 
     1225    def __init__(self, sep='=', value_pattern=r'\S+', *args, **kwargs): 
     1226        pattern = kwargs.pop('pattern', r'(\w+)\s*' + sep + '\s*(' + value_pattern + ')') 
     1227        super(KeyValue, self).__init__(pattern=pattern, *args, **kwargs) 
     1228 
     1229    def parse(self, context, match): 
     1230        return match.group(1), match.group(2) 
     1231 
     1232 
    11781233class String(Variable): 
    11791234    """Matches either a bare word or a quoted string. 
     
    12331288    '123.45' 
    12341289    """ 
    1235     pattern = r'\d+' 
     1290    pattern = r'[-+]?\d+' 
    12361291 
    12371292    def parse(self, context, match): 
     
    12761331 
    12771332class IP(Variable): 
    1278     """Match an IP address, parsing it as a tuple of four integers. 
     1333    """Match an IP address. 
    12791334 
    12801335    >>> from cly.parser import Parser 
     
    12981353 
    12991354 
     1355class CIDR(Variable): 
     1356    """Match a CIDR network representation. 
     1357 
     1358    If a netmask is not provided a default of /32 will be used. 
     1359 
     1360    >>> from cly import * 
     1361    >>> parser = Parser(Grammar(foo=CIDR())) 
     1362    >>> parser.parse('123.34.67.89').vars['foo'] 
     1363    '123.34.67.89/32' 
     1364    >>> parser.parse('123.34.67.89/24').vars['foo'] 
     1365    '123.34.67.89/24' 
     1366    """ 
     1367    pattern = r'(%s)(?:/(\d{1,2}))?' % IP.pattern 
     1368 
     1369    def parse(self, context, match): 
     1370        mask = match.group(6) or '32' 
     1371        return match.group(1) + '/' + mask 
     1372 
     1373 
    13001374class Hostname(Variable): 
    13011375    """Match only a hostname (not an IP address). 
    13021376 
     1377    Arguments: 
     1378        :parts: The minimum number of host parts required. 
     1379        :suffix: Optional domain suffix to require. 
     1380 
    13031381    >>> from cly.parser import Parser 
    1304     >>> parser = Parser(Grammar(foo=Hostname())) 
     1382    >>> parser = Parser(Grammar(foo=Hostname(parts=2))) 
    13051383    >>> parser.parse('www').vars['foo'] 
    1306     'www' 
     1384    Traceback (most recent call last): 
     1385    ... 
     1386    KeyError: 'foo' 
    13071387    >>> parser.parse('www.example.com').vars['foo'] 
    13081388    'www.example.com' 
     
    13131393    ... 
    13141394    KeyError: 'foo' 
     1395 
    13151396    """ 
    13161397    pattern = r'(?i)([A-Z0-9][A-Z0-9_-]*)((\.([A-Z0-9][A-Z0-9_-]*))*\.([A-Z0-9][A-Z0-9_-]*[A-Z]))?\.?' 
    13171398 
     1399    parts = 0 
     1400    suffix = None 
     1401 
     1402    def match(self, context): 
     1403        match = Variable.match(self, context) 
     1404        if match and self.parts and len(match.group().split('.')) < self.parts: 
     1405            match = None 
     1406        if match and self.suffix and not match.group().endswith(self.suffix): 
     1407            match = None 
     1408        return match 
     1409 
    13181410 
    13191411class Host(Variable): 
    1320     """Match either an IP address. 
     1412    """Match either an IP address or a hostname. 
    13211413 
    13221414    >>> from cly.parser import Parser 
     
    14171509        return [clean(os.path.join(dir, f)) 
    14181510                for f in candidates if self.allow_dotfiles or f[0] != '.'] 
     1511 
     1512 
     1513class AbsoluteTime(Variable): 
     1514    """Parse an absolute time value in the form HH:MM[:SS]]. 
     1515 
     1516    :returns: a datetime.time object. 
     1517    """ 
     1518    pattern = r'(\d\d):(\d\d)(?::(\d\d))?' 
     1519 
     1520    def parse(self, context, match): 
     1521        hour, minute, second = int(match.group(1)), int(match.group(2)), \ 
     1522                               int(match.group(3) or 0) 
     1523        return datetime.time(hour=hour, minute=minute, second=second) 
     1524 
     1525 
     1526class RelativeTime(Variable): 
     1527    """Parse a relative time. 
     1528 
     1529    Relative times are specified as an optionally negative float followed by a 
     1530    single character time unit: 
     1531 
     1532        [-]NN.N[w|d|h|m|s] 
     1533 
     1534    eg. 
     1535 
     1536    15m, 3.5d 
     1537 
     1538    :returns: a datetime.timedelta object. 
     1539    """ 
     1540 
     1541    units = ['weeks', 'days', 'hours', 'minutes', 'seconds'] 
     1542    units = dict((u[0], u) for u in units) 
     1543    pattern = r'(?i)(%s)([%s])' % (Float.pattern, ''.join(units)) 
     1544 
     1545    def parse(self, context, match): 
     1546        units = match.group(2).lower() 
     1547        value = float(match.group(1)) 
     1548        args = {self.units[units]: value} 
     1549        return datetime.timedelta(**args) 
     1550 
     1551 
     1552class FixedOffsetTZ(datetime.tzinfo): 
     1553    """Fixed offset in minutes east from UTC.""" 
     1554 
     1555    def __init__(self, offset, name): 
     1556        self._offset = datetime.timedelta(minutes=offset) 
     1557        self.zone = name 
     1558 
     1559    def __str__(self): 
     1560        return self.zone 
     1561 
     1562    def __repr__(self): 
     1563        return '<FixedOffsetTZ "%s" %s>' % (self.zone, self._offset) 
     1564 
     1565    def utcoffset(self, dt): 
     1566        return self._offset 
     1567 
     1568    def tzname(self, dt): 
     1569        return self.zone 
     1570 
     1571    def dst(self, dt): 
     1572        return _zero 
     1573 
     1574 
     1575class Timezone(Variable): 
     1576    """Parse a timezone, using pytz if available.""" 
     1577 
     1578    STATIC_TIMEZONES = [ 
     1579        FixedOffsetTZ(0, 'UTC'), 
     1580        FixedOffsetTZ(-720, 'GMT-12:00'), FixedOffsetTZ(-660, 'GMT-11:00'), 
     1581        FixedOffsetTZ(-600, 'GMT-10:00'), FixedOffsetTZ(-540, 'GMT-9:00'), 
     1582        FixedOffsetTZ(-480, 'GMT-8:00'),  FixedOffsetTZ(-420, 'GMT-7:00'), 
     1583        FixedOffsetTZ(-360, 'GMT-6:00'),  FixedOffsetTZ(-300, 'GMT-5:00'), 
     1584        FixedOffsetTZ(-240, 'GMT-4:00'),  FixedOffsetTZ(-180, 'GMT-3:00'), 
     1585        FixedOffsetTZ(-120, 'GMT-2:00'),  FixedOffsetTZ(-60, 'GMT-1:00'), 
     1586        FixedOffsetTZ(0, 'GMT'),           FixedOffsetTZ(60, 'GMT+1:00'), 
     1587        FixedOffsetTZ(120, 'GMT+2:00'),   FixedOffsetTZ(180, 'GMT+3:00'), 
     1588        FixedOffsetTZ(240, 'GMT+4:00'),   FixedOffsetTZ(300, 'GMT+5:00'), 
     1589        FixedOffsetTZ(360, 'GMT+6:00'),   FixedOffsetTZ(420, 'GMT+7:00'), 
     1590        FixedOffsetTZ(480, 'GMT+8:00'),   FixedOffsetTZ(540, 'GMT+9:00'), 
     1591        FixedOffsetTZ(600, 'GMT+10:00'),  FixedOffsetTZ(660, 'GMT+11:00'), 
     1592        FixedOffsetTZ(720, 'GMT+12:00'),  FixedOffsetTZ(780, 'GMT+13:00'), 
     1593        ] 
     1594    STATIC_TIMEZONES = dict([(z.zone, z) for z in STATIC_TIMEZONES]) 
     1595 
     1596    pattern = r'[+:/\w]+' 
     1597    match_candidates = True 
     1598 
     1599    if pytz: 
     1600        def candidates(self, context, text): 
     1601            return cull_candidates(pytz.all_timezones, text) 
     1602 
     1603        def parse(self, context, match): 
     1604            return pytz.timezone(match.group()) 
     1605    else: 
     1606        def candidates(self, context, text): 
     1607            return cull_candidates(self.STATIC_TIMEZONES, text) 
     1608 
     1609        def parse(self, context, match): 
     1610            return self.STATIC_TIMEZONES[match.group()] 
  • cly/trunk/cly/console.py

    r547 r556  
    1212control sequences. The syntax is a carat ``^`` followed by a single character. 
    1313 
    14 Valid formatting controls are: 
    15  
    16 ``^N`` 
    17     Reset all formatting. 
    18 ``^B`` 
    19     Toggle bold. 
    20 ``^U`` 
    21     Toggle underline. 
    22 ``^0`` 
    23     Set black foreground. 
    24 ``^1`` 
    25     Set red foreground. 
    26 ``^2`` 
    27     Set green foreground. 
    28 ``^3`` 
    29     Set brown foreground. 
    30 ``^4`` 
    31     Set blue foreground. 
    32 ``^5`` 
    33     Set magenta foreground. 
    34 ``^6`` 
    35     Set cyan foreground. 
    36 ``^7`` 
    37     Set white foreground. 
     14Valid colour escape sequences are: 
     15 
     16    :``^N``: Reset all formatting. 
     17    :``^B``: Toggle bold. 
     18    :``^U``: Toggle underline. 
     19    :``^0``: Set black foreground. 
     20    :``^1``: Set red foreground. 
     21    :``^2``: Set green foreground. 
     22    :``^3``: Set brown foreground. 
     23    :``^4``: Set blue foreground. 
     24    :``^5``: Set magenta foreground. 
     25    :``^6``: Set cyan foreground. 
     26    :``^7``: Set white foreground. 
    3827 
    3928""" 
     
    4433import codecs 
    4534 
     35 
     36__all__ = """ 
     37cwrite getch cerror cfatal register_codec cinfo cjustify clen cprint cprintstrip 
     38csplice cwarning cwraptext print_table rjustify termheight termwidth wraptoterm 
     39""".split() 
    4640 
    4741__docformat__ = 'restructuredtext en' 
     
    175169 
    176170    def cwrite(io, text): 
    177         io.write(decode(text)[0]) 
     171        io.write(_decode(text)[0]) 
    178172else: 
    179173    _terminal_type = 'dumb' 
     
    187181 
    188182cwrite.__doc__ = \ 
    189     """Print using colour escape codes similar to the Quake engine. 
    190  
    191     That is, ^0-7 correspond to colours, ^B toggles bold, ^U toggles underline 
    192     and ^N is reset to normal text. Colour is not automatically reset at the 
    193     end of output. 
     183    """Print using simple colour escape codes. 
     184 
     185    Colour is not automatically reset at the end of output. 
    194186 
    195187    If ``sys.stdout`` is not a TTY, colour codes will be stripped. 
     
    197189 
    198190 
    199 class Codec(codecs.Codec): 
     191class _Codec(codecs.Codec): 
    200192    def __init__(self, *args, **kwargs): 
    201193        try: 
     
    252244 
    253245 
    254 class CodecStreamWriter(Codec, codecs.StreamWriter): 
     246class _CodecStreamWriter(_Codec, codecs.StreamWriter): 
    255247    def __init__(self, stream, errors='strict'): 
    256         Codec.__init__(self) 
     248        _Codec.__init__(self) 
    257249        codecs.StreamWriter.__init__(self, stream, errors) 
    258250        self.errors = errors 
     
    267259 
    268260 
    269 class CodecStreamReader(Codec, codecs.StreamReader): 
     261class _CodecStreamReader(_Codec, codecs.StreamReader): 
    270262    def __init__(self, stream, errors='strict'): 
    271         Codec.__init__(self) 
     263        _Codec.__init__(self) 
    272264        codecs.StreamReader.__init__(self, stream, errors) 
    273265 
     
    286278 
    287279 
    288 def decode(input, errors='strict'): 
    289     return (Codec(errors=errors).decode(input), len(input)) 
    290  
    291  
    292 def encode(input, errors='strict'): 
    293     return (Codec(errors=errors).encode(input), len(input)) 
     280def _decode(input, errors='strict'): 
     281    return (_Codec(errors=errors).decode(input), len(input)) 
     282 
     283 
     284def _encode(input, errors='strict'): 
     285    return (_Codec(errors=errors).encode(input), len(input)) 
    294286 
    295287 
     
    308300        if encoding != 'cly': 
    309301            return None 
    310         return (encode, decode, CodecStreamReader, CodecStreamWriter) 
     302        return (_encode, _decode, _CodecStreamReader, _CodecStreamWriter) 
    311303    return codecs.register(inner_register) 
    312304 
     
    401393 
    402394 
    403 def cwraptext(rtext, width=termwidth(), subsequent_indent=''): 
    404     """Wrap multi-line text to width (defaults to termwidth())""" 
     395def cwraptext(rtext, width=None, subsequent_indent=''): 
     396    """Wrap multi-line text to width (defaults to :func:`termwidth`)""" 
     397    if width is None: 
     398        width = termwidth() 
    405399    out = [] 
    406400    for text in rtext.splitlines(): 
     
    438432 
    439433 
    440 def rjustify(text, width=termwidth()): 
     434def rjustify(text, width=None): 
    441435    """Right justify the given text.""" 
     436    if width is None: 
     437        width = termwidth() 
    442438    text = cwraptext(text, width) 
    443439    out = '' 
     
    447443 
    448444 
    449 def cjustify(text, width=termwidth()): 
     445def cjustify(text, width=None): 
    450446    """Centre the given text.""" 
     447    if width is None: 
     448        width = termwidth() 
    451449    text = cwraptext(text, width) 
    452450    out = '' 
  • cly/trunk/cly/extra.py

    r547 r556  
    77# 
    88 
    9 """Useful functions for CLY.""" 
     9"""Useful functions for CLY. 
     10 
     11""" 
    1012 
    1113 
     
    1416 
    1517 
    16 def cull_candidates(candidates, text): 
    17     """Cull candidates that do not start with ``text``.""" 
    18     return filter(None, [c + ' ' for c in candidates if c.startswith(text)]) 
     18def cull_candidates(candidates, text, sep=' '): 
     19    """Cull candidates that do not start with ``text``. 
     20 
     21    Returned candidates also have a space appended. 
     22 
     23    This is a very common idiom when overriding :meth:`~cly.builder.Node.candidates`. 
     24 
     25    Arguments: 
     26        :candidates: Sequence of match candidates. 
     27        :text: Text to match. 
     28        :sep: Separator to append to match. 
     29 
     30    >>> from cly import * 
     31    >>> def match_names(context, text): 
     32    ...     return cull_candidates(['bob', 'fred', 'barry', 'harry'], text) 
     33    >>> parser = Parser(Grammar(node=Node(candidates=match_names))) 
     34    >>> list(parser.parse('b').candidates()) 
     35    ['bob ', 'barry '] 
     36    """ 
     37    return [c + sep for c in candidates if c and c.startswith(text)] 
    1938 
    2039 
     
    2241    """Convenience function to provide candidates matching a prefix. 
    2342 
    24     Returns a callable that can be used directly with ``Node.candidates=``. 
     43    Returns a callable that can be used as 
     44    :meth:`~cly.builder.Node.candidates`. 
    2545 
    26     >>> from cly.parser import Parser 
    27     >>> from cly.builder import Grammar, Node 
     46    Arguments: 
     47        :candidates: Sequence of match candidates. 
     48 
     49    >>> from cly import * 
    2850    >>> static_candidates('foo', 'bar')(None, 'f') 
    2951    ['foo '] 
     
    3254    ['foo ', 'fuzz '] 
    3355    """ 
    34     def cull_candidates(context, text): 
    35         return filter(None, [c + ' ' for c in candidates if c.startswith(text)]) 
    36     return cull_candidates 
     56    def _cull_candidates(context, text): 
     57        return cull_candidates(candidates, text) 
     58    return _cull_candidates 
    3759 
    3860 
  • cly/trunk/cly/interactive.py

    r555 r556  
    1515customisable completion key, interactive help and more. 
    1616 
    17 Press ``?`` at any time to view contextual help. 
     17*Users can press ? at any time to view contextual help.* 
    1818""" 
    1919 
     
    4040 
    4141 
    42 __all__ = ['Interact', 'interact'] 
     42__all__ = ['Interact', 'interact', 'brief_exceptions', 'verbose_exceptions', 
     43           'debug_exceptions'] 
    4344__docformat__ = 'restructuredtext en' 
     45 
     46 
     47# Interact.loop exception modifiers 
     48def brief_exceptions(interact, context, completing, e): 
     49    """Display the string  summary for exceptions.""" 
     50    if not completing: 
     51        console.cerror(str(e)) 
     52 
     53def verbose_exceptions(interact, context, completing, e): 
     54    if not completing: 
     55        interact.dump_traceback(e) 
     56 
     57def debug_exceptions(interact, context, completing, e): 
     58    interact.dump_traceback(e) 
    4459 
    4560 
     
    137152 
    138153    # Internal methods 
    139     def _dump_traceback(self, exception): 
    140         import traceback 
    141         from StringIO import StringIO 
    142         out = StringIO() 
    143         traceback.print_exc(file=out) 
    144         print >>sys.stderr, str(exception) 
    145         print >>sys.stderr, out.getvalue() 
    146  
    147154    def _completion(self, text, state): 
    148155        line = readline.get_line_buffer()[0:readline.get_begidx()] 
     
    151158            result = self.parser.parse(line) 
    152159            if not state: 
    153                 self._completion_candidates = list(result.candidates(text)) 
     160                try: 
     161                    self._completion_candidates = list(result.candidates(text)) 
     162                except Exception, e: 
     163                    Interact.dump_traceback(e) 
     164                    self._force_redisplay() 
     165                    raise 
    154166            if self._completion_candidates: 
    155167                return self._completion_candidates.pop() 
     
    157169        except cly.Error: 
    158170            return None 
    159         except Exception, e: 
    160             self._dump_traceback(e) 
    161             self._force_redisplay() 
    162             raise 
    163171 
    164172    def _redraw_input(self): 
     
    185193            return 0 
    186194        except Exception, e: 
    187             self._dump_traceback(e) 
     195            Interact.dump_traceback(e) 
    188196            self._force_redisplay() 
    189197            return 0 
     
    242250    Interact object can be active within an application. 
    243251 
    244     Constructor arguments: 
    245  
    246     :param parser: 
    247         The parser/grammar to use for interaction. 
    248  
    249     :param application: 
    250         The application name. Used to construct the history file name and 
    251         prompt, if not provided. 
    252  
    253     :param prompt: 
    254         The prompt. 
    255  
    256     :param data: 
    257         A user-specified object to pass to the parser. The parser builds each 
    258         parse ``Context`` with this object, which in turn will deliver this 
    259         object on to terminal nodes that have set ``with_context=True``. 
    260  
    261     :param with_context: 
    262         Force current parser Context to be passed to all action nodes, unless 
    263         they explicitly set the member variable ``with_context=False``. 
    264  
    265     :param history_file: 
    266         Defaults to ``~/.<application>_history``. 
    267  
    268     :param history_length: 
    269         Lines of history to keep. 
    270  
    271     :param inhibit_exceptions: 
    272         As for :meth:`loop`. 
    273  
    274     :param with_backtrace: 
    275         As for :meth:`loop`. 
     252    Arguments: 
     253 
     254        :grammar_or_parser: The :class:`~cly.parser.Parser` or 
     255                            :class:`~cly.builder.Grammar` to use for 
     256                            interaction. 
     257        :application: The application name. Used to construct the history file 
     258                      name and prompt, if not provided. 
     259        :prompt: The prompt. 
     260        :data: A user-specified object to pass to the parser. The parser builds 
     261               each parse :class:`~cly.parser.Context` with this object, which 
     262               in turn will deliver this object on to terminal nodes that have 
     263               set ``with_context=True``. 
     264        :with_context: Force current parser :class:`~cly.parser.Context` to be 
     265                       passed to all action nodes, unless they explicitly set 
     266                       the member variable ``with_context=False``. 
     267        :history_file: Defaults to ``~/.<application>_history``. 
     268        :history_length: Lines of history to keep. 
     269        :exceptions: See :meth:`loop`. 
    276270    """ 
    277271 
     
    283277    def __init__(self, grammar_or_parser, application='cly', prompt=None, 
    284278                 data=None, history_file=None, history_length=500, 
    285                  inhibit_exceptions=False, with_backtrace=False): 
     279                 exceptions=None): 
    286280        if prompt is None: 
    287281            prompt = application + '> ' 
     
    295289        self.parser = parser 
    296290        self.data = data 
     291        self.exceptions = exceptions or (lambda *a, **kw: True) 
    297292 
    298293        self.input_driver = self.best_input_driver( 
     
    326321            return context 
    327322 
    328     def loop(self, inhibit_exceptions=False, with_backtrace=False, callback=None): 
     323    def loop(self, exceptions=None, every=None): 
    329324        """Repeatedly read and execute commands from the user. 
    330325 
    331         :param inhibit_exceptions: 
    332             Normally, ``interact_loop`` will pass exceptions back to the caller for 
    333             handling. Setting this to ``True`` will cause an error message to 
    334             be printed, but interaction will continue. 
    335  
    336         :param with_backtrace: 
    337             Whether to print a full backtrace when ``inhibit_exceptions=True``. 
    338  
    339         :param callback: 
    340             Called with the Interact object before each line is displayed. 
     326        Arguments: 
     327            :exceptions: A callback used to handle exceptions. It has the 
     328                         signature: 
     329 
     330                         exceptions(interact, context, completing, e) => bool 
     331 
     332                         context may be None and completing is True if 
     333                         exception was thrown from a completion function. 
     334 
     335                         If True is returned the exception will be re-raised. 
     336            :each: Called with the Interact object before each line is 
     337                   displayed. 
    341338        """ 
     339        exceptions = exceptions or self.exceptions 
    342340        while True: 
    343341            try: 
    344                 if callback: 
    345                     callback(self) 
     342                if every: 
     343                    every(self) 
    346344                if not self.once(): 
    347345                    break 
    348346            except Exception, e: 
    349                 if inhibit_exceptions: 
    350                     if with_backtrace: 
    351                         import traceback 
    352                         console.cerror(traceback.format_exc()) 
    353                     else: 
    354                         console.cerror('error: %s' % e) 
    355                 else: 
     347                if exceptions(self, None, False, e): 
    356348                    raise 
    357349 
     
    379371            console.cerror(indent + '^ ' + text) 
    380372 
    381     def _dump_traceback(exception): 
     373    @classmethod 
     374    def dump_traceback(cls, exception): 
    382375        import traceback 
    383376        from StringIO import StringIO 
     
    405398 
    406399 
    407 def interact(grammar_or_parser, inhibit_exceptions=False, with_backtrace=False, 
    408              *args, **kwargs): 
    409     """Start an interactive session with the given grammar or parser object.""" 
     400def interact(grammar_or_parser, exceptions=None, *args, **kwargs): 
     401    """Start an interactive session with the given grammar or parser object. 
     402 
     403    Arguments are as for :class:`Interact`. 
     404    """ 
    410405    interact = Interact(grammar_or_parser, *args, **kwargs) 
    411     interact.loop(inhibit_exceptions=inhibit_exceptions, 
    412                   with_backtrace=with_backtrace) 
     406    interact.loop(exceptions=exceptions) 
  • cly/trunk/cly/parser.py

    r552 r556  
    77# 
    88 
    9 """CLY parser classes.""" 
     9"""CLY parser classes. 
     10 
     11Constructs for parsing user input with a :class:`~cly.builder.Grammar`. 
     12""" 
    1013 
    1114 
     
    8689 
    8790class Context(object): 
    88     """Represents the parsing context of a single command. 
    89  
    90     A parse context is created automatically when input is parsed. It contains all 
    91     the information needed to parse input tokens, including the current cursor 
    92     position in the input stream, the current node in the grammar, variables 
    93     collected and a history of nodes traversed. 
    94   
     91    """Represents the parsing context for a single command. 
     92 
     93    A `Context` is created automatically when input is parsed. It contains all 
     94    the information needed to maintain state during the parse, including the 
     95    current cursor position in the input stream, the current node in the 
     96    grammar, variables collected and a history of nodes traversed. 
     97 
    9598    Basic usage is:: 
    96   
     99 
    97100      parser = Parser(grammar) 
    98101      context = parser.parse('some input text') 
    99102      print context.vars 
    100   
     103 
    101104    If the input is invalid the context will have consumed as much input as 
    102105    possible. The attributes ``parsed`` and ``remaining`` contain how much text has 
    103106    been consumed and remains, respectively. 
     107 
     108    Useful attributes: 
     109 
     110    .. attribute:: parser 
     111 
     112        :class:`Parser` this `Context` is attached to. 
     113 
     114    .. attribute:: command 
     115 
     116        Command being parsed. 
     117 
     118    .. attribute:: cursor 
     119 
     120        Position of :class:`Parser` cursor. 
     121 
     122    .. attribute:: vars 
     123 
     124        :class:`~cly.builder.Variable`\ s collected during the parse. 
    104125 
    105126    """ 
     
    126147    def _get_parsed(self): 
    127148        """Return command text that has been successfully parsed. 
     149 
    128150        >>> context = Context(None, 'one two') 
    129151        >>> context.advance(4) 
     
    181203        parsed node. 
    182204 
    183         If text is not provided, the remaining unparsed text in the current 
    184         command will be used. 
     205        Arguments: 
     206            :text: If provided, return candidates after ``text``, otherwise the 
     207                   remaining unparsed text in the current command will be used. 
    185208 
    186209        >>> from cly.builder import Grammar, Node 
     
    242265 
    243266class Parser(object): 
    244     """Parse and execute CLY grammars. 
    245  
    246     A Parser object parses user input using a CLY grammar. For each parse, the 
    247     parser creates a ``Context`` containing the state for the run, parses the 
    248     input, and executes any callbacks. 
    249  
    250     :param grammar: Grammar to parse with. 
    251     :param data: User data to attach to Context. 
     267    """Parse and execute user input against a :class:`~cly.builder.Grammar`. 
     268 
     269    For each parse, the parser creates a :class:`Context` containing the state 
     270    for the run and parses the input, and executes any callbacks. 
     271 
     272    After parsing, the returned :class:`Context` can be interrogated for 
     273    information or used to execute any :class:`~cly.builder.Action`\ s. 
     274 
     275    Arguments: 
     276        :grammar: Grammar to parse with. 
     277        :data: User data to attach to Context. 
    252278    """ 
    253279    def __init__(self, grammar, data=None): 
     
    255281        self.grammar = grammar 
    256282        self.data = data 
     283        self.labels = self._collect_labels() 
    257284 
    258285    def _set_grammar(self, grammar): 
    259         """Set grammar and update all nodes' ``parser`` attribute.""" 
     286        """Set grammar to parse with.""" 
    260287        from cly.builder import Grammar 
    261288        assert isinstance(grammar, Grammar) 
     
    263290 
    264291    def _get_grammar(self): 
    265         """The grammar associated with this parser.""" 
     292        """The :class:`~cly.builder.Grammar` associated with this parser.""" 
    266293        return self._grammar 
    267294 
    268295    grammar = property(_get_grammar, _set_grammar) 
    269296 
    270     def __iter__(self): 
    271         """Walk every node in the grammar. 
    272  
    273         >>> from cly.builder import Node, Grammar 
    274         >>> parser = Parser(Grammar(one=Node(), two=Node(three=Node()))) 
    275         >>> list(parser) 
    276         [<Grammar:/>, <Node:/two>, <Node:/two/three>, <Node:/one>] 
    277         """ 
    278         for node in self.grammar.walk(): 
    279             yield node 
    280  
    281297    def parse(self, command, data=None): 
    282         """Parse command using the current grammar. 
    283  
    284         This will return a Context object that can be used to inspect the state 
    285         of the parser. 
    286  
    287         :param command: String to parse. 
    288         :param data: The Context object has this as a data member, available to 
    289                      any ``Action`` node callbacks that have set 
    290                      ``with_context=True``. 
    291  
    292         >>> from cly.builder import Grammar, Node, Action 
     298        """Parse command using the current :class:`~cly.builder.Grammar`. 
     299 
     300        This will return a :class:`Context` object that can be used to inspect 
     301        the state of the parser. 
     302 
     303        Arguments: 
     304            :command: String to parse. 
     305            :data: Used to pass user data through to callbacks. The 
     306                   :class:`Context` object has this as an attribute , available 
     307                   to any :class:`~cly.builder.Action` node callbacks that have 
     308                   set ``with_context=True``. 
     309 
     310        >>> from cly import * 
    293311        >>> parser = Parser(Grammar(one=Node(), two=Node(three=Node( 
    294312        ...                 action=Action(callback=lambda: "foo bar"))))) 
     
    326344        """Parse and execute the given command. 
    327345 
     346        This is a convenience function that calls :meth:`~Context.execute` on 
     347        the :class:`Context` object returned by :meth:`parse`. 
     348 
     349        Arguments are the same as for :meth:`parse`. 
     350 
    328351        >>> from cly.builder import Grammar, Node, Action 
    329352        >>> parser = Parser(Grammar(one=Node(), two=Node(three=Node( 
     
    344367        return self.grammar.find(path) 
    345368 
     369    def _collect_labels(self): 
     370        """Collect labels from grammar.""" 
     371        labels = {} 
     372        for node in self.grammar.walk(): 
     373            if node.label is not None: 
     374                labels[node.label] = node 
     375        return labels 
     376 
    346377 
    347378if __name__ == '__main__': 
  • cly/trunk/cly/test.py

    r552 r556  
    5959        <grammar> 
    6060            <node name='echo'> 
    61                 <apply traversals='0'> 
    62                     <variable name='text'> 
    63                         <alias target='../../*'/> 
    64                         <action callback='echo(text)'/> 
    65                     </variable> 
    66                 </apply> 
     61                <variable traversals='0' name='text'> 
     62                    <alias target='../../*'/> 
     63                    <action callback='echo(text)'/> 
     64                </variable> 
    6765            </node> 
    6866        </grammar>