Changeset 556

Show
Ignore:
Timestamp:
07/10/08 09:46:59 (5 months 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().

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • 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