Changeset 556
- Timestamp:
- 07/10/08 09:46:59 (5 months ago)
- Files:
-
- cly/trunk/cly/builder.py (modified) (44 diffs)
- cly/trunk/cly/console.py (modified) (12 diffs)
- cly/trunk/cly/extra.py (modified) (4 diffs)
- cly/trunk/cly/__init__.py (modified) (2 diffs)
- cly/trunk/cly/interactive.py (modified) (12 diffs)
- cly/trunk/cly/parser.py (modified) (9 diffs)
- cly/trunk/cly/_rlext.c (modified) (3 diffs)
- cly/trunk/cly/test.py (modified) (1 diff)
- cly/trunk/COPYING (modified) (1 diff)
- cly/trunk/MANIFEST.in (added)
- cly/trunk/.todo (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
cly/trunk/cly/builder.py
r552 r556 11 11 12 12 13 import datetime 13 14 import os 14 15 import posixpath … … 18 19 from xml.dom import minidom 19 20 from inspect import isclass, getargspec 21 from cly.extra import cull_candidates 20 22 from cly.exceptions import * 21 23 from cly.parser import Context 22 24 25 try: 26 import pytz 27 except ImportError: 28 pytz = None 29 23 30 24 31 __all__ = [ 25 'Node', 'Masquerade', ' Alias', 'Conditional', 'Apply', 'Action',32 'Node', 'Masquerade', 'Set', 'Alias', 'Group', 'If', 'Apply', 'Action', 26 33 'Variable', 'Grammar', 'XMLGrammar', 'Help', 'LazyHelp', 'Word', 'Keyword', 27 34 'String', 'URI', 'LDAPDN', 'Integer', 'Float', 'IP', 'Hostname', 'Host', 28 'EMail', 'File', 'Boolean', 35 'EMail', 'File', 'Boolean', 'KeyValue', 'AbsoluteTime', 'RelativeTime', 36 'Timezone', 29 37 ] 30 38 __docformat__ = 'restructuredtext en' … … 34 42 """The base class for all grammar nodes. 35 43 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> 48 65 49 66 The following constructor arguments are also class variables, and as … … 51 68 you find yourself using a particular pattern repeatedly. 52 69 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. 86 116 """ 87 117 pattern = None 88 118 separator = r'\s+|\s*$' 89 119 order = 0 90 group = 091 120 match_candidates = False 92 121 traversals = 1 122 label = None 93 123 94 124 def __init__(self, *anonymous, **kwargs): 95 125 self._children = {} 126 self._group = None 96 127 help = kwargs.pop('help', '') 97 128 if isinstance(help, basestring): … … 105 136 if 'separator' in kwargs: 106 137 self.separator = kwargs.pop('separator') 138 if 'candidates' in kwargs: 139 self.candidates = kwargs.pop('candidates') 140 self.match_candidates = True 107 141 if self.pattern is not None: 108 142 self._pattern = re.compile(self.pattern) … … 116 150 self(*anonymous, **kwargs) 117 151 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 118 163 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 """ 122 169 self._name = name 123 170 if isinstance(name, basestring) and self.pattern is None: … … 130 177 lambda self, name: self._set_name(name)) 131 178 179 132 180 def __call__(self, *anonymous, **options): 133 181 """Update or add options and child nodes. … … 140 188 >>> top(subnode=Node()) 141 189 <Node:/top> 142 >>> top.find(' subnode')190 >>> top.find('/subnode') 143 191 <Node:/top/subnode> 144 192 """ … … 249 297 """Iterate over child nodes, optionally follow()ing branches. 250 298 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)) 257 306 [<Alias:/five for /two/*>, <Node:/two>] 258 >>> list( tree.children(context, follow=True))307 >>> list(grammar.children(context, follow=True)) 259 308 [<Node:/two/four>, <Node:/two/three>, <Node:/two>] 260 309 """ … … 327 376 >>> grammar.depth() 328 377 0 329 >>> grammar.find(' two').depth()378 >>> grammar.find('/two').depth() 330 379 1 331 380 """ … … 337 386 338 387 >>> grammar = Grammar(one=Node(), two=Node()) 339 >>> grammar.find(' two').path()388 >>> grammar.find('/two').path() 340 389 '/two' 341 390 """ … … 352 401 default is to use the content of self.help(). 353 402 403 Arguments: 404 405 :text: Text entered so far. 406 354 407 >>> grammar = Grammar(one=Node(), two=Node()) 355 >>> list(grammar.find(' one').candidates(None, 'o'))408 >>> list(grammar.find('/one').candidates(None, 'o')) 356 409 ['one '] 357 >>> list(grammar.find(' one').candidates(None, 't'))410 >>> list(grammar.find('/one').candidates(None, 't')) 358 411 [] 359 412 """ … … 364 417 def find(self, path): 365 418 """Find a Node by path rooted at this node. 419 420 Arguments: 421 422 :path: "Path" to the node, or a label. 366 423 367 424 >>> top = Node(name='top', one=Node(), … … 374 431 InvalidNodePath: /top/one/bar 375 432 """ 433 if self.label == path: 434 return self 376 435 components = filter(None, path.split('/')) 377 436 if not components: 378 437 return self 379 438 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) 383 447 384 448 def valid(self, context): … … 400 464 will be preserved and the merging nodes children merged. 401 465 402 :param node: Node to merge into this. 466 Arguments: 467 468 :node: Node to merge into this. 403 469 """ 404 470 self.__anonymous_children += node.__anonymous_children … … 412 478 return '<%s:%s>' % (self.__class__.__name__, self.path() or '<root>') 413 479 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'} 458 503 459 504 … … 462 507 463 508 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. 465 511 466 512 Use cases: … … 494 540 """Return a sequence of all masqueraded nodes. 495 541 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) 500 545 501 546 def selected(self, context, match): … … 518 563 519 564 565 class 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 577 class 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 520 591 class Alias(Masquerade): 521 592 """An alias for another node, or set of nodes. … … 524 595 are supported. 525 596 526 Constructor arguments:527 528 :param alias:529 Relative or absolute path to the aliased node. If the alias contains530 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. 531 602 532 603 >>> from cly.parser import Parser, Context … … 537 608 >>> alias 538 609 <Alias:/four for /one> 539 >>> context = Context( None, None)610 >>> context = Context(parser, None) 540 611 >>> list(alias.follow(context)) 541 612 [<Node:/one>] … … 557 628 def masqueraded(self, context): 558 629 """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 559 641 root = self 560 642 while root.parent: 561 643 root = root.parent 562 644 try: 563 yield root.find( self.target)645 yield root.find(target) 564 646 except InvalidNodePath: 565 647 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): 569 651 if fnmatch(child.name, match): 570 652 yield child 571 653 572 654 def _get_target(self): 573 """Absolute path to the aliased node."""655 """Absolute (normalised) path to the aliased node.""" 574 656 return posixpath.normpath(posixpath.join(self.path(), self._target)) 575 657 … … 581 663 582 664 583 class Conditional(Masquerade):665 class If(Masquerade): 584 666 """A set of conditional nodes. 585 667 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. 592 676 593 677 >>> from cly.parser import Parser 594 678 >>> active = False 595 >>> parser = Parser(Grammar( Conditional(lambda c: active, one=Node())))679 >>> parser = Parser(Grammar(If(lambda c: active, one=Node()))) 596 680 >>> parser.parse('one') 597 681 <Context command:'one' remaining:'one'> … … 601 685 """ 602 686 603 def __init__(self, condition, *args, **kwargs):604 kwargs[' condition'] = condition605 super( Conditional, self).__init__(*args, **kwargs)687 def __init__(self, test, *args, **kwargs): 688 kwargs['test'] = test 689 super(If, self).__init__(*args, **kwargs) 606 690 607 691 def masqueraded(self, context): 608 if not self. condition(context):692 if not self.test(context): 609 693 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 700 class 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 611 735 612 736 613 737 class 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. 620 750 621 751 >>> from cly.parser import Parser, Context … … 625 755 >>> parser = Parser(grammar) 626 756 >>> context = Context(parser, 'foo bar') 627 >>> node = grammar.find(' action')757 >>> node = grammar.find('/action') 628 758 >>> node.help(None) 629 759 (('<eol>', ''),) … … 632 762 """ 633 763 pattern = '$' 634 group = 9999635 764 with_context = None 636 765 637 766 def __init__(self, callback, *anonymous, **kwargs): 638 767 help = kwargs.pop('help', '') 768 kwargs.setdefault('group', 9999) 639 769 if isinstance(help, basestring): 640 770 help_string = help … … 655 785 # and if we do they get excluded from help. 656 786 pass 787 788 @classmethod 789 def xml_attribute_aliases(cls): 790 return {'exec': 'callback'} 657 791 658 792 … … 733 867 class Grammar(Node): 734 868 """The root node for a grammar.""" 735 pattern = '' 869 pattern = '^' 870 736 871 def __init__(self, *anonymous, **kwargs): 737 872 Node.__init__(self, help='<root>', *anonymous, **kwargs) … … 740 875 """Null-op for empty lines.""" 741 876 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_evaluator773 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 evaluate785 ))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 return795 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, v803 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 pass815 else:816 v = arg_types.get(k, str)(v)817 attributes[k] = v818 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 raise825 if name:826 path = parent.path() + '/' + name827 parent(**{str(name): node})828 else:829 parent(node)830 else:831 node = parent832 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 grammar842 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 to851 locals.852 """853 # Extract positional arguments from function object854 positional_args = positional_args or []855 def xml_attr_evaluator(*args, **kwargs):856 locals = dict(kwargs)857 858 # Convert positional args into locals859 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.' % attr864 )865 locals.update(zip(positional_args[:len(args)], args))866 867 if 'context' in locals:868 context = locals.pop('context')869 data = context.data870 if isinstance(data, dict):871 locals.update(data)872 vars = context.vars873 else:874 data = {}875 vars = {}876 locals.update(vars)877 locals['v'] = vars878 locals['a'] = args879 locals['kw'] = kwargs880 locals['d'] = data881 locals['defined'] = lambda v: v in locals882 try:883 return eval(attr, locals)884 except Exception, e:885 e.args = e.args + ('(while parsing "%s")' % attr,)886 raise887 return xml_attr_evaluator888 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 902 877 903 878 class XMLGrammar(Grammar): 904 879 """A Grammar that builds its structure from an XML file. 905 880 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. 909 890 910 891 eg. 911 892 912 .. code-block:: python893 .. code-block:: xml 913 894 914 895 <word name="abc" valid="'var' in v" pattern=r"[abc]+"/> … … 923 904 ) 924 905 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. 941 917 942 918 v in particular is useful for passing all collected arguments to 943 919 functions. 944 920 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 949 934 950 935 eg. … … 977 962 """ 978 963 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 988 964 node_aliases = { 989 965 'var': 'variable', … … 992 968 } 993 969 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): 1000 971 super(XMLGrammar, self).__init__() 1001 972 … … 1008 979 self.node_map.update([(k, self.node_map[v]) 1009 980 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 {})1013 981 1014 982 if dom.firstChild.localName != 'grammar': … … 1055 1023 1056 1024 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 1065 1037 in xnode.attributes.items()]) 1066 1038 1067 1039 for k, v in attributes.items(): 1068 1040 # 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) 1071 1043 # Is the destination Node attribute callable? Do a lazy eval. 1072 1044 elif callable(getattr(cls, k, None)): … … 1089 1061 1090 1062 1063 def _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
