Changeset 434
- Timestamp:
- 05/24/07 04:02:27 (2 years ago)
- Files:
-
- cly/trunk/cly/all.py (deleted)
- cly/trunk/cly/builder.py (copied) (copied from cly/trunk/cly/__init__.py) (8 diffs)
- cly/trunk/cly/console.py (modified) (1 diff)
- cly/trunk/cly/exceptions.py (added)
- cly/trunk/cly/extra.py (modified) (2 diffs)
- cly/trunk/cly/__init__.py (modified) (3 diffs)
- cly/trunk/cly/interactive.py (modified) (5 diffs)
- cly/trunk/cly/parser.py (copied) (copied from cly/trunk/cly/__init__.py) (13 diffs)
- cly/trunk/cly/test.py (modified) (1 diff)
- cly/trunk/cly/types.py (deleted)
- cly/trunk/setup.py (modified) (1 diff)
- cly/trunk/.todo (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
cly/trunk/cly/builder.py
r430 r434 7 7 # 8 8 9 """CLY is a Python module for simplifying the creation of Python interactive 10 shells. Kind of like the builtin `cmd` module on steroids. 11 12 It has the following features: 13 14 - Tab completion of all commands. 15 16 - Contextual help. 17 18 - Extensible grammar - you can define your own commands with full dynamic 19 completion, contextual help, and so on. 20 21 - Simple. Grammars are constructed from objects using a convenient 22 ''function-like'' syntax. 23 24 - Flexible command grouping and ordering. 25 26 - Grammar parser, including completion and help enumeration, can be used 27 independently of the readline-based shell. This allows CLY's parser to 28 be used in other environments (think "web-based shell" ;)) 29 30 - Lots of other cool stuff. 31 """ 9 10 """Classes for constructing CLY grammars.""" 11 32 12 33 13 import re 14 import os 34 15 import posixpath 35 import string 36 37 __author__ = 'Alec Thomas <alec@swapoff.org>' 38 __version__ = '0.9' 39 40 __all__ = """ 41 Error InvalidNodePath ParseError UnexpectedEOL InvalidToken ValidationError 42 43 Node Alias Group Action Variable Grammar 44 45 Help LazyHelp HelpParser Context Parser 46 47 quickstart 48 """.split() 49 50 51 class Error(Exception): 52 """The base of all CLY exceptions.""" 53 54 class InvalidHelp(Error): 55 """Thrown when the help provided to a Node is of an invalid type.""" 56 57 class InvalidNodePath(Error): 58 """Thrown when an attempt is made to reference an invalid node path. This 59 can occur when an Alias target is invalid.""" 60 61 class InvalidAnonymousNode(Error): 62 """When Node is used as a callable to add child nodes, positional arguments 63 are treated as anonymous child nodes. This exception is thrown if an object 64 that is not a Node is passed.""" 65 66 class ParseError(Error): 67 """Report a parse error. Output is formatted using string templates, 68 where variables are passed as arguments to the constructor. 69 70 >>> print ParseError(Context(None, 'foo bar'), "remaining=$remaining, time=$time", time=123) 71 remaining=foo bar, time=123 72 """ 73 message = 'parse error' 74 75 def __init__(self, context, message=None, **kwargs): 76 Error.__init__(self) 77 self.context = context 78 self.template_args = kwargs 79 if message is not None: 80 self.message = message 81 82 def __str__(self): 83 template = string.Template(self.message) 84 return template.safe_substitute(remaining=self.context.remaining, 85 **self.template_args) 86 87 class UnexpectedEOL(ParseError): 88 """Raised when all input is consumed and no terminal (``Action``) node is 89 reached.""" 90 message = 'more input required' 91 92 class InvalidToken(ParseError): 93 """Raised when a token is reached that is invalid at the current grammar 94 branch.""" 95 message = "invalid token '$remaining'" 96 97 class ValidationError(ParseError): 98 """Raised when a variable fails to parse. In practise this is rare, as the 99 regex for a variable is usually sufficient to rule it out of selection 100 before parsing occurs.""" 101 message = "validation of '$token' failed; $exception" 16 17 18 __all__ = ['Node', 'Alias', 'Group', 'Action', 'Variable', 'Grammar', 'Help', 19 'LazyHelp', 'Word', 'String', 'URI', 'LDAPDN', 'Integer', 'Float', 20 'IP', 'Hostname', 'Host', 'EMail', 'File'] 21 __docformat__ = 'restructuredtext en' 22 23 24 from cly.exceptions import * 102 25 103 26 … … 314 237 def children(self, context, follow=False): 315 238 """Iterate over child nodes, optionally follow()ing branches. 239 >>> from cly.parser import Context 316 240 >>> tree = Node('One')(two=Node('Two', three=Node('Three'), 317 241 ... four=Node('Four')), five=Alias('../two/*')) … … 454 378 455 379 456 def annotate(*args, **kwargs):457 def apply_annotation(function):458 function.cly_args = args459 function.cly_kwargs = kwargs460 return function461 return apply_annotation462 463 464 380 class Group(Node): 465 381 """Group all children together at this location. 466 382 467 383 >>> import sys 384 >>> from cly.parser import HelpParser, Context 468 385 >>> group = Group(1) 469 386 >>> top = Node('Top', group(name='top', one=Node('One'), two=Node('Two')), three=Node('Three')) … … 505 422 or ``?``) all matching nodes are aliased. 506 423 424 >>> from cly.parser import Parser, Context 507 425 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', 508 426 ... three=Node('Three')), four=Alias('../one'), five=Node('Five', six=Alias('../../*')))) … … 540 458 def selected(self, context, match): 541 459 """This node was selected by the parser.""" 542 raise E xception('Alias nodes should never be selected')460 raise Error('Alias nodes should never be selected') 543 461 544 462 def follow(self, context): … … 573 491 the ``Parser`` will be passed as the first argument. 574 492 493 >>> from cly.parser import Parser, Context 575 494 >>> def write_text(): 576 495 ... print "some text" … … 664 583 Raises ValidationError if the variable raises InvalidMatch. 665 584 585 >>> from cly.parser import Context 666 586 >>> c = Context(None, 'foo bar') 667 587 >>> v = Variable('Test', name='var') … … 763 683 764 684 765 766 class HelpParser(object): 767 """Extract the help for children of the specified Node. 768 769 Help is extracted from the Node's children, following branches, and 770 returned ordered by group, order and finally help key and string. 771 """ 772 def __init__(self, context, node): 773 self.help = [] 774 self.node = node 775 776 def add_help(node): 777 node_help = sorted(node.help(context)) 778 for help in node_help: 779 self.help.append((node.group, node.order, help[0], help[1])) 780 781 for child in node.children(context, follow=True): 782 if child.visible(context): 783 add_help(child) 784 785 self.help.sort() 786 787 def __iter__(self): 788 """Iterate over each (key, help) help pair. 789 790 >>> context = Context(None, None) 791 >>> help = HelpParser(context, Grammar(one=Node('One'), 792 ... two=Node(Help.pair('<two>', 'Two'), group=2))) 793 >>> list(help) 794 [(0, 'one', 'One'), (2, '<two>', 'Two')] 795 """ 796 797 for help in self.help: 798 yield (help[0],) + help[2:] 799 800 def format(self, out): 801 """Format help into a human readable form. 802 803 >>> import sys 804 >>> context = Context(None, None) 805 >>> help = HelpParser(context, Grammar(one=Node('One'), 806 ... two=Node(Help.pair('<two>', 'Two'), group=2))) 807 >>> help.format(sys.stdout) 808 one One 809 <BLANKLINE> 810 <two> Two 811 """ 812 import cly.console as console 813 814 if not self.help: 815 return 816 last_group = None 817 max_len = max([len(h[2]) for h in self.help]) 818 if out.isatty(): 819 write = console.colour_cwrite 685 class Word(Variable): 686 """Matches a Pythonesque variable name. 687 688 >>> from cly.parser import Parser 689 >>> parser = Parser(Grammar(foo=Word('Foo'))) 690 >>> parser.parse('a123').vars['foo'] 691 'a123' 692 >>> parser.parse('123').remaining 693 '123' 694 """ 695 pattern = r'(?i)[A-Z_]\w+' 696 697 698 class String(Variable): 699 """Matches either a bare word or a quoted string. 700 701 >>> from cly.parser import Parser 702 >>> parser = Parser(Grammar(foo=String('Foo'))) 703 >>> parser.parse('"foo bar"').vars['foo'] 704 'foo bar' 705 >>> parser.parse('foo_bar').vars['foo'] 706 'foo_bar' 707 """ 708 pattern = r"""(\w+)|"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'""" 709 710 def parse(self, context, match): 711 return match.group(match.lastindex).decode('string_escape') 712 713 714 715 class URI(Variable): 716 """Matches a URI. Result is a string. 717 718 >>> from cly.parser import Parser 719 >>> parser = Parser(Grammar(foo=URI('Foo'))) 720 >>> parser.parse('http://www.example.com/test/;test?a=10&b=10#fragment').vars['foo'] 721 'http://www.example.com/test/;test?a=10&b=10#fragment' 722 """ 723 pattern = r"""(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?/{0,2}[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?""" 724 725 def __init__(self, doc, scheme='', allow_fragments=1, *argl, **argd): 726 Variable.__init__(self, doc, *argl, **argd) 727 self.scheme = scheme 728 self.allow_fragments = allow_fragments 729 730 #def parse(self, context, match): 731 #import urlparse 732 #return urlparse.urlparse(match.string[match.start():match.end()], self.scheme, self.allow_fragments) 733 734 735 class LDAPDN(Variable): 736 """Matches an LDAP DN. 737 738 >>> from cly.parser import Parser 739 >>> parser = Parser(Grammar(foo=LDAPDN('Foo'))) 740 >>> parser.parse('cn=Manager,dc=example,dc=com').vars['foo'] 741 'cn=Manager,dc=example,dc=com' 742 """ 743 pattern = r'(\w+=\w+)(?:,(\w+=\w+))*' 744 745 746 class Integer(Variable): 747 """Matches an integer. 748 749 >>> from cly.parser import Parser 750 >>> parser = Parser(Grammar(foo=Integer('Foo'))) 751 >>> parser.parse('12345').vars['foo'] 752 12345 753 >>> parser.parse('123.45').remaining 754 '123.45' 755 """ 756 pattern = r'\d+' 757 758 def parse(self, context, match): 759 return int(match.group()) 760 761 762 class Float(Variable): 763 """Matches a floating point number. 764 765 >>> from cly.parser import Parser 766 >>> parser = Parser(Grammar(foo=Float('Foo'))) 767 >>> parser.parse('12345.34').vars['foo'] 768 12345.34 769 >>> parser.parse('123.45e10').vars['foo'] 770 1234500000000.0 771 """ 772 pattern = r'[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?' 773 774 def parse(self, context, match): 775 return float(match.group()) 776 777 778 class IP(Variable): 779 """Match an IP address, parsing it as a tuple of four integers. 780 781 >>> from cly.parser import Parser 782 >>> parser = Parser(Grammar(foo=IP('Foo'))) 783 >>> parser.parse('123.34.67.89').vars['foo'] 784 (123, 34, 67, 89) 785 >>> parser.parse('123.34.67.256').remaining 786 '123.34.67.256' 787 """ 788 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]?)' 789 790 def parse(self, context, match): 791 return tuple(map(int, match.groups())) 792 793 794 class Hostname(Variable): 795 """Match a hostname and parse it as a tuple of components. 796 797 Note: This will match hostnames consisting only of numbers, including but 798 not limited to IP addresses. 799 800 >>> from cly.parser import Parser 801 >>> parser = Parser(Grammar(foo=Hostname('Foo'))) 802 >>> parser.parse('www.example.com').vars['foo'] 803 ('www', 'example', 'com') 804 """ 805 pattern = r'(?i)([A-Z0-9][A-Z0-9_-]*)(?:\.([A-Z0-9][A-Z0-9_-]*))*' 806 807 def parse(self, context, match): 808 return tuple(match.group().split('.')) 809 810 811 class Host(Variable): 812 """Match either an IP address or a hostname and return a tuple. 813 814 If an IP address is matched, the elements of the tuple will be integers. 815 816 >>> from cly.parser import Parser 817 >>> parser = Parser(Grammar(foo=Host('Foo'))) 818 >>> parser.parse('www.example.com').vars['foo'] 819 ('www', 'example', 'com') 820 >>> parser.parse('123.34.67.89').vars['foo'] 821 (123, 34, 67, 89) 822 """ 823 824 pattern = r'(?i)(%s)|(%s)' % (IP.pattern, Hostname.pattern) 825 826 def parse(self, context, match): 827 components = match.string[match.start():match.end()].split('.') 828 if match.lastindex == 1: 829 return tuple(map(int, components)) 830 return tuple(components) 831 832 833 class EMail(Variable): 834 """Match an E-Mail address. 835 836 >>> from cly.parser import Parser 837 >>> parser = Parser(Grammar(foo=EMail('Foo'))) 838 >>> parser.parse('foo@bar.com').vars['foo'] 839 'foo@bar.com' 840 """ 841 pattern = r'(?i)[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}' 842 843 844 class File(Variable): 845 """Match and provide completion candidates for local files. 846 847 >>> from cly.parser import Parser 848 >>> parser = Parser(Grammar(foo=File('Foo', allow_directories=True))) 849 >>> parser.parse('.').vars['foo'] 850 '.' 851 """ 852 pattern = r'\S+' 853 includes = ['*'] 854 excludes = [] 855 allow_dotfiles = False 856 allow_directories = False 857 858 def match(self, context): 859 match = Variable.match(self, context) 860 if match and self.match_file(match.group(), self.allow_directories): 861 return match 862 863 def match_file(self, file, match_directories=True): 864 from fnmatch import fnmatch 865 file = os.path.expanduser(file) 866 if match_directories and os.path.isdir(file): 867 return True 868 if not self.allow_dotfiles and os.path.basename(file).startswith('.'): 869 return False 870 for exclude in self.excludes: 871 if fnmatch(file, exclude): 872 return False 873 for include in self.includes: 874 if fnmatch(file, include): 875 return True 876 return False 877 878 def candidates(self, context, text): 879 """Return list of valid file candidates.""" 880 881 if text.startswith('~'): 882 if '/' in text: 883 short_home = text[:text.index('/')] 884 else: 885 short_home = text 886 expanded_home = os.path.expanduser(short_home) 887 text = os.path.expanduser(text) 820 888 else: 821 write = console.mono_cwrite 822 for group, order, command, help in self.help: 823 if last_group is not None and last_group != group: 824 out.write('\n') 825 last_group = group 826 write(out, ' ^B%-*s^B %s\n' % (max_len, command, help)) 827 828 829 class Context(object): 830 """Represents the parsing context of a single command. 831 832 The context contains all the information the parser needs to maintain 833 state while parsing the input command. 834 """ 835 def __init__(self, parser, command, user_context=None): 836 self.parser = parser 837 self.command = command 838 self.cursor = 0 839 self.user_context = user_context 840 self.vars = {} 841 self._traversed = {} 842 self.trail = [] 843 844 def _get_remaining_input(self): 845 """Return the current remaining unparsed text in the command.""" 846 return self.command[self.cursor:] 847 remaining = property(_get_remaining_input) 848 849 def _get_parsed(self): 850 """Return command text that has been successfully parsed.""" 851 return self.command[:self.cursor] 852 parsed = property(_get_parsed) 853 854 def _last_node(self): 855 """Return the last node parsed.""" 856 if self.trail[-1][1] is None or self.trail[-1][1].group(): 857 return self.trail[-1][0] 858 else: 859 return self.trail[-2][0] 860 last_node = property(_last_node) 861 862 def execute(self): 863 """Execute the current (terminal) node. If there is still input 864 remaining an exception will be thrown.""" 865 if self.remaining.strip(): 866 raise InvalidToken(self) 867 node = self.trail[-1][0] 868 return node.terminal(self) 869 870 def advance(self, distance): 871 """Advance cursor.""" 872 self.cursor += distance 873 874 def candidates(self, text=None): 875 """Return potential candidates from children of last successfully 876 parsed node. 877 878 If text is not provided, the remaining unparsed text in the current 879 command will be used.""" 880 if text is None: 881 text = self.remaining 882 for child in self.last_node.children(self, follow=True): 883 for candidate in child.candidates(self, text): 884 yield candidate 885 886 def help(self): 887 """Return a HelpParser object describing the last successfully parsed 888 node.""" 889 return HelpParser(self, self.last_node) 890 891 def selected(self, node): 892 """The given node has been selected and will be followed.""" 893 path = node.path() 894 self._traversed.setdefault(path, 0) 895 self._traversed[path] += 1 896 897 def traversed(self, node): 898 """How many times has node been traversed in this context?""" 899 return self._traversed.get(node.path(), 0) 900 901 def __repr__(self): 902 return "<Context command:'%s' remaining:'%s'>" % (self.command, self.remaining) 903 904 905 class Parser(object): 906 """Parse and execute CLY grammars.""" 907 def __init__(self, grammar, with_context=False): 908 self.grammar = grammar 909 self.with_context = with_context 910 911 def _set_grammar(self, grammar): 912 assert isinstance(grammar, Grammar) 913 self._grammar = grammar 914 for node in self: 915 node.parser = self 916 917 def _get_grammar(self): 918 """The grammar associated with this parser.""" 919 return self._grammar 920 921 grammar = property(_get_grammar, _set_grammar) 922 923 def __iter__(self): 924 """Walk every node in the grammar. 925 926 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three')))) 927 >>> list(parser) 928 [<Grammar:/>, <Node:/two>, <Node:/two/three>, <Node:/one>] 929 """ 930 for node in self.grammar.walk(): 931 yield node 932 933 def parse(self, command, user_context=None): 934 """Parse command using the current grammar. 935 936 This will return a Context object that can be used to inspect the state 937 of the parser. 938 939 If a user_context is provided it will be passed on to any ``Action`` 940 node callbacks that have set ``with_context=True``. 941 942 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three', 943 ... action=Action('Do stuff', lambda: "foo bar"))))) 944 >>> context = parser.parse('two three') 945 >>> context 946 <Context command:'two three' remaining:''> 947 >>> context.execute() 948 'foo bar' 949 >>> parser.parse('two four') 950 <Context command:'two four' remaining:'four'> 951 """ 952 context = Context(self, command, user_context) 953 954 def parse(node, match): 955 context.trail.append((node, match)) 956 if match is not None: 957 node.advance(context) 958 node.selected(context, match) 959 960 for subnode in node.next(context): 961 if subnode.valid(context): 962 submatch = subnode.match(context) 963 if submatch is not None: 964 return parse(subnode, submatch) 889 short_home = None 890 891 text = os.path.expanduser(text) 892 dir = os.path.dirname(text) or os.path.curdir 893 file = os.path.basename(text) 894 cwd = os.path.curdir + os.path.sep 895 896 def clean(file): 897 if file.startswith(cwd): 898 return file[len(cwd):] 899 if short_home and file.startswith(expanded_home): 900 return short_home + file[len(expanded_home):] 901 return file 902 903 def get_candidates(dir, file): 904 return [f for f in os.listdir(dir) if f.startswith(file) 905 and self.match_file(os.path.join(dir, f))] 906 907 candidates = get_candidates(dir, file) 908 if len(candidates) == 1: 909 if os.path.isdir(os.path.join(dir, candidates[0])): 910 dir = os.path.join(dir, candidates[0] + '/') 911 return [dir] 912 file = '' 913 candidates = get_candidates(dir, file) 965 914 else: 966 return 967 raise InvalidToken(context) 968 969 parse(self.grammar, None) 970 return context 971 972 def execute(self, command, user_context=None): 973 """Parse and execute the given command. 974 975 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three', 976 ... action=Action('Do stuff', lambda: "foo bar"))))) 977 >>> parser.execute('two three') 978 'foo bar' 979 """ 980 return self.parse(command, user_context).execute() 981 982 def find(self, path): 983 """Find a node by its absolute path. 984 985 >>> parser = Parser(Grammar(one=Node('One'), two=Node('Two', three=Node('Three')))) 986 >>> parser.find('/two/three') 987 <Node:/two/three> 988 """ 989 return self.grammar.find(path) 990 991 992 def quickstart(grammar_or_callables, *interact_args, **interact_kwargs): 993 """Start an interactive session from a grammar, or from inspecting a set of 994 callables.""" 995 from cly.extra import quickstart 996 quickstart(grammar_or_callables, *interact_args, **interact_kwargs) 915 return [clean(os.path.join(dir, candidates[0] + ' '))] 916 return [clean(os.path.join(dir, f)) 917 for f in candidates if self.allow_dotfiles or f[0] != '.'] 997 918 998 919 cly/trunk/cly/console.py
r433 r434 43 43 import os 44 44 import codecs 45 46 47 __docformat__ = 'restructuredtext en' 45 48 46 49 cly/trunk/cly/extra.py
r430 r434 9 9 """Useful functions for use in conjunction with CLY.""" 10 10 11 from cly import Parser, Grammar, Node, Action, Variable, Alias12 from cly.interactive import Interact13 11 14 __all__ = ['quickstart', 'integrate', 'static_candidates'] 15 16 17 def _integrate_function(node, components, doc): 18 if not components: 19 return node 20 key = components.pop(0) 21 try: 22 next = node[key] 23 except KeyError: 24 next = Node(doc) 25 node[key] = next 26 return _integrate_function(next, components, doc) 27 28 29 def _integrate_positional_args(node, args): 30 if not args: 31 return node 32 arg = args.pop(0) 33 node[arg] = Variable(arg.title(), pattern=r'[^\s]+') 34 return _integrate_positional_args(node[arg], args) 35 36 37 def _integrate_keywords_args(node, kwargs): 38 if len(kwargs) > 1: 39 for arg in kwargs: 40 node[arg] = Node(arg.title()) 41 node[arg][arg] = Variable(arg.title())(Alias('../../../*')) 42 else: 43 node[arg] = Variable(arg.title())(Alias('../../*')) 44 45 46 def integrate(root, functions): 47 """Use heuristics to integrate functions into a node. 48 49 Each underscore separated part of the function name is converted into a 50 Node, each argument without a default is converted into a required 51 Variable, and each argument with a default is converted into an optional 52 Variable. 53 54 >>> from cly import Grammar 55 >>> def foo_bar(one, two, three="Baz", four="Waz"): 56 ... print one, two, three, four 57 >>> grammar = Grammar() 58 >>> integrate(grammar, [foo_bar]) 59 >>> list(grammar.walk()) # doctest: +NORMALIZE_WHITESPACE 60 [<Grammar:/>, <Node:/foo>, <Node:/foo/bar>, <Variable:/foo/bar/one>, 61 <Variable:/foo/bar/one/two>, <Action:/foo/bar/one/two/action>, 62 <Node:/foo/bar/one/two/four>, <Variable:/foo/bar/one/two/four/four>, 63 <Alias:/foo/bar/one/two/four/four/__anonymous_0 for /foo/bar/one/two/*>, 64 <Node:/foo/bar/one/two/three>, <Variable:/foo/bar/one/two/three/three>, 65 <Alias:/foo/bar/one/two/three/three/__anonymous_0 for /foo/bar/one/two/*>] 66 """ 67 import inspect 68 69 for function in functions: 70 try: 71 name = function.func_name 72 except AttributeError: 73 name = function.__class__.__name__ 74 75 args, varargs, varkw, defaults = inspect.getargspec(function) 76 doc = inspect.getdoc(function) or '' 77 78 node = _integrate_function(root, name.split('_'), doc) 79 80 if defaults: 81 positional_args = args[:-len(defaults)] 82 else: 83 positional_args = args[:] 84 node = _integrate_positional_args(node, positional_args) 85 86 node(action=Action(doc, function)) 87 88 if defaults: 89 keyword_args = args[-len(defaults):] 90 _integrate_keywords_args(node, keyword_args) 91 92 93 def hint(*args, **kwargs): 94 """Annotate a function with CLY function integration hints. 95 96 XXX Not functional yet. XXX""" 97 def apply_hint(function): 98 function.cly_args = args 99 function.cly_kwargs = kwargs 100 return function 101 return apply_hint 102 103 104 def quickstart(grammar_or_callables, *interact_args, **interact_kwargs): 105 """Start an interactive session from a grammar, or from inspecting a set of 106 callables.""" 107 if isinstance(grammar_or_callables, (Grammar, Parser)): 108 interact = Interact(grammar_or_callables, *interact_args, 109 **interact_kwargs) 110 else: 111 grammar = Grammar() 112 integrate(grammar, grammar_or_callables) 113 interact = Interact(grammar, *interact_args, **interact_kwargs) 114 interact.interact_loop() 12 __all__ = ['static_candidates'] 13 __docformat__ = 'restructuredtext en' 115 14 116 15 … … 120 19 Returns a callable that can be used directly with ``Node.candidates=``. 121 20 122 >>> from cly import Parser, Grammar, Node 21 >>> from cly.parser import Parser 22 >>> from cly.builder import Grammar, Node 123 23 >>> static_candidates('foo', 'bar')(None, 'f') 124 24 ['foo '] cly/trunk/cly/__init__.py
r430 r434 7 7 # 8 8 9 """CLY is a Python module for simplifying the creation of Python interactive10 shells. Kind of like the builtin `cmd` module on steroids.9 """CLY is a Python module for simplifying the creation of interactive shells. 10 Kind of like the builtin ``cmd`` module on steroids. 11 11 12 12 It has the following features: … … 20 20 21 21 - Simple. Grammars are constructed from objects using a convenient 22 ''function- like'' syntax.22 ''function-call'' syntax. 23 23 24 24 - Flexible command grouping and ordering. … … 31 31 """ 32 32 33 import re34 import posixpath35 import string36 33 34 __docformat__ = 'restructuredtext en' 37 35 __author__ = 'Alec Thomas <alec@swapoff.org>' 38 __version__ = '0.9' 39 40 __all__ = """ 41 Error InvalidNodePath ParseError UnexpectedEOL InvalidToken ValidationError 42 43 Node Alias Group Action Variable Grammar 44 45 Help LazyHelp HelpParser Context Parser 46 47 quickstart 48 """.split() 36 try: 37 __version__ = __import__('pkg_resources').get_distribution('cly').version 38 except ImportError: 39 pass 49 40 50 41 51 class Error(Exception): 52 """The base of all CLY exceptions.""" 53 54 class InvalidHelp(Error): 55 """Thrown when the help provided to a Node is of an invalid type.""" 56 57 class InvalidNodePath(Error): 58 """Thrown when an attempt is made to reference an invalid node path. This 59 can occur when an Alias target is invalid.""" 60 61 class InvalidAnonymousNode(Error): 62 """When Node is used as a callable to add child nodes, positional arguments 63 are treated as anonymous child nodes. This exception is thrown if an object 64 that is not a Node is passed.""" 65 66 class ParseError(Error): 67 """Report a parse error. Output is formatted using string templates, 68 where variables are passed as arguments to the constructor. 69 70 >>> print ParseError(Context(None, 'foo bar'), "remaining=$remaining, time=$time", time=123) 71 remaining=foo bar, time=123 72 """ 73 message = 'parse error' 74 75 def __init__(self, context, message=None, **kwargs): 76 Error.__init__(self) 77 self.context = context 78 self.template_args = kwargs 79 if message is not None: 80 self.message = message 81 82 def __str__(self): 83 template = string.Template(self.message) 84 return template.safe_substitute(remaining=self.context.remaining, 85 **self.template_args) 86 87 class UnexpectedEOL(ParseError): 88 """Raised when all input is consumed and no terminal (``Action``) node is 89 reached.""" 90 message = 'more input required' 91 92 class InvalidToken(ParseError): 93 """Raised when a token is reached that is invalid at the current grammar 94 branch.""" 95 message = "invalid token '$remaining'" 96 97 class ValidationError(ParseError): 98 """Raised when a variable fails to parse. In practise this is rare, as the 99 regex for a variable is usually sufficient to rule it out of selection 100 before parsing occurs.""" 101 message = "validation of '$token' failed; $exception" 102 103 104 class Node(object): 105 """The base class for all grammar nodes. 106 107 Constructor arguments are: 108 109 ``help``: string or callable returning a list of (key, help) tuples 110 A help string or a callable returning an iterable of (key, help) 111 pairs. There is a useful class called Help which can be used for 112 this purpose. 113 114 Help strings are required. 115 116 >>> Node() 117 Traceback (most recent call last): 118 ... 119 TypeError: __init__() takes at least 2 arguments (1 given) 120 >>> Node("Something") 121 <Node:/> 122 123 ``name=None``: string 124 The name of the node. If ommitted the key used by the parent Node 125 is used. The node name also defines the node path: 126 127 >>> Node('Something', name='something') 128 <Node:/something> 129 130 The following constructor arguments are also class variables, and as 131 such can be overridden at the class level by subclasses of Node. Useful If 132 you find yourself using a particular pattern repeatedly. 133 134 ``pattern=None``: regular expression string 135 The regular expression used to match user input. If not provided, 136 the node name is used: 137 138 >>> a = Node('Something', name='something') 139 >>> a.pattern == a.name 140 True 141 142 ``separator=r'\s+|\s*$'``: regular expression string 143 A regular expression used to match the text separating this node 144 and the next. 145 146 ``group=0``: integer 147 Nodes can be grouped together to provide visual cues. Groups are 148 ordered ascending numerically. 149 150 ``order=0``: integer 151 Within a group, nodes are normally ordered alphabetically. This can 152 be overridden by setting this to a value other than 0. 153 154 ``match_candidates=False``: boolean 155 The candidates() method returns a list of words that match at the 156 current token which are used for completion, but can also be used 157 to constrain the allowed matches if match_candidates=True. Useful 158 for situations where you have a general regex pattern (eg. a 159 pattern matching files) but a known set of matches at this point 160 (eg. files in the current directory). 161 162 ``traversals=1``: integer 163 The number of times this node can match in any parse context. Alias 164 nodes allow for multiple traversal. 165 166 If ``traversals=0`` the node will match an infinite number of times. 167 """ 168 pattern = None 169 separator = r'\s+|\s*$' 170 order = 0 171 group = 0 172 match_candidates = False 173 traversals = 1 174 175 def __init__(self, help, *args, **kwargs): 176 self._children = {} 177 if isinstance(help, basestring): 178 self.help = LazyHelp(self, help) 179 elif callable(help): 180 self.help = help 181 else: 182 raise InvalidHelp('help must be a callable or a string') 183 if 'pattern' in kwargs: 184 self.pattern = kwargs.pop('pattern') 185 if 'separator' in kwargs: 186 self.separator = kwargs.pop('separator') 187 if self.pattern is not None: 188 self._pattern = re.compile(self.pattern) 189 self._separator = re.compile(self.separator) 190 if self.pattern is not None and self.separator is not None: 191 self._full_match = re.compile('(?:%s)(?:%s)' % 192 (self.pattern, self.separator)) 193 self.name = kwargs.pop('name', None) 194 self.parent = None 195 self.__anonymous_children = 0 196 self(*args, **kwargs) 197 198 def _set_name(self, name): 199 """Set the name of this node. If the Node does not have an existing 200 matching pattern associated with it, a pattern will be created using 201 the name.""" 202 self._name = name 203 if isinstance(name, basestring) and self.pattern is None: 204 self.pattern = name 205 self._pattern = re.compile(name) 206 if self.pattern is not None and self.separator is not None: 207 self._full_match = re.compile('(?:%s)(?:%s)' % 208 (self.pattern, self.separator)) 209 name = property(lambda self: self._name, _set_name) 210 211 def __call__(self, *anonymous, **options): 212 """Update or add options and child nodes. 213 214 Positional arguments are treated as anonymous child nodes, while 215 keyword arguments can either be named child nodes or attribute updates 216 for this node. See __init__ for more information on attributes. 217 218 >>> top = Node('Top', name='top') 219 >>> top(subnode=Node('Subnode')) 220 <Node:/top> 221 >>> top.find('subnode') 222 <Node:/top/subnode> 223 """ 224 for node in anonymous: 225 if not isinstance(node, Node): 226 raise InvalidAnonymousNode('Anonymous node is not a Node object') 227 # TODO Convert help to name instead of __anonymous_<n> 228 node.name = '__anonymous_%i' % self.__anonymous_children 229 node.parent = self 230 self._children[node.name] = node 231 self.__anonymous_children += 1 232 233 for k, v in options.iteritems(): 234 if isinstance(v, Node): 235 if k.endswith('_'): 236 k = k[:-1] 237 v.name = k 238 v.parent = self 239 self._children[k] = v 240 else: 241 setattr(self, k, v) 242 return self 243 244 def __iter__(self): 245 """Iterate over child nodes, ignoring context. 246 247 >>> tree = Node('One')(two=Node('Two'), three=Node('Three')) 248 >>> list(tree) 249 [<Node:/three>, <Node:/two>] 250 """ 251 children = sorted(self._children.values(), 252 key=lambda i: (i.group, i.order, i.name)) 253 for child in children: 254 yield child 255 256 def __setitem__(self, key, child): 257 """Emulate dictionary set. 258 259 >>> node = Node('One') 260 >>> node['two'] = Node('Two') 261 >>> list(node.walk()) 262 [<Node:/>, <Node:/two>] 263 """ 264 self(**{key: child}) 265 266 def __getitem__(self, key): 267 """Emulate dictionary get. 268 269 >>> node = Node('One')(two=Node('Two')) 270 >>> node['two'] 271 <Node:/two> 272 """ 273 return self._children[key] 274 275 def __delitem__(self, key): 276 """Emulate dictionary delete. 277 278 >>> node = Node('One')(two=Node('Two'), three=Node('Three')) 279 >>> list(node.walk()) 280 [<Node:/>, <Node:/three>, <Node:/two>] 281 >>> del node['two'] 282 >>> list(node.walk()) 283 [<Node:/>, <Node:/three>] 284 """ 285 child = self._children.pop(key) 286 child.parent = None 287 288 def __contains__(self, key): 289 """Emulate dictionary key existence test. 290 291 >>> node = Node('One')(two=Node('Two'), three=Node('Three')) 292 >>> 'two' in node 293 True 294 """ 295 return key in self._children 296 297 def walk(self): 298 """Perform a recursive walk of the grammar tree. 299 300 >>> tree = Node('One')(two=Node('Two', three=Node('Three'), 301 ... four=Node('Four'))) 302 >>> list(tree.walk()) 303 [<Node:/>, <Node:/two>, <Node:/two/four>, <Node:/two/three>] 304 """ 305 def walk(root): 306 yield root 307 for node in root._children.itervalues(): 308 for subnode in walk(node): 309 yield subnode 310 311 for node in walk(self): 312 yield node 313 314 def children(self, context, follow=False): 315 """Iterate over child nodes, optionally follow()ing branches. 316 >>> tree = Node('One')(two=Node('Two', three=Node('Three'), 317 ... four=Node('Four')), five=Alias('../two/*')) 318 >>> context = Context(None, None) 319 >>> list(tree.children(context)) 320 [<Alias:/five for /two/*>, <Node:/two>] 321 >>> list(tree.children(context, follow=True)) 322 [<Node:/two/four>, <Node:/two/three>, <Node:/two>] 323 &n
