Changeset 556
- Timestamp:
- 11/07/08 00:46:59 (2 years ago)
- Location:
- cly/trunk
- Files:
-
- 1 added
- 10 modified
-
.todo (modified) (2 diffs)
-
COPYING (modified) (1 diff)
-
MANIFEST.in (added)
-
cly/__init__.py (modified) (2 diffs)
-
cly/_rlext.c (modified) (3 diffs)
-
cly/builder.py (modified) (44 diffs)
-
cly/console.py (modified) (12 diffs)
-
cly/extra.py (modified) (4 diffs)
-
cly/interactive.py (modified) (12 diffs)
-
cly/parser.py (modified) (9 diffs)
-
cly/test.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
cly/trunk/.todo
r552 r556 7 7 cmd.Cmd emulation (extended though) 8 8 </note> 9 <note priority="low" time="1178150358" >9 <note priority="low" time="1178150358" done="1215652461"> 10 10 Add XPath support: eg. grammar.find_all('//action') 11 <comment> 12 Unnecessary. 13 </comment> 11 14 </note> 12 <note priority="medium" time="1179508609" >15 <note priority="medium" time="1179508609" done="1215652470"> 13 16 console.textwrap() has issues 17 <comment> 18 Fixed a while back. 19 </comment> 14 20 </note> 15 21 <note priority="medium" time="1179509575" done="1179988905"> … … 22 28 Clarify/clean-up how with_context/with_user_context/user_context is used. 23 29 </note> 30 <note priority="medium" time="1214634243"> 31 Add grammar introspection from classes. 32 </note> 24 33 </todo> -
cly/trunk/COPYING
r413 r556 1 Copyright (C) 2006-200 7Alec Thomas1 Copyright (C) 2006-2008 Alec Thomas 2 2 All rights reserved. 3 3 -
cly/trunk/cly/__init__.py
r513 r556 8 8 9 9 """CLY is a Python module for simplifying the creation of interactive shells. 10 Kind of like the builtin ``cmd`` module on steroids. 10 Kind of like the builtin `cmd <http://docs.python.org/lib/module-cmd.html>`_ 11 module on steroids. 11 12 12 13 It has the following features: 13 14 14 - Tab completion of all commands.15 - Automatic tab completion of all commands:: 15 16 16 - Contextual help. 17 cly> s<TAB><TAB> 18 show status 17 19 18 - Extensible grammar - you can define your own commands with full dynamic 19 completion, contextual help, and so on. 20 - Contextual help:: 20 21 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. 23 39 24 40 - Flexible command grouping and ordering. … … 27 43 independently of the readline-based shell. This allows CLY's parser to 28 44 be used in other environments (think "web-based shell" ;)) 29 30 - Lots of other cool stuff.31 45 """ 32 46 -
cly/trunk/cly/_rlext.c
r528 r556 6 6 * you should have received as part of this distribution. 7 7 * 8 * 9 */8 * vim: ts=4 sts=4 sw=4 et 9 */ 10 10 #include "Python.h" 11 11 #include <readline/readline.h> … … 98 98 99 99 static 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}, 104 104 }; 105 105 … … 107 107 init_rlext(void) 108 108 { 109 Py_InitModule((char*)"_rlext", methods);109 Py_InitModule((char*)"_rlext", methods); 110 110 } -
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 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 1122 def _xml_boolean_type(value): 1123 """Converter for boolean attributes.""" 1124 return str(value).lower() in ('true', '1', 'yes') 1125 1126 1127 def _xml_group_type(value): 1128 """Parse a group="n" attribute.""" 1129 try: 1130 return int(value) 1131 except ValueError: 1132 return value 1133 1134 1091 1135 class Help(object): 1092 1136 """A callable object representing help for a Node. … … 1094 1138 Returns an iterable of pairs in the form (key, help). 1095 1139 1096 Constructor arguments1097 1098 :paramdoc: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')] 1104 1148 """ 1105 1149 def __init__(self, doc): … … 1113 1157 @staticmethod 1114 1158 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. 1116 1160 1117 1161 >>> h = Help.pair('a', 'b') … … 1158 1202 '123' 1159 1203 """ 1160 pattern = r'(?i)[A-Z_]\w +'1204 pattern = r'(?i)[A-Z_]\w*' 1161 1205 1162 1206 … … 1176 1220 1177 1221 1222 class 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 1178 1233 class String(Variable): 1179 1234 """Matches either a bare word or a quoted string. … … 1233 1288 '123.45' 1234 1289 """ 1235 pattern = r' \d+'1290 pattern = r'[-+]?\d+' 1236 1291 1237 1292 def parse(self, context, match): … … 1276 1331 1277 1332 class IP(Variable): 1278 """Match an IP address , parsing it as a tuple of four integers.1333 """Match an IP address. 1279 1334 1280 1335 >>> from cly.parser import Parser … … 1298 1353 1299 1354 1355 class 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 1300 1374 class Hostname(Variable): 1301 1375 """Match only a hostname (not an IP address). 1302 1376 1377 Arguments: 1378 :parts: The minimum number of host parts required. 1379 :suffix: Optional domain suffix to require. 1380 1303 1381 >>> from cly.parser import Parser 1304 >>> parser = Parser(Grammar(foo=Hostname( )))1382 >>> parser = Parser(Grammar(foo=Hostname(parts=2))) 1305 1383 >>> parser.parse('www').vars['foo'] 1306 'www' 1384 Traceback (most recent call last): 1385 ... 1386 KeyError: 'foo' 1307 1387 >>> parser.parse('www.example.com').vars['foo'] 1308 1388 'www.example.com' … … 1313 1393 ... 1314 1394 KeyError: 'foo' 1395 1315 1396 """ 1316 1397 pattern = r'(?i)([A-Z0-9][A-Z0-9_-]*)((\.([A-Z0-9][A-Z0-9_-]*))*\.([A-Z0-9][A-Z0-9_-]*[A-Z]))?\.?' 1317 1398 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 1318 1410 1319 1411 class Host(Variable): 1320 """Match either an IP address .1412 """Match either an IP address or a hostname. 1321 1413 1322 1414 >>> from cly.parser import Parser … … 1417 1509 return [clean(os.path.join(dir, f)) 1418 1510 for f in candidates if self.allow_dotfiles or f[0] != '.'] 1511 1512 1513 class 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 1526 class 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 1552 class 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 1575 class 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 12 12 control sequences. The syntax is a carat ``^`` followed by a single character. 13 13 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. 14 Valid 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. 38 27 39 28 """ … … 44 33 import codecs 45 34 35 36 __all__ = """ 37 cwrite getch cerror cfatal register_codec cinfo cjustify clen cprint cprintstrip 38 csplice cwarning cwraptext print_table rjustify termheight termwidth wraptoterm 39 """.split() 46 40 47 41 __docformat__ = 'restructuredtext en' … … 175 169 176 170 def cwrite(io, text): 177 io.write( decode(text)[0])171 io.write(_decode(text)[0]) 178 172 else: 179 173 _terminal_type = 'dumb' … … 187 181 188 182 cwrite.__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. 194 186 195 187 If ``sys.stdout`` is not a TTY, colour codes will be stripped. … … 197 189 198 190 199 class Codec(codecs.Codec):191 class _Codec(codecs.Codec): 200 192 def __init__(self, *args, **kwargs): 201 193 try: … … 252 244 253 245 254 class CodecStreamWriter(Codec, codecs.StreamWriter):246 class _CodecStreamWriter(_Codec, codecs.StreamWriter): 255 247 def __init__(self, stream, errors='strict'): 256 Codec.__init__(self)248 _Codec.__init__(self) 257 249 codecs.StreamWriter.__init__(self, stream, errors) 258 250 self.errors = errors … … 267 259 268 260 269 class CodecStreamReader(Codec, codecs.StreamReader):261 class _CodecStreamReader(_Codec, codecs.StreamReader): 270 262 def __init__(self, stream, errors='strict'): 271 Codec.__init__(self)263 _Codec.__init__(self) 272 264 codecs.StreamReader.__init__(self, stream, errors) 273 265 … … 286 278 287 279 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))280 def _decode(input, errors='strict'): 281 return (_Codec(errors=errors).decode(input), len(input)) 282 283 284 def _encode(input, errors='strict'): 285 return (_Codec(errors=errors).encode(input), len(input)) 294 286 295 287 … … 308 300 if encoding != 'cly': 309 301 return None 310 return ( encode, decode, CodecStreamReader,CodecStreamWriter)302 return (_encode, _decode, _CodecStreamReader, _CodecStreamWriter) 311 303 return codecs.register(inner_register) 312 304 … … 401 393 402 394 403 def cwraptext(rtext, width=termwidth(), subsequent_indent=''): 404 """Wrap multi-line text to width (defaults to termwidth())""" 395 def 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() 405 399 out = [] 406 400 for text in rtext.splitlines(): … … 438 432 439 433 440 def rjustify(text, width= termwidth()):434 def rjustify(text, width=None): 441 435 """Right justify the given text.""" 436 if width is None: 437 width = termwidth() 442 438 text = cwraptext(text, width) 443 439 out = '' … … 447 443 448 444 449 def cjustify(text, width= termwidth()):445 def cjustify(text, width=None): 450 446 """Centre the given text.""" 447 if width is None: 448 width = termwidth() 451 449 text = cwraptext(text, width) 452 450 out = '' -
cly/trunk/cly/extra.py
r547 r556 7 7 # 8 8 9 """Useful functions for CLY.""" 9 """Useful functions for CLY. 10 11 """ 10 12 11 13 … … 14 16 15 17 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)]) 18 def 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)] 19 38 20 39 … … 22 41 """Convenience function to provide candidates matching a prefix. 23 42 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`. 25 45 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 * 28 50 >>> static_candidates('foo', 'bar')(None, 'f') 29 51 ['foo '] … … 32 54 ['foo ', 'fuzz '] 33 55 """ 34 def cull_candidates(context, text):35 return filter(None, [c + ' ' for c in candidates if c.startswith(text)])36 return cull_candidates56 def _cull_candidates(context, text): 57 return cull_candidates(candidates, text) 58 return _cull_candidates 37 59 38 60 -
cly/trunk/cly/interactive.py
r555 r556 15 15 customisable completion key, interactive help and more. 16 16 17 Press ``?`` at any time to view contextual help. 17 *Users can press ? at any time to view contextual help.* 18 18 """ 19 19 … … 40 40 41 41 42 __all__ = ['Interact', 'interact'] 42 __all__ = ['Interact', 'interact', 'brief_exceptions', 'verbose_exceptions', 43 'debug_exceptions'] 43 44 __docformat__ = 'restructuredtext en' 45 46 47 # Interact.loop exception modifiers 48 def brief_exceptions(interact, context, completing, e): 49 """Display the string summary for exceptions.""" 50 if not completing: 51 console.cerror(str(e)) 52 53 def verbose_exceptions(interact, context, completing, e): 54 if not completing: 55 interact.dump_traceback(e) 56 57 def debug_exceptions(interact, context, completing, e): 58 interact.dump_traceback(e) 44 59 45 60 … … 137 152 138 153 # Internal methods 139 def _dump_traceback(self, exception):140 import traceback141 from StringIO import StringIO142 out = StringIO()143 traceback.print_exc(file=out)144 print >>sys.stderr, str(exception)145 print >>sys.stderr, out.getvalue()146 147 154 def _completion(self, text, state): 148 155 line = readline.get_line_buffer()[0:readline.get_begidx()] … … 151 158 result = self.parser.parse(line) 152 159 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 154 166 if self._completion_candidates: 155 167 return self._completion_candidates.pop() … … 157 169 except cly.Error: 158 170 return None 159 except Exception, e:160 self._dump_traceback(e)161 self._force_redisplay()162 raise163 171 164 172 def _redraw_input(self): … … 185 193 return 0 186 194 except Exception, e: 187 self._dump_traceback(e)195 Interact.dump_traceback(e) 188 196 self._force_redisplay() 189 197 return 0 … … 242 250 Interact object can be active within an application. 243 251 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`. 276 270 """ 277 271 … … 283 277 def __init__(self, grammar_or_parser, application='cly', prompt=None, 284 278 data=None, history_file=None, history_length=500, 285 inhibit_exceptions=False, with_backtrace=False):279 exceptions=None): 286 280 if prompt is None: 287 281 prompt = application + '> ' … … 295 289 self.parser = parser 296 290 self.data = data 291 self.exceptions = exceptions or (lambda *a, **kw: True) 297 292 298 293 self.input_driver = self.best_input_driver( … … 326 321 return context 327 322 328 def loop(self, inhibit_exceptions=False, with_backtrace=False, callback=None):323 def loop(self, exceptions=None, every=None): 329 324 """Repeatedly read and execute commands from the user. 330 325 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. 341 338 """ 339 exceptions = exceptions or self.exceptions 342 340 while True: 343 341 try: 344 if callback:345 callback(self)342 if every: 343 every(self) 346 344 if not self.once(): 347 345 break 348 346 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): 356 348 raise 357 349 … … 379 371 console.cerror(indent + '^ ' + text) 380 372 381 def _dump_traceback(exception): 373 @classmethod 374 def dump_traceback(cls, exception): 382 375 import traceback 383 376 from StringIO import StringIO … … 405 398 406 399 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.""" 400 def 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 """ 410 405 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 7 7 # 8 8 9 """CLY parser classes.""" 9 """CLY parser classes. 10 11 Constructs for parsing user input with a :class:`~cly.builder.Grammar`. 12 """ 10 13 11 14 … … 86 89 87 90 class Context(object): 88 """Represents the parsing context ofa single command.89 90 A parse contextis created automatically when input is parsed. It contains all91 the information needed to parse input tokens, including the current cursor92 position in the input stream, the current node in the grammar, variables93 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 95 98 Basic usage is:: 96 99 97 100 parser = Parser(grammar) 98 101 context = parser.parse('some input text') 99 102 print context.vars 100 103 101 104 If the input is invalid the context will have consumed as much input as 102 105 possible. The attributes ``parsed`` and ``remaining`` contain how much text has 103 106 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. 104 125 105 126 """ … … 126 147 def _get_parsed(self): 127 148 """Return command text that has been successfully parsed. 149 128 150 >>> context = Context(None, 'one two') 129 151 >>> context.advance(4) … … 181 203 parsed node. 182 204 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. 185 208 186 209 >>> from cly.builder import Grammar, Node … … 242 265 243 266 class 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. 252 278 """ 253 279 def __init__(self, grammar, data=None): … … 255 281 self.grammar = grammar 256 282 self.data = data 283 self.labels = self._collect_labels() 257 284 258 285 def _set_grammar(self, grammar): 259 """Set grammar and update all nodes' ``parser`` attribute."""286 """Set grammar to parse with.""" 260 287 from cly.builder import Grammar 261 288 assert isinstance(grammar, Grammar) … … 263 290 264 291 def _get_grammar(self): 265 """The grammarassociated with this parser."""292 """The :class:`~cly.builder.Grammar` associated with this parser.""" 266 293 return self._grammar 267 294 268 295 grammar = property(_get_grammar, _set_grammar) 269 296 270 def __iter__(self):271 """Walk every node in the grammar.272 273 >>> from cly.builder import Node, Grammar274 >>> 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 node280 281 297 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 * 293 311 >>> parser = Parser(Grammar(one=Node(), two=Node(three=Node( 294 312 ... action=Action(callback=lambda: "foo bar"))))) … … 326 344 """Parse and execute the given command. 327 345 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 328 351 >>> from cly.builder import Grammar, Node, Action 329 352 >>> parser = Parser(Grammar(one=Node(), two=Node(three=Node( … … 344 367 return self.grammar.find(path) 345 368 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 346 377 347 378 if __name__ == '__main__': -
cly/trunk/cly/test.py
r552 r556 59 59 <grammar> 60 60 <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> 67 65 </node> 68 66 </grammar>
