root/cly/trunk/cly/interactive.py

Revision 571, 12.3 kB (checked in by athomas, 2 months ago)

Allow variables nodes with pre-existing variables to execute correctly.

Line 
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2006-2007 Alec Thomas <alec@swapoff.org>
4 #
5 # This software is licensed as described in the file COPYING, which
6 # you should have received as part of this distribution.
7 #
8
9 """CLY and readline, together at last.
10
11 This module uses readline's line editing and tab completion along with CLY's
12 grammar parser to provide an interactive command line environment.
13
14 It includes support for application specific history files, dynamic prompt,
15 customisable completion key, interactive help and more.
16
17 *Users can press ? at any time to view contextual help.*
18 """
19
20 import os
21 import sys
22 import types
23 import cly.console as console
24 from cly.exceptions import Error, ParseError
25 from cly.builder import Grammar
26 from cly.parser import Parser
27
28 try:
29     import readline
30 except ImportError:
31     readline = None
32 try:
33     from cly import _rlext
34 except ImportError:
35     _rlext = None
36     try:
37         import pyreadline
38     except ImportError:
39         pyreadline = None
40
41
42 __all__ = ['Interact', 'interact', 'brief_exceptions', 'verbose_exceptions',
43            'debug_exceptions']
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)
59
60
61 class InputDriver(object):
62     """Abstraction for line input."""
63
64     def __init__(self, parser, prompt, history_file, history_length):
65         self.parser = parser
66         self.prompt = prompt
67         self.history_file = history_file
68         self.history_length = history_length
69
70     def enter(self):
71         """Enter the input context."""
72
73     def input(self):
74         """Input one line from user and return it."""
75         raise NotImplementedError
76
77     def leave(self):
78         """Exit the input context."""
79
80     @staticmethod
81     def usable():
82         """Called to determine whether this driver is usable."""
83         raise NotImplementedError
84
85
86 class DumbInput(InputDriver):
87     """The horror."""
88
89     def input(self):
90         return raw_input(self.prompt)
91
92     @staticmethod
93     def usable():
94         print >> sys.stderr, \
95             'WARNING: Most line editing features are unavailable.'
96         return True
97
98
99 class ReadlineDriver(InputDriver):
100     """Base class for readline variants."""
101
102     def __init__(self, *args, **kwargs):
103         super(ReadlineDriver, self).__init__(*args, **kwargs)
104         self._cli_inject_text = ''
105         self._completion_candidates = []
106
107     def enter(self):
108         try:
109             readline.set_history_length(self.history_length)
110             readline.read_history_file(self.history_file)
111         except:
112             pass
113
114         readline.parse_and_bind('tab: complete')
115         readline.set_completer_delims(' \t')
116         readline.set_completer(self._completion)
117         readline.set_startup_hook(self._redraw_input)
118
119         self._bind_help()
120
121     def input(self):
122         return raw_input(self.prompt)
123
124     def leave(self):
125         try:
126             readline.write_history_file(self.history_file)
127         except:
128             pass
129
130     @staticmethod
131     def usable():
132         if readline:
133             print >> sys.stderr, \
134                 'WARNING: neither pyreadline nor CLY\'s built-in readline ' \
135                 'extensions found,\n         contextual help is not ' \
136                 'available.'
137         return readline
138
139     def _bind_help(self):
140         pass
141
142     def _force_redisplay(self):
143         raise NotImplementedError
144
145     def _get_cursor(self):
146         raise NotImplementedError
147
148     def _set_cursor(self, cursor):
149         raise NotImplementedError
150
151     cursor = property(lambda s: s._get_cursor(), lambda s, c: s._set_cursor(c))
152
153     # Internal methods
154     def _completion(self, text, state):
155         line = readline.get_line_buffer()[0:readline.get_begidx()]
156         ctx = None
157         try:
158             result = self.parser.parse(line)
159             if not state:
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
166             if self._completion_candidates:
167                 return self._completion_candidates.pop()
168             return None
169         except cly.Error:
170             return None
171
172     def _redraw_input(self):
173         readline.insert_text(self._cli_inject_text)
174         self._cli_inject_text = ''
175
176     def _show_help(self, key, count):
177         try:
178             command = readline.get_line_buffer()[:self.cursor]
179             context = self.parser.parse(command)
180             if context.remaining.strip():
181                 print
182                 candidates = [help[1] for help in context.help()]
183                 text = '%s^ invalid token (candidates are %s)' % \
184                        (' ' * (context.cursor + len(self.prompt)),
185                        ', '.join(candidates))
186                 console.cerror(text)
187                 self._force_redisplay()
188                 return
189             help = context.help()
190             print
191             console.cprint('\n'.join(help.format()))
192             self._force_redisplay()
193             return 0
194         except Exception, e:
195             Interact.dump_traceback(e)
196             self._force_redisplay()
197             return 0
198
199
200 class PyReadlineDriver(ReadlineDriver):
201     """The IPython pure-Python pyreadline implementation."""
202
203     @staticmethod
204     def usable():
205         return pyreadline
206
207     def _bind_help(self):
208         def _show_help_proxy(_, __):
209             self._show_help(None, None)
210
211         pyreadline.rl.mode.cly_help = types.MethodType(
212             _show_help_proxy, pyreadline.rl.mode, pyreadline.Readline
213             )
214         pyreadline.parse_and_bind('?: cly-help')
215         pyreadline.parse_and_bind('Shift-?: cly-help')
216         pyreadline.parse_and_bind('F1: cly-help')
217
218     def _force_redisplay(self):
219         pyreadline.rl._print_prompt()
220
221     def _get_cursor(self):
222         return pyreadline.rl.l_buffer.point
223
224     def _set_cursor(self, cursor):
225         pyreadline.rl.l_buffer.point = cursor
226
227
228 class ExtendedReadlineDriver(ReadlineDriver):
229     """Use CLY's built-in readline extensions."""
230
231     @staticmethod
232     def usable():
233         return _rlext
234
235     def _bind_help(self):
236         _rlext.bind_key(ord('?'), self._show_help)
237
238     def _force_redisplay(self):
239         _rlext.force_redisplay()
240
241     def _get_cursor(self):
242         return _rlext.cursor()
243
244     def _set_cursor(self, cursor):
245         _rlext.cursor(cursor)
246
247
248 class Interact(object):
249     """CLY interaction through readline. Due to readline limitations, only one
250     Interact object can be active within an application.
251
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`.
270     """
271
272     # Available input drivers
273     INPUT_DRIVERS = [ExtendedReadlineDriver, PyReadlineDriver, ReadlineDriver,
274                      DumbInput]
275
276
277     def __init__(self, grammar_or_parser, application='cly', prompt=None,
278                  data=None, history_file=None, history_length=500,
279                  exceptions=None):
280         if prompt is None:
281             prompt = application + '> '
282         if history_file is None:
283             history_file = os.path.expanduser('~/.%s_history' % application)
284         if isinstance(grammar_or_parser, Grammar):
285             parser = Parser(grammar_or_parser, data=data)
286         else:
287             parser = grammar_or_parser
288             assert not data, '"data" ignored because a Parser was passed'
289
290         self.parser = parser
291         self.exceptions = exceptions or (lambda *a, **kw: True)
292
293         self.input_driver = self.best_input_driver(
294             parser, prompt, history_file, history_length
295             )
296
297     def once(self):
298         """Input one command from the user and return the result of the
299         executed command.
300         """
301         while True:
302             command = ''
303             try:
304                 self.input_driver.enter()
305                 try:
306                     command = self.input_driver.input()
307                 except KeyboardInterrupt:
308                     print
309                     continue
310                 except EOFError:
311                     print
312                     return None
313             finally:
314                 self.input_driver.leave()
315
316             try:
317                 context = self.parser.parse(command)
318                 context.execute()
319             except ParseError, e:
320                 self.print_error(context, e)
321             return context
322
323     def loop(self, exceptions=None, every=None):
324         """Repeatedly read and execute commands from the user.
325
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.
338         """
339         exceptions = exceptions or self.exceptions
340         while True:
341             try:
342                 if every:
343                     every(self)
344                 if not self.once():
345                     break
346             except Exception, e:
347                 if exceptions(self, None, False, e):
348                     raise
349
350     def print_error(self, context, e):
351         """Called by `once()` to print a ParseError."""
352         candidates = [help[1] for help in context.help()]
353         if len(candidates) > 1:
354             message = '%s (candidates are %s)'
355         else:
356             message = '%s (expected %s)'
357         message = message % (str(e), ', '.join(candidates))
358         self.error_at_cursor(context, message)
359
360     def error_at_cursor(self, context, text):
361         """Attempt to intelligently print an error at the current cursor
362         offset."""
363         text = str(text)
364         term_width = console.termwidth()
365         indent = ' ' * (context.cursor % term_width
366                         + len(self.prompt))
367         if len(indent + text) > term_width:
368             console.cerror(indent + '^')
369             console.cerror(text)
370         else:
371             console.cerror(indent + '^ ' + text)
372
373     @classmethod
374     def dump_traceback(cls, exception):
375         import traceback
376         from StringIO import StringIO
377         out = StringIO()
378         traceback.print_exc(file=out)
379         print >>sys.stderr, str(exception)
380         print >>sys.stderr, out.getvalue()
381
382     def _get_prompt(self):
383         return self.input_driver.prompt
384
385     def _set_prompt(self, prompt):
386         self.input_driver.prompt = prompt
387
388     prompt = property(_get_prompt, _set_prompt, doc='Prompt. Can be set.')
389
390
391     @classmethod
392     def best_input_driver(cls, *args, **kwargs):
393         """Select the "best" available input driver."""
394         for driver in cls.INPUT_DRIVERS:
395             if driver.usable():
396                 return driver(*args, **kwargs)
397         raise Error('No usable input driver found')
398
399
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     """
405     interact = Interact(grammar_or_parser, *args, **kwargs)
406     interact.loop(exceptions=exceptions)
Note: See TracBrowser for help on using the browser.