1#   Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
2#                       Alex Gaynor
3#                       Antonio Cuni
4#                       Armin Rigo
5#                       Holger Krekel
6#
7#                        All Rights Reserved
8#
9#
10# Permission to use, copy, modify, and distribute this software and
11# its documentation for any purpose is hereby granted without fee,
12# provided that the above copyright notice appear in all copies and
13# that both that copyright notice and this permission notice appear in
14# supporting documentation.
15#
16# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
17# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
18# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
19# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
20# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
21# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
22# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
23
24"""A compatibility wrapper reimplementing the 'readline' standard module
25on top of pyrepl.  Not all functionalities are supported.  Contains
26extensions for multiline input.
27"""
28
29import sys
30import os
31from pyrepl import commands
32from pyrepl.historical_reader import HistoricalReader
33from pyrepl.completing_reader import CompletingReader
34from pyrepl.unix_console import UnixConsole, _error
35
36
37ENCODING = sys.getfilesystemencoding() or 'latin1'     # XXX review
38
39__all__ = [
40    'add_history',
41    'clear_history',
42    'get_begidx',
43    'get_completer',
44    'get_completer_delims',
45    'get_current_history_length',
46    'get_endidx',
47    'get_history_item',
48    'get_history_length',
49    'get_line_buffer',
50    'insert_text',
51    'parse_and_bind',
52    'read_history_file',
53    'read_init_file',
54    'redisplay',
55    'remove_history_item',
56    'replace_history_item',
57    'set_completer',
58    'set_completer_delims',
59    'set_history_length',
60    'set_pre_input_hook',
61    'set_startup_hook',
62    'write_history_file',
63    # ---- multiline extensions ----
64    'multiline_input',
65]
66
67# ____________________________________________________________
68
69
70class ReadlineConfig(object):
71    readline_completer = None
72    completer_delims = dict.fromkeys(' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?')
73
74
75class ReadlineAlikeReader(HistoricalReader, CompletingReader):
76    assume_immutable_completions = False
77    use_brackets = False
78    sort_in_column = True
79
80    def error(self, msg="none"):
81        pass    # don't show error messages by default
82
83    def get_stem(self):
84        b = self.buffer
85        p = self.pos - 1
86        completer_delims = self.config.completer_delims
87        while p >= 0 and b[p] not in completer_delims:
88            p -= 1
89        return ''.join(b[p+1:self.pos])
90
91    def get_completions(self, stem):
92        result = []
93        function = self.config.readline_completer
94        if function is not None:
95            try:
96                stem = str(stem)   # rlcompleter.py seems to not like unicode
97            except UnicodeEncodeError:
98                pass   # but feed unicode anyway if we have no choice
99            state = 0
100            while True:
101                try:
102                    next = function(stem, state)
103                except:
104                    break
105                if not isinstance(next, str):
106                    break
107                result.append(next)
108                state += 1
109            # emulate the behavior of the standard readline that sorts
110            # the completions before displaying them.
111            result.sort()
112        return result
113
114    def get_trimmed_history(self, maxlength):
115        if maxlength >= 0:
116            cut = len(self.history) - maxlength
117            if cut < 0:
118                cut = 0
119        else:
120            cut = 0
121        return self.history[cut:]
122
123    # --- simplified support for reading multiline Python statements ---
124
125    # This duplicates small parts of pyrepl.python_reader.  I'm not
126    # reusing the PythonicReader class directly for two reasons.  One is
127    # to try to keep as close as possible to CPython's prompt.  The
128    # other is that it is the readline module that we are ultimately
129    # implementing here, and I don't want the built-in raw_input() to
130    # start trying to read multiline inputs just because what the user
131    # typed look like valid but incomplete Python code.  So we get the
132    # multiline feature only when using the multiline_input() function
133    # directly (see _pypy_interact.py).
134
135    more_lines = None
136
137    def collect_keymap(self):
138        return super(ReadlineAlikeReader, self).collect_keymap() + (
139            (r'\n', 'maybe-accept'),)
140
141    def __init__(self, console):
142        super(ReadlineAlikeReader, self).__init__(console)
143        self.commands['maybe_accept'] = maybe_accept
144        self.commands['maybe-accept'] = maybe_accept
145
146    def after_command(self, cmd):
147        super(ReadlineAlikeReader, self).after_command(cmd)
148        if self.more_lines is None:
149            # Force single-line input if we are in raw_input() mode.
150            # Although there is no direct way to add a \n in this mode,
151            # multiline buffers can still show up using various
152            # commands, e.g. navigating the history.
153            try:
154                index = self.buffer.index("\n")
155            except ValueError:
156                pass
157            else:
158                self.buffer = self.buffer[:index]
159                if self.pos > len(self.buffer):
160                    self.pos = len(self.buffer)
161
162
163class maybe_accept(commands.Command):
164    def do(self):
165        r = self.reader
166        r.dirty = 1  # this is needed to hide the completion menu, if visible
167        #
168        # if there are already several lines and the cursor
169        # is not on the last one, always insert a new \n.
170        text = r.get_unicode()
171        if "\n" in r.buffer[r.pos:]:
172            r.insert("\n")
173        elif r.more_lines is not None and r.more_lines(text):
174            r.insert("\n")
175        else:
176            self.finish = 1
177
178
179class _ReadlineWrapper(object):
180    reader = None
181    saved_history_length = -1
182    startup_hook = None
183    config = ReadlineConfig()
184    stdin = None
185    stdout = None
186    stderr = None
187
188    def __init__(self, f_in=None, f_out=None):
189        self.f_in = f_in if f_in is not None else os.dup(0)
190        self.f_out = f_out if f_out is not None else os.dup(1)
191
192    def setup_std_streams(self, stdin, stdout, stderr):
193        self.stdin = stdin
194        self.stdout = stdout
195        self.stderr = stderr
196
197    def get_reader(self):
198        if self.reader is None:
199            console = UnixConsole(self.f_in, self.f_out, encoding=ENCODING)
200            self.reader = ReadlineAlikeReader(console)
201            self.reader.config = self.config
202        return self.reader
203
204    def raw_input(self, prompt=''):
205        try:
206            reader = self.get_reader()
207        except _error:
208            return _old_raw_input(prompt)
209
210        # the builtin raw_input calls PyOS_StdioReadline, which flushes
211        # stdout/stderr before displaying the prompt. Try to mimic this
212        # behavior: it seems to be the correct thing to do, and moreover it
213        # mitigates this pytest issue:
214        # https://github.com/pytest-dev/pytest/issues/5134
215        if self.stdout and hasattr(self.stdout, 'flush'):
216            self.stdout.flush()
217        if self.stderr and hasattr(self.stderr, 'flush'):
218            self.stderr.flush()
219
220        reader.ps1 = prompt
221        return reader.readline(startup_hook=self.startup_hook)
222
223    def multiline_input(self, more_lines, ps1, ps2, returns_unicode=False):
224        """Read an input on possibly multiple lines, asking for more
225        lines as long as 'more_lines(unicodetext)' returns an object whose
226        boolean value is true.
227        """
228        reader = self.get_reader()
229        saved = reader.more_lines
230        try:
231            reader.more_lines = more_lines
232            reader.ps1 = reader.ps2 = ps1
233            reader.ps3 = reader.ps4 = ps2
234            return reader.readline(returns_unicode=returns_unicode)
235        finally:
236            reader.more_lines = saved
237
238    def parse_and_bind(self, string):
239        pass  # XXX we don't support parsing GNU-readline-style init files
240
241    def set_completer(self, function=None):
242        self.config.readline_completer = function
243
244    def get_completer(self):
245        return self.config.readline_completer
246
247    def set_completer_delims(self, string):
248        self.config.completer_delims = dict.fromkeys(string)
249
250    def get_completer_delims(self):
251        chars = self.config.completer_delims.keys()
252        chars.sort()
253        return ''.join(chars)
254
255    def _histline(self, line):
256        line = line.rstrip('\n')
257        try:
258            return unicode(line, ENCODING)
259        except UnicodeDecodeError:   # bah, silently fall back...
260            return unicode(line, 'utf-8', 'replace')
261
262    def get_history_length(self):
263        return self.saved_history_length
264
265    def set_history_length(self, length):
266        self.saved_history_length = length
267
268    def get_current_history_length(self):
269        return len(self.get_reader().history)
270
271    def read_history_file(self, filename='~/.history'):
272        # multiline extension (really a hack) for the end of lines that
273        # are actually continuations inside a single multiline_input()
274        # history item: we use \r\n instead of just \n.  If the history
275        # file is passed to GNU readline, the extra \r are just ignored.
276        history = self.get_reader().history
277        f = open(os.path.expanduser(filename), 'r')
278        buffer = []
279        for line in f:
280            if line.endswith('\r\n'):
281                buffer.append(line)
282            else:
283                line = self._histline(line)
284                if buffer:
285                    line = ''.join(buffer).replace('\r', '') + line
286                    del buffer[:]
287                if line:
288                    history.append(line)
289        f.close()
290
291    def write_history_file(self, filename='~/.history'):
292        maxlength = self.saved_history_length
293        history = self.get_reader().get_trimmed_history(maxlength)
294        f = open(os.path.expanduser(filename), 'w')
295        for entry in history:
296            if isinstance(entry, unicode):
297                try:
298                    entry = entry.encode(ENCODING)
299                except UnicodeEncodeError:   # bah, silently fall back...
300                    entry = entry.encode('utf-8')
301            entry = entry.replace('\n', '\r\n')   # multiline history support
302            f.write(entry + '\n')
303        f.close()
304
305    def clear_history(self):
306        del self.get_reader().history[:]
307
308    def get_history_item(self, index):
309        history = self.get_reader().history
310        if 1 <= index <= len(history):
311            return history[index-1]
312        else:
313            return None        # blame readline.c for not raising
314
315    def remove_history_item(self, index):
316        history = self.get_reader().history
317        if 0 <= index < len(history):
318            del history[index]
319        else:
320            raise ValueError("No history item at position %d" % index)
321            # blame readline.c for raising ValueError
322
323    def replace_history_item(self, index, line):
324        history = self.get_reader().history
325        if 0 <= index < len(history):
326            history[index] = self._histline(line)
327        else:
328            raise ValueError("No history item at position %d" % index)
329            # blame readline.c for raising ValueError
330
331    def add_history(self, line):
332        self.get_reader().history.append(self._histline(line))
333
334    def set_startup_hook(self, function=None):
335        self.startup_hook = function
336
337    def get_line_buffer(self):
338        return self.get_reader().get_buffer()
339
340    def _get_idxs(self):
341        start = cursor = self.get_reader().pos
342        buf = self.get_line_buffer()
343        for i in xrange(cursor - 1, -1, -1):
344            if buf[i] in self.get_completer_delims():
345                break
346            start = i
347        return start, cursor
348
349    def get_begidx(self):
350        return self._get_idxs()[0]
351
352    def get_endidx(self):
353        return self._get_idxs()[1]
354
355    def insert_text(self, text):
356        return self.get_reader().insert(text)
357
358
359_wrapper = _ReadlineWrapper()
360
361# ____________________________________________________________
362# Public API
363
364parse_and_bind = _wrapper.parse_and_bind
365set_completer = _wrapper.set_completer
366get_completer = _wrapper.get_completer
367set_completer_delims = _wrapper.set_completer_delims
368get_completer_delims = _wrapper.get_completer_delims
369get_history_length = _wrapper.get_history_length
370set_history_length = _wrapper.set_history_length
371get_current_history_length = _wrapper.get_current_history_length
372read_history_file = _wrapper.read_history_file
373write_history_file = _wrapper.write_history_file
374clear_history = _wrapper.clear_history
375get_history_item = _wrapper.get_history_item
376remove_history_item = _wrapper.remove_history_item
377replace_history_item = _wrapper.replace_history_item
378add_history = _wrapper.add_history
379set_startup_hook = _wrapper.set_startup_hook
380get_line_buffer = _wrapper.get_line_buffer
381get_begidx = _wrapper.get_begidx
382get_endidx = _wrapper.get_endidx
383insert_text = _wrapper.insert_text
384
385# Extension
386multiline_input = _wrapper.multiline_input
387
388# Internal hook
389_get_reader = _wrapper.get_reader
390
391# ____________________________________________________________
392# Stubs
393
394
395def _make_stub(_name, _ret):
396    def stub(*args, **kwds):
397        import warnings
398        warnings.warn("readline.%s() not implemented" % _name, stacklevel=2)
399    stub.func_name = _name
400    globals()[_name] = stub
401
402for _name, _ret in [
403    ('read_init_file', None),
404    ('redisplay', None),
405    ('set_pre_input_hook', None),
406]:
407    assert _name not in globals(), _name
408    _make_stub(_name, _ret)
409
410
411def _setup():
412    global _old_raw_input
413    if _old_raw_input is not None:
414        return
415    # don't run _setup twice
416
417    try:
418        f_in = sys.stdin.fileno()
419        f_out = sys.stdout.fileno()
420    except (AttributeError, ValueError):
421        return
422    if not os.isatty(f_in) or not os.isatty(f_out):
423        return
424
425    _wrapper.f_in = f_in
426    _wrapper.f_out = f_out
427    _wrapper.setup_std_streams(sys.stdin, sys.stdout, sys.stderr)
428
429    if '__pypy__' in sys.builtin_module_names:    # PyPy
430
431        def _old_raw_input(prompt=''):
432            # sys.__raw_input__() is only called when stdin and stdout are
433            # as expected and are ttys.  If it is the case, then get_reader()
434            # should not really fail in _wrapper.raw_input().  If it still
435            # does, then we will just cancel the redirection and call again
436            # the built-in raw_input().
437            try:
438                del sys.__raw_input__
439            except AttributeError:
440                pass
441            return raw_input(prompt)
442        sys.__raw_input__ = _wrapper.raw_input
443
444    else:
445        # this is not really what readline.c does.  Better than nothing I guess
446        import __builtin__
447        _old_raw_input = __builtin__.raw_input
448        __builtin__.raw_input = _wrapper.raw_input
449
450_old_raw_input = None
451_setup()
452