root/cly/tags/final-dictionary-based-version/cly/util.py

Revision 110, 10.0 kB (checked in by athomas, 3 years ago)
  • Changed MERGE behaviour so that a list of grammars nust be returned by the callable.
  • Made COMPLETE automatically cull candidates that do not match the current prefix.
Line 
1 from crash.console import *
2 from cly.symbols import *
3 import cly.parser, cly.rlext
4 from cly.parser import Parser, Result
5 import os
6 import os.path
7 import inspect
8 import pydoc
9 import sys
10 import types
11 import re
12 import readline
13 from fnmatch import fnmatch
14
15 """
16
17 A collection of functions useful in conjunction with CLY.
18
19 """
20
21
22 __all__ = [ "file_completer", "introspect_grammar", "format_help", "execute_result", "execute_command", "interact", "interact_loop", ]
23
24 # XXX Used by the interactive functions XXX
25 __cli_inject_text = ''
26 __completion_candidates = []
27 __parser = None
28
29 class file_completer:
30     """File tab completion for CLY.
31     Usage:
32         ... COMPLETE : cly.util.file_completer('*.py')
33     """
34     def __init__(self, *args):
35         self.__args = args
36
37     def __call__(self, ctx, sofar):
38         return self.__complete(sofar, *self.__args)
39
40     def __complete(self, sofar, pattern = '*', show_hidden = False):
41         base = sofar
42         remainder = ''
43         files = []
44         # Is the current text a directory?
45         if not os.path.exists(base):
46             remainder = os.path.basename(base)
47             base = os.path.dirname(base)
48         if os.path.isdir(base or '.'):
49             files = os.listdir(base or '.')
50             files.sort()
51             # Filter file list
52             files = [os.path.join(base, file) for file in files if \
53                 (not show_hidden and file[0] != '.') \
54                 and file.startswith(remainder) \
55                 and (os.path.isdir(os.path.join(base, file)) or fnmatch(file, pattern)) \
56                 ]
57             files = [os.path.isdir(file) and file + '/' or file for file in files]
58         if len(files) == 1 and not os.path.isdir(files[0]):
59             return [files[0] + ' ']
60         return files
61
62 def dump_traceback(exception):
63     import traceback
64     from StringIO import StringIO
65     out = StringIO()
66     traceback.print_exc(file = out)
67     error(str(exception), "\n^N^1", out.getvalue())
68     raise exception
69
70 def introspect_grammar(instance, split_names = True):
71     """ Generate a CLY grammar from a class instance.
72
73         Methods in the form "def some_command(self, arg): ..." are converted to
74         the grammar:
75             
76             'some' : {
77                 HELP : 'some',
78                 'command' : {
79                     HELP : pydoc.getdoc(instance.some_command),
80                     '.+' : {
81                         VAR : 's:arg',
82                         ACTION : instance.some_command,
83                     },
84                 },
85             },
86
87         CamelCase function names are also translated in a similar fashion.
88
89         In addition, class members that are also instances of classes, will be
90         inserted into the grammar in a similar fashion.
91     """
92     grammar = {}
93     namere = re.compile(r'''([A-Z]{2,}|[A-Z]+[a-z]*|[a-z]+)''')
94     for fullname, method in inspect.getmembers(instance, lambda m: type(m) is types.InstanceType or inspect.ismethod(m)):
95         if fullname[0] == '_':
96             continue
97
98         if split_names:
99             name = [x.lower() for x in namere.findall(' '.join(fullname.split('_')))]
100         else:
101             name = [ fullname ]
102         # Merge generated name into grammar
103         def merge(grammar, name):
104             if not name:
105                 return grammar
106             return merge(grammar.setdefault(name[0], {
107                 HELP : name[0],
108                 }), name[1:])
109         g = merge(grammar, name)
110
111         label = 'execute_%s' % fullname
112
113         g[HELP] = pydoc.getdoc(method) or name[-1]
114         g[LABEL] = label
115
116         if type(method) is types.InstanceType:
117             g.update(introspect_grammar(method, split_names = split_names))
118             continue
119
120         # Insert args
121         argspec = inspect.getargspec(method)
122         args = argspec[0]
123         if args and args[0] == 'self':
124             args = args[1:]
125         if argspec[3] is not None:
126             defaults = dict(zip(args[-len(argspec[3]):], argspec[3]))
127         else:
128             defaults = {}
129
130         def generate_argg(arg):
131             help = '%s (^B%s^N)' % (arg, arg in defaults and '%s' % str(defaults[arg]) or '^Urequired')
132             return help, {
133                 UNLESS_VAR : arg,
134                 VAR : arg,
135                 HELP : ('<%s>' % arg, help),
136                 JUMP : label,
137             }
138
139         if len(args) == 1:
140             arg = args[0]
141             help, argg = generate_argg(arg)
142             g['.*'] = argg
143         else:
144             for arg in args:
145                 help, argg = generate_argg(arg)
146                 g[arg] = {
147                     UNLESS_VAR : arg,
148                     HELP : help,
149                     '.*' : argg,
150                 }
151
152         class validate_args:
153             def __init__(self, args, defaults):
154                 from copy import copy
155                 self.args = args
156                 self.defaults = defaults
157
158             def __call__(self, ctx):
159                 for arg in self.args:
160                     if arg not in self.defaults and arg not in ctx:
161                         return False
162                 return True
163        
164         g[ACTION] = {
165             IF : validate_args(args, defaults),
166             ACTION : method,
167             HELP : pydoc.getdoc(method) or 'Execute command',
168         }
169
170     return grammar
171
172 def __cli_completion(text, state):
173     global __completion_candidates
174     line = readline.get_line_buffer()[0:readline.get_begidx()]
175     ctx = None
176     try:
177         result = __parser.parse(line)
178         if not state:
179             __completion_candidates = result.candidates(text)
180         if __completion_candidates:
181             return __completion_candidates.pop()
182         return None
183     except cly.parser.Error:
184         return None
185     except Exception, e:
186         print
187         dump_traceback(e)
188         cly.rlext.force_redisplay()
189
190 def __cli_injector():
191     try:
192         global __cli_inject_text
193         readline.insert_text(__cli_inject_text)
194         __cli_inject_text = ''
195     except e:
196         dump_traceback(e)
197
198 def __generate_help(parser, line, cursor_offset, need_linefeed = False):
199     try:
200         result = parser.parse(line)
201         # We are not at the end of the line
202         if result.token:
203             readline.insert_text("?")
204             return False
205         if result.context.parsed_tokens and result.context.parsed_tokens[-1].start() >= cursor_offset:
206             print result.context.parsed_tokens[-1].start()
207             return False
208         if need_linefeed:
209             print
210         format_help(result.help())
211         return True
212     except Exception, e:
213         dump_traceback(e)
214
215 def __cli_help(key, count):
216     global __parser
217     try:
218         if __generate_help(__parser, readline.get_line_buffer(), cly.rlext.cursor(), True):
219             cly.rlext.force_redisplay()
220     except Exception, e:
221         print
222         try:
223             dump_traceback(e)
224         finally:
225             cly.rlext.force_redisplay()
226             raise
227
228 def format_help(help):
229     """ Format the output of cly.parser.Result.help() in a useful way. """
230     header, help, footer = help
231     length = 0
232     for group in help:
233         for h in help[group]:
234             if len(h[0]) > length:
235                 length = len(h[0])
236     if header:
237         cprint(wraptoterm(header))
238         print
239     count = 0
240     groups = len(help.keys())
241     last_group = last_order = None
242     for order, group in sorted(help, lambda a, b: cmp(a[0], b[0])):
243         subcount = 0
244         for h in help[(order, group)]:
245             if last_order != None and order != last_order:
246                 last_order = order
247
248             cprint(wraptoterm("  ^B%s^N %s" % (h[0] + (length - len(h[0])) * ' ', h[1]), subsequent_indent = ' ' * (length + 3)))
249             last_order = order
250         count += 1
251         if count != groups:
252             print
253     if footer:
254         print
255         cprint(wraptoterm(footer))
256
257 def execute_result(result):
258     """ Execute the Result object passed. """
259     if result.state == Result.NOP:
260         pass
261     elif result.state == Result.OK:
262         result()
263     else:
264         if result.token:
265             error("%s (near '%s')^N" % (result.message, result.token.group(0)))
266         else:
267             error(result.message)
268
269 def execute_command(parser, command):
270     """ Parse and execute the given command, returning the Result. """
271     __parser = parser
272     command = re.sub(r'^\s+|\s+$', '', command)
273     if command == '':
274         return Result(Result.NOP)
275
276     # Looking for help?
277     if command[-1] == '?':
278         command = re.sub(r'\?\s*$', '', command)
279         __generate_help(parser, command, len(command))
280         __cli_inject_text = command
281         return Result(Result.NOP)
282     else:
283         result =  parser.parse(command)
284         execute_result(result)
285         return result
286
287 def interact(parser, prompt = 'cly> ', default_text=''):
288     """ Input one command from the user and return the Result of the executed
289         command. """
290     global __parser, __cli_inject_text
291
292     __cli_inject_text = default_text
293     # - and : are acceptable "word" characters
294     readline.set_completer_delims('`~!#$%^&*()=+[{]}\|;\'",<>? \t')
295     readline.set_completer(__cli_completion)
296     readline.set_startup_hook(__cli_injector)
297
298     # Use custom readline extensions not in 2.4
299     cly.rlext.bind_key(ord('?'), __cli_help)
300
301     while True:
302         __parser = parser
303         command = ''
304         try:
305             command = raw_input(prompt)
306         except KeyboardInterrupt:
307             print
308             continue
309         except EOFError:
310             print
311             return Result(Result.ENDOFINPUT)
312
313         return execute_command(parser, command)
314
315 def interact_loop(grammar, prompt='cly> ',
316                   history=os.path.expanduser('~/.clyhistory'),
317                   user_context=None, history_length=500):
318     """ Repeatedly read and execute commands from the user. """
319     try:
320         readline.set_history_length(history_length)
321         readline.read_history_file(history)
322     except:
323         pass
324     parser = Parser(grammar, user_context)
325     try:
326         while interact(parser, prompt).state != Result.ENDOFINPUT:
327             pass
328     finally:
329         try:
330             readline.write_history_file(history)
331         except:
332             pass
Note: See TracBrowser for help on using the browser.