Changeset 408
- Timestamp:
- 05/04/07 10:53:50 (2 years ago)
- Files:
-
- cly/trunk/cly/__init__.py (modified) (32 diffs)
- cly/trunk/cly/interactive.py (modified) (8 diffs)
- cly/trunk/cly/test.py (added)
- cly/trunk/cly/validators.py (modified) (6 diffs)
- cly/trunk/setup.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
cly/trunk/cly/__init__.py
r407 r408 3 3 import pydoc 4 4 import string 5 import console 5 6 6 7 __all__ = """ … … 35 36 Error.__init__(self) 36 37 self.context = context 37 self. args = kwargs38 self.template_args = kwargs 38 39 if message is not None: 39 40 self.message = message … … 42 43 template = string.Template(self.message) 43 44 return template.safe_substitute(remaining=self.context.remaining, 44 **self. args)45 **self.template_args) 45 46 46 47 class UnexpectedEOL(ParseError): … … 55 56 56 57 class Node(object): 57 """ The base class for all grammar nodes.58 """The base class for all grammar nodes. 58 59 59 60 Document strings are not optional: … … 74 75 True 75 76 76 Note that nodes are automatically named using their keyword argument if a77 name is not explicitly provided.78 77 """ 79 78 pattern = None … … 90 89 """Construct a new CLY grammar node. 91 90 92 `help` can be either a help string, or a callable returning an iterable 93 of (key, help) pairs. 94 95 `pattern` is a regular expression for matching user input.""" 91 Constructor arguments are: 92 93 help: 94 A help string or a callable returning an iterable of (key, help) 95 pairs. There is a useful class called Help which can be used for 96 this purpose. 97 name=None: 98 The name of the node. If ommitted the key used by the parent Node 99 is used. 100 101 The following constructor arguments are also class variables, and as 102 such can be overridden at the class level by subclasses of Node. If you 103 find yourself using a particular pattern repeatedly, for example, it 104 might be useful to create a new subclass of Node and set the pattern 105 class variable. 106 107 pattern=None 108 The regular expression used to match user input. If not provided, 109 the node name is used. 110 separator=r'\s+|\s*$' 111 A regular expression used to match the text separating this node 112 and the next. 113 group=0 114 Nodes can be grouped together to provide visual cues. Groups are 115 ordered ascending numerically. 116 order=0 117 Within a group, nodes are normally ordered alphabetically. This can 118 be overridden by setting this to a value other than 0. 119 match_candidates=False 120 The candidates() method returns a list of words that match at the 121 current token which are used for completion, but can also be used 122 to constrain the allowed matches if match_candidates=True. Useful 123 for situations where you have a general regex pattern (eg. a 124 pattern matching files) but a known set of matches at this point 125 (eg. files in the current directory). 126 traversals=1 127 The number of times this node can match in any parse context. Alias 128 nodes allow for multiple traversal. 129 130 """ 96 131 self._children = {} 97 132 if isinstance(help, basestring): … … 133 168 """ Update or add options and child nodes. 134 169 170 Positional arguments are treated as anonymous child nodes, while 171 keyword arguments can either be named child nodes or attribute updates 172 for this node. See __init__ for more information on attributes. 173 135 174 >>> top = Node('Top', name='top') 136 175 >>> top(subnode=Node('Subnode')) … … 171 210 yield child 172 211 212 def walk(self): 213 """Perform a recursive walk of the grammar tree. 214 215 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three')))) 216 >>> for node in parser: print node 217 <Grammar:/> 218 <Node:/two> 219 <Node:/two/three> 220 <Node:/one> 221 """ 222 def walk(root): 223 yield root 224 for node in root._children.itervalues(): 225 for subnode in walk(node): 226 yield subnode 227 228 for node in walk(self): 229 yield node 230 173 231 def children(self, context, follow=False): 174 """ Iterate over child nodes, optionally following branches. """ 175 def follow_branch(child): 176 branches = list(child.follow(context)) 177 if branches: 178 for branch in branches: 179 yield branch 180 follow_branch(branch) 181 else: 182 yield child 183 232 """Iterate over child nodes, optionally follow()ing branches.""" 184 233 for child in self: 185 234 if child.valid(context): 186 235 if follow: 187 for branch in follow_branch(child):236 for branch in child.follow(context): 188 237 if branch.valid(context): 189 238 yield branch … … 192 241 193 242 def follow(self, context): 194 """ Return alternative Nodes to traverse. """ 195 return [] 243 """Return alternative Nodes to traverse. 244 245 The children() method calls this method when follow=True to expand 246 aliased nodes, although it could be used for other purposes.""" 247 return [self] 196 248 197 249 def selected(self, context, match): 198 """ This node was selected by the parser. """ 199 context.traverse(self) 250 """This node was selected by the parser. 251 252 By default, informs the context that the node has been traversed.""" 253 context.selected(self) 200 254 201 255 def next(self, context): … … 236 290 237 291 def terminal(self, context): 238 """ This node was selected as a terminal."""292 """This node was selected as a terminal.""" 239 293 raise UnexpectedEOL(context) 240 294 … … 334 388 335 389 def selected(self, context, match): 336 """ This node was selected by the parser."""390 """This node was selected by the parser.""" 337 391 alias = self.parser.find(self.alias) 338 context.traverse(alias)392 alias.selected(context, match) 339 393 340 394 def follow(self, context): … … 356 410 class Action(Node): 357 411 """ Action node, matches EOL. The `callback` arg will be used as the 358 callable. If `with_context` is true, the context object will be passed as359 the first argument.412 callable. If `with_context` is true, the user_context object provided to 413 the `Parser` will be passed as the first argument. 360 414 361 415 >>> def write_text(): … … 374 428 with_context = False 375 429 376 def __init__(self, help, callback=None, * args, **kwargs):430 def __init__(self, help, callback=None, **kwargs): 377 431 if isinstance(help, basestring): 378 432 help_string = help 379 433 help = lambda ctx: (('<eol>', help_string),) 380 Node.__init__(self, help, callback=callback, * args, **kwargs)434 Node.__init__(self, help, callback=callback, **kwargs) 381 435 382 436 def help(self, context): … … 404 458 405 459 class Validator(Node): 406 """Validate and record the users input in the `vars` member of the context. 407 The Node name is used as the variable name. If `traversals` is > 1 the 408 validator will accumulate values into a list. 460 """Validate and record the users input in the vars member of the context. 461 462 The Node name is used as the variable name. 463 464 If traversals is > 1 the validator will accumulate values into a list. 409 465 """ 410 466 … … 419 475 420 476 def selected(self, context, match): 477 """Convert the match to a value with self.parse_value(), then add 478 the result to the context "vars" member. 479 480 Raises ValidationError if the validator raises InvalidMatch. 481 482 >>> c = Context(None, 'foo bar') 483 >>> v = Validator('Test', name='var') 484 >>> v.selected(c, re.match(r'\w+', 'test')) 485 >>> c.vars['var'] 486 'test' 487 """ 421 488 try: 422 value = self. validator(match)423 except InvalidMatch, e:489 value = self.parse(match) 490 except ValidationError, e: 424 491 raise ValidationError(context, token=match.group(), 425 492 exception=unicode(e)) … … 430 497 return Node.selected(self, context, match) 431 498 432 def validator(self, match): 433 """Validate the regex match object provided and return the matched 434 value. Must throw a ValidationError if the input is invalid. Alternate 435 validators should override this method.""" 499 def parse(self, match): 500 """Parse the match and return a value. Value can be of any type, tuple, 501 list, object, etc. 502 503 Must throw a ValidationError if the input is invalid. Alternate 504 validators should override this method. 505 506 >>> v = Validator('Test') 507 >>> v.parse(re.match(r'\w+', 'test')) 508 'test' 509 """ 436 510 return match.group() 437 511 438 512 439 513 class Grammar(Node): 440 """ The root node for a grammar."""514 """The root node for a grammar.""" 441 515 def __init__(self, **options): 442 516 Node.__init__(self, '<root>', pattern='', **options) … … 447 521 448 522 class Help(object): 449 """ A callable object representing help for a Node. Must return an iterable 450 of pairs in the form (key, help). 451 452 """ 523 """ A callable object representing help for a Node. 524 525 Returns an iterable of pairs in the form (key, help).""" 453 526 def __init__(self, doc): 454 """Accepts an iterable of two element tuples in the form (key, help).""" 527 """Accepts an iterable of two element tuples in the form (key, help). 528 529 >>> h = Help([('a', 'b'), ('b', 'c')]) 530 >>> [i for i in h(None)] 531 [('a', 'b'), ('b', 'c')] 532 """ 455 533 self.doc = doc 456 534 … … 472 550 473 551 class LazyHelp(Help): 474 """ Lazily generate help from a Node. 552 """Extract help key from a node. 553 554 Used internally by Node when a string is provided as help. 475 555 476 556 If the Node does not have a custom pattern, the help will be in the form … … 482 562 483 563 def __call__(self, context): 564 """Extract help key from node. 565 566 >>> node = Node('Test', name='test') 567 >>> help = LazyHelp(node, 'Moo') 568 >>> [i for i in help(None)] 569 [('test', 'Moo')] 570 """ 484 571 if self.node.name == self.node.pattern: 485 572 yield (self.node.name, self.text) … … 490 577 491 578 class HelpParser(object): 492 """ Extract the help for children of the specified Node. """ 493 579 """Extract the help for children of the specified Node. 580 581 Help is extracted from the Node's children, following branches, and 582 returned ordered by group, order and finally help key and string. 583 """ 494 584 def __init__(self, context, node): 495 585 self.help = [] … … 531 621 <two> Two 532 622 """ 623 if not self.help: 624 return 533 625 last_group = None 534 626 max_len = max([len(h[2]) for h in self.help]) 535 627 if out.isatty(): 536 bold = '\033[1m' 537 clear = '\033[0m' 628 write = console.colour_cwrite 538 629 else: 539 bold = clear = ''630 write = console.mono_cwrite 540 631 for group, order, command, help in self.help: 541 632 if last_group is not None and last_group != group: 542 633 out.write('\n') 543 634 last_group = group 544 out.write(' %s%-*s%s %s\n' % (bold, max_len, command, clear, help))635 write(out, ' ^B%-*s^B %s\n' % (max_len, command, help)) 545 636 546 637 547 638 class Context(object): 548 """ The context of a command parse run. """ 549 def __init__(self, parser, command, user=None): 639 """Represents the parsing context of a single command. 640 641 The context contains all the information the parser needs to maintain 642 state while parsing the input command. 643 """ 644 def __init__(self, parser, command, user_context=None): 550 645 self.parser = parser 551 646 self.command = command 552 647 self.cursor = 0 553 self.user = user648 self.user_context = user_context 554 649 self.vars = {} 555 650 self._traversed = {} 556 651 self.trail = [] 557 652 558 remaining = property(lambda self: self.command[self.cursor:]) 653 def _get_remaining_input(self): 654 """Return the current remaining unparsed text in the command.""" 655 return self.command[self.cursor:] 656 remaining = property(_get_remaining_input) 559 657 560 658 def _last_node(self): 659 """Return the last node parsed.""" 561 660 if self.trail[-1][1] is None or self.trail[-1][1].group(): 562 661 return self.trail[-1][0] … … 566 665 567 666 def execute(self): 568 """ Execute the current (terminal) node. If there is still input569 remaining an exception will be thrown. """667 """Execute the current (terminal) node. If there is still input 668 remaining an exception will be thrown.""" 570 669 if self.remaining.strip(): 571 670 raise InvalidToken(self) … … 574 673 575 674 def advance(self, distance): 576 """ Advance cursor."""675 """Advance cursor.""" 577 676 self.cursor += distance 578 677 579 def candidates(self, text=''): 580 """ Return potential candidates from children of last successfully 581 parsed node. """ 678 def candidates(self, text=None): 679 """Return potential candidates from children of last successfully 680 parsed node. 681 682 If text is not provided, the remaining unparsed text in the current 683 command will be used.""" 684 if text is None: 685 text = self.remaining 582 686 for child in self.last_node.children(self, follow=True): 583 687 for candidate in child.candidates(self, text): … … 585 689 586 690 def help(self): 587 """ Return a HelpParser object describing the last successfully parsed588 node. """691 """Return a HelpParser object describing the last successfully parsed 692 node.""" 589 693 return HelpParser(self, self.last_node) 590 694 591 def traverse(self, node):592 """ Mark node as traversed in this context."""695 def selected(self, node): 696 """The given node has been selected and will be followed.""" 593 697 path = node.path() 594 698 self._traversed.setdefault(path, 0) … … 596 700 597 701 def traversed(self, node): 598 """ How many times has node been traversed in this context?"""702 """How many times has node been traversed in this context?""" 599 703 return self._traversed.get(node.path(), 0) 600 704 … … 604 708 605 709 class Parser(object): 606 """ Parse and execute CLY grammars."""710 """Parse and execute CLY grammars.""" 607 711 def __init__(self, grammar): 712 self.grammar = grammar 713 714 def _set_grammar(self, grammar): 608 715 assert isinstance(grammar, Grammar) 609 self. grammar = grammar716 self._grammar = grammar 610 717 for node in self: 611 718 node.parser = self 719 720 def _get_grammar(self): 721 """The grammar associated with this parser.""" 722 return self._grammar 723 724 grammar = property(_get_grammar, _set_grammar) 612 725 613 726 def __iter__(self): … … 621 734 <Node:/one> 622 735 """ 623 def walk(root): 624 yield root 625 for node in root._children.itervalues(): 626 for subnode in walk(node): 627 yield subnode 628 629 for node in walk(self.grammar): 736 for node in self.grammar.walk(): 630 737 yield node 631 738 632 739 def parse(self, command, user_context=None): 633 """ Parse command using the current grammar. 740 """Parse command using the current grammar. 741 742 This will return a Context object that can be used to inspect the state 743 of the parser. 744 745 If a user_context is provided it will be passed on to any `Action` 746 node callbacks that have set `with_context=True`. 634 747 635 748 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three', 636 749 ... action=Action('Do stuff', lambda: "foo bar"))))) 637 >>> context = parser.parse('two three') 750 >>> context = parser.parse('two three') 638 751 >>> context 639 752 <Context command:'two three' remaining:''> … … 644 757 """ 645 758 context = Context(self, command, user_context) 759 646 760 def parse(node, match): 647 761 context.trail.append((node, match)) … … 651 765 652 766 for subnode in node.next(context): 653 if not subnode.valid(context): 654 continue 655 submatch = subnode.match(context) 656 if submatch is not None: 657 return parse(subnode, submatch) 767 if subnode.valid(context): 768 submatch = subnode.match(context) 769 if submatch is not None: 770 return parse(subnode, submatch) 658 771 else: 659 772 return … … 664 777 665 778 def execute(self, command, user_context=None): 666 """ Parse and execute the given command.779 """Parse and execute the given command. 667 780 668 781 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three', 669 782 ... action=Action('Do stuff', lambda: "foo bar"))))) 670 >>> parser.execute('two three') 783 >>> parser.execute('two three') 671 784 'foo bar' 672 785 """ … … 674 787 675 788 def find(self, path): 676 """ Find a node by its absolute path.789 """Find a node by its absolute path. 677 790 678 791 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three')))) … … 684 797 685 798 def static_candidates(*candidates): 686 """Convenience function to provide candidates matching a prefix. Returns a 687 callable that can be used directly with `Node.candidates=`. 799 """Convenience function to provide candidates matching a prefix. 800 801 Returns a callable that can be used directly with `Node.candidates=`. 688 802 689 803 >>> static_candidates('foo', 'bar')(None, 'f') 690 804 ['foo '] 805 >>> parser = Parser(Grammar(node=Node('Test', candidates=static_candidates('foo', 'fuzz', 'bar')))) 806 >>> list(parser.parse('f').candidates()) 807 ['foo ', 'fuzz '] 691 808 """ 692 809 def cull_candidates(context, text): cly/trunk/cly/interactive.py
r407 r408 4 4 import cly 5 5 import cly.rlext 6 7 8 def print_error(*message): 9 if sys.stderr.isatty(): 10 print >>sys.stderr, '\033[31m\033[1m%s\033[0m' % ' '.join(message) 11 else: 12 print >>sys.stderr, ' '.join(message) 6 from cly.console import error as print_error 13 7 14 8 … … 20 14 `prompt`, `user_context`, `history_file`, `history_length`. 21 15 """ 22 _ _cli_inject_text = ''23 _ _completion_candidates = []24 _ _parser = None16 _cli_inject_text = '' 17 _completion_candidates = [] 18 _parser = None 25 19 26 20 def __init__(self, parser, application='cly', prompt=None, … … 35 29 parser = cly.Parser(parser) 36 30 37 Interact._ _parser = parser31 Interact._parser = parser 38 32 Interact.prompt = prompt 39 33 Interact.application = application … … 62 56 executed command. `callback` is called with the Interact object before 63 57 each line is displayed. """ 64 Interact._ _cli_inject_text = default_text58 Interact._cli_inject_text = default_text 65 59 66 60 while True: … … 76 70 77 71 try: 78 context = Interact._ _parser.parse(command)72 context = Interact._parser.parse(command) 79 73 context.execute() 80 74 except cly.ParseError, e: 81 print_error( 'error:', str(e))75 print_error(e) 82 76 return context 83 77 … … 110 104 @staticmethod 111 105 def __cli_injector(): 112 readline.insert_text(Interact._ _cli_inject_text)113 Interact._ _cli_inject_text = ''106 readline.insert_text(Interact._cli_inject_text) 107 Interact._cli_inject_text = '' 114 108 115 109 … … 119 113 ctx = None 120 114 try: 121 result = Interact._ _parser.parse(line)115 result = Interact._parser.parse(line) 122 116 if not state: 123 Interact._ _completion_candidates = list(result.candidates(text))124 if Interact._ _completion_candidates:125 return Interact._ _completion_candidates.pop()117 Interact._completion_candidates = list(result.candidates(text)) 118 if Interact._completion_candidates: 119 return Interact._completion_candidates.pop() 126 120 return None 127 121 except cly.Error: … … 137 131 try: 138 132 command = readline.get_line_buffer()[:cly.rlext.cursor()] 139 context = Interact._ _parser.parse(command)133 context = Interact._parser.parse(command) 140 134 if context.remaining.strip(): 141 135 print cly/trunk/cly/validators.py
r406 r408 27 27 'foo_bar' 28 28 """ 29 pattern = r"""\w+|"([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)'""" 30 31 def validator(self, match): 32 import compiler 33 node = compiler.parse(match.group(), 'eval') 34 return node.asList()[0].asList()[0] 29 pattern = r"""(\w+)|"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'""" 30 31 def parse(self, match): 32 return match.group(match.lastindex).decode('string_escape') 33 35 34 36 35 37 36 class URI(Validator): 38 """ Matches a URI. Result is the return value from urlparse.urlparse().37 """ Matches a URI. Result is a string. 39 38 40 39 >>> from cly import * … … 50 49 self.allow_fragments = allow_fragments 51 50 52 #def validator(self, match):51 #def parse(self, match): 53 52 #import urlparse 54 53 #return urlparse.urlparse(match.string[match.start():match.end()], self.scheme, self.allow_fragments) … … 77 76 pattern = r'\d+' 78 77 79 def validator(self, match):78 def parse(self, match): 80 79 return int(match.group()) 81 80 … … 93 92 pattern = r'[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?' 94 93 95 def validator(self, match):94 def parse(self, match): 96 95 return float(match.group()) 97 96 … … 109 108 110 109 class IP(Validator): 111 """ Match an IP address .110 """ Match an IP address, parsing it as a tuple of four integers. 112 111 113 112 >>> from cly import * 114 113 >>> parser = Parser(Grammar(foo=IP('Foo'))) 115 114 >>> parser.parse('123.34.67.89').vars['foo'] 116 ( '123', '34', '67', '89')115 (123, 34, 67, 89) 117 116 >>> parser.parse('123.34.67.256').remaining 118 117 '123.34.67.256' … … 120 119 pattern = r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' 121 120 122 def validator(self, match):123 return match.groups()121 def parse(self, match): 122 return tuple(map(int, match.groups())) 124 123 125 124 126 125 class Hostname(Validator): 127 """ Match a hostname. 126 """Match a hostname and parse it as a tuple of components. 127 128 Note: This will match hostnames consisting only of numbers including, but 129 not limited to, IP addresses. 128 130 129 131 >>> from cly import * 130 132 >>> parser = Parser(Grammar(foo=Hostname('Foo'))) 131 133 >>> parser.parse('www.example.com').vars['foo'] 132 ['www', 'example', 'com']134 ('www', 'example', 'com') 133 135 """ 134 136 pattern = r'(?i)([A-Z0-9][A-Z0-9_-]*)(?:\.([A-Z0-9][A-Z0-9_-]*))+' 135 137 136 def validator(self, match):137 return match.string[match.start():match.end()].split('.')138 def parse(self, match): 139 return tuple(match.group().split('.')) 138 140 139 141 140 142 class Host(Validator): 141 """ Match either an IP address or a hostname. 142 143 >>> from cly import * 144 >>> parser = Parser(Grammar(foo=Hostname('Foo'))) 143 """Match either an IP address or a hostname and return a tuple. 144 145 If an IP address is matched the elements of the tuple will be integers. 146 147 >>> from cly import * 148 >>> parser = Parser(Grammar(foo=Host('Foo'))) 145 149 >>> parser.parse('www.example.com').vars['foo'] 146 ['www', 'example', 'com']150 ('www', 'example', 'com') 147 151 >>> parser.parse('123.34.67.89').vars['foo'] 148 ['123', '34', '67', '89']152 (123, 34, 67, 89) 149 153 """ 150 154 151 155 pattern = r'(?i)(%s)|(%s)' % (IP.pattern, Hostname.pattern) 152 156 153 def validator(self, match): 154 return match.string[match.start():match.end()].split('.') 157 def parse(self, match): 158 components = match.string[match.start():match.end()].split('.') 159 if match.lastindex == 1: 160 return tuple(map(int, components)) 161 return tuple(components) 155 162 156 163 cly/trunk/setup.py
r342 r408 10 10 version='0.9', 11 11 packages=['cly'], 12 ext_modules=[Extension('cly.rlext', ['cly/rlext.c'], libraries = ['readline', 'curses'])]) 12 test_suite='cly.test.suite', 13 ext_modules=[Extension('cly.rlext', ['cly/rlext.c'], 14 libraries = ['readline', 'curses'])])
