root/cly/trunk/cly/interactive.py @ 430

Revision 430, 8.6 KB (checked in by athomas, 3 years ago)

cly:

  • Cleaned up and added a few more doctests.
  • You can now set a Variable's var_name after construction.
  • Renamed Interact.interact_{loop,once}() to Interact.{loop,once}()
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
11This module uses readline's line editing and tab completion along with CLY's
12grammar parser to provide an interactive command line environment.
13
14It includes support for application specific history files, dynamic prompt,
15customisable completion key, interactive help and more.
16
17Press ``?`` at any location to contextual help.
18"""
19
20import os
21import sys
22import readline
23import cly
24import cly.rlext
25import cly.console as console
26
27
28__all__ = ['Interact']
29
30
31class Interact(object):
32    """CLY interaction through readline. Due to readline limitations, only one
33    Interact object can be active within an application.
34
35    Constructor arguments:
36
37    ``parser``: ``Parser`` or ``Grammar`` object
38        The parser/grammar to use for interaction.
39
40    ``application='cly'``: string
41        The application name. Used to construct the history file name and
42        prompt, if not provided.
43
44    ``prompt=None``: string
45        The prompt.
46
47    ``user_context=None``: `anything`
48        A user-specified object to pass to the parser. The parser builds each
49        parse ``Context`` with this object, which in turn will deliver this
50        object on to terminal nodes that have set ``with_context=True``.
51
52    ``with_context=False``: `boolean`
53        Force ``user_context`` to be passed to all action nodes, unless they
54        explicitly set the member variable ``with_context=False``.
55
56    ``history_file=None``: `string`
57        Defaults to ``~/.<application>_history``.
58
59    ``history_length=500``: `integer`
60        Lines of history to keep.
61
62    ``completion_key='tab'``: `string`
63        Key to use for completion, per the readline documentation.
64
65    ``completion_delimiters=' \t'``: `string`
66        Characters that terminate completion.
67
68    ``help_key='?'``: `key`
69        Key to use for tab completion.
70
71    """
72    _cli_inject_text = ''
73    _completion_candidates = []
74    _parser = None
75    prompt = None
76    user_context = None
77    history_file = None
78    application = None
79
80    def __init__(self, parser, application='cly', prompt=None,
81                 user_context=None, with_context=None, history_file=None,
82                 history_length=500, completion_key='tab',
83                 #completion_delimiters='`!#$%^&*()=+[{]}\|;\'",<>? \t',
84                 completion_delimiters=' \t',
85                 help_key='?', inhibit_exceptions=False,
86                 with_backtrace=False):
87        if prompt is None:
88            prompt = application + '> '
89        if history_file is None:
90            history_file = os.path.expanduser('~/.%s_history' % application)
91        if isinstance(parser, cly.Grammar):
92            parser = cly.Parser(parser)
93
94        if with_context is not None:
95            parser.with_context = with_context
96        if user_context is not None:
97            parser.user_context = user_context
98        Interact._parser = parser
99        Interact.prompt = prompt
100        Interact.application = application
101        Interact.user_context = user_context
102        Interact.history_file = history_file
103        Interact.history_length = history_length
104        Interact.completion_delimiters = completion_delimiters
105        Interact.completion_key = completion_key
106
107        try:
108            readline.set_history_length(history_length)
109            readline.read_history_file(history_file)
110        except:
111            pass
112
113        readline.parse_and_bind("%s: complete" % completion_key)
114        readline.set_completer_delims(self.completion_delimiters)
115        readline.set_completer(Interact._cli_completion)
116        readline.set_startup_hook(Interact._cli_injector)
117
118        # Use custom readline extensions
119        cly.rlext.bind_key(ord(help_key), Interact._cli_help)
120
121
122    def once(self, default_text='', callback=None):
123        """Input one command from the user and return the result of the
124        executed command. `callback` is called with the Interact object before
125        each line is displayed."""
126        Interact._cli_inject_text = default_text
127
128        while True:
129            command = ''
130            try:
131                command = raw_input(self.prompt)
132            except KeyboardInterrupt:
133                print
134                continue
135            except EOFError:
136                print
137                return None
138
139            try:
140                context = Interact._parser.parse(command, user_context=self.user_context)
141                context.execute()
142            except cly.ParseError, e:
143                self.print_error(context, e)
144            return context
145
146    def loop(self, inhibit_exceptions=False, with_backtrace=False):
147        """Repeatedly read and execute commands from the user.
148
149        Arguments:
150
151        ``inhibit_exceptions=True``: `boolean`
152            Normally, ``interact_loop`` will pass exceptions back to the caller for
153            handling. Setting this to ``True`` will cause an error message to
154            be printed, but interaction will continue.
155
156        ``with_backtrace=False``: `boolean`
157            Whether to print a full backtrace when ``inhibit_exceptions=True``.
158        """
159        try:
160            while True:
161                try:
162                    if not self.once():
163                        break
164                except Exception, e:
165                    if inhibit_exceptions:
166                        if with_backtrace:
167                            import traceback
168                            console.error(traceback.format_exc())
169                        else:
170                            console.error('error: %s' % e)
171                    else:
172                        raise
173        finally:
174            self.write_history()
175
176    def print_error(self, context, e):
177        """Called by `once()` to print a ParseError."""
178        candidates = [help[1] for help in context.help()]
179        if len(candidates) > 1:
180            message = '%s (candidates are %s)'
181        else:
182            message = '%s (expected %s)'
183        message = message % (str(e), ', '.join(candidates))
184        self.error_at_cursor(context, message)
185
186    def error_at_cursor(self, context, text):
187        """Attempt to intelligently print an error at the current cursor
188        offset."""
189        text = str(text)
190        term_width = console.termwidth()
191        indent = ' ' * (context.cursor % term_width
192                        + len(Interact.prompt))
193        if len(indent + text) > term_width:
194            console.error(indent + '^')
195            console.error(text)
196        else:
197            console.error(indent + '^ ' + text)
198
199    def write_history(self):
200        """ Write command line history out. """
201        try:
202            readline.write_history_file(self.history_file)
203        except:
204            pass
205
206    @staticmethod
207    def _dump_traceback(exception):
208        import traceback
209        from StringIO import StringIO
210        out = StringIO()
211        traceback.print_exc(file = out)
212        print >>sys.stderr, str(exception)
213        print >>sys.stderr, out.getvalue()
214
215
216    @staticmethod
217    def _cli_injector():
218        readline.insert_text(Interact._cli_inject_text)
219        Interact._cli_inject_text = ''
220
221
222    @staticmethod
223    def _cli_completion(text, state):
224        line = readline.get_line_buffer()[0:readline.get_begidx()]
225        ctx = None
226        try:
227            result = Interact._parser.parse(line)
228            if not state:
229                Interact._completion_candidates = list(result.candidates(text))
230            if Interact._completion_candidates:
231                return Interact._completion_candidates.pop()
232            return None
233        except cly.Error:
234            return None
235        except Exception, e:
236            Interact._dump_traceback(e)
237            cly.rlext.force_redisplay()
238            raise
239
240
241    @staticmethod
242    def _cli_help(key, count):
243        try:
244            command = readline.get_line_buffer()[:cly.rlext.cursor()]
245            context = Interact._parser.parse(command)
246            if context.remaining.strip():
247                print
248                candidates = [help[1] for help in context.help()]
249                text = '%s^ invalid token (candidates are %s)' % \
250                       (' ' * (context.cursor + len(Interact.prompt)),
251                        ', '.join(candidates))
252                console.error(text)
253                cly.rlext.force_redisplay()
254                return
255            help = context.help()
256            print
257            help.format(sys.stdout)
258            cly.rlext.force_redisplay()
259        except Exception, e:
260            Interact._dump_traceback(e)
261            cly.rlext.force_redisplay()
262            raise
Note: See TracBrowser for help on using the browser.