diff --git a/pyrepl/_minimal_curses.py b/pyrepl/_minimal_curses.py deleted file mode 100644 index 6dade44..0000000 --- a/pyrepl/_minimal_curses.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Minimal '_curses' module, the low-level interface for curses module -which is not meant to be used directly. - -Based on ctypes. It's too incomplete to be really called '_curses', so -to use it, you have to import it and stick it in sys.modules['_curses'] -manually. - -Note that there is also a built-in module _minimal_curses which will -hide this one if compiled in. -""" - -import ctypes.util - - -class error(Exception): - pass - - -def _find_clib(): - trylibs = ['ncursesw', 'ncurses', 'curses'] - - for lib in trylibs: - path = ctypes.util.find_library(lib) - if path: - return path - raise ImportError("curses library not found") - -_clibpath = _find_clib() -clib = ctypes.cdll.LoadLibrary(_clibpath) - -clib.setupterm.argtypes = [ctypes.c_char_p, ctypes.c_int, - ctypes.POINTER(ctypes.c_int)] -clib.setupterm.restype = ctypes.c_int - -clib.tigetstr.argtypes = [ctypes.c_char_p] -clib.tigetstr.restype = ctypes.POINTER(ctypes.c_char) - -clib.tparm.argtypes = [ctypes.c_char_p] + 9 * [ctypes.c_int] -clib.tparm.restype = ctypes.c_char_p - -OK = 0 -ERR = -1 - -# ____________________________________________________________ - -try: - from __pypy__ import builtinify - builtinify # silence broken pyflakes -except ImportError: - builtinify = lambda f: f - - -@builtinify -def setupterm(termstr, fd): - err = ctypes.c_int(0) - result = clib.setupterm(termstr, fd, ctypes.byref(err)) - if result == ERR: - raise error("setupterm() failed (err=%d)" % err.value) - - -@builtinify -def tigetstr(cap): - if not isinstance(cap, bytes): - cap = cap.encode('ascii') - result = clib.tigetstr(cap) - if ctypes.cast(result, ctypes.c_void_p).value == ERR: - return None - return ctypes.cast(result, ctypes.c_char_p).value - - -@builtinify -def tparm(str, i1=0, i2=0, i3=0, i4=0, i5=0, i6=0, i7=0, i8=0, i9=0): - result = clib.tparm(str, i1, i2, i3, i4, i5, i6, i7, i8, i9) - if result is None: - raise error("tparm() returned NULL") - return result diff --git a/pyrepl/commands.py b/pyrepl/commands.py index af71c70..c121f34 100644 --- a/pyrepl/commands.py +++ b/pyrepl/commands.py @@ -369,8 +369,12 @@ def do(self): class qIHelp(Command): def do(self): + from .reader import disp_str + r = self.reader - r.insert((bytes(self.event) + r.console.getpending().data) * r.get_arg()) + pending = r.console.getpending().data + disp = disp_str((self.event + pending))[0] + r.insert(disp * r.get_arg()) r.pop_input_trans() from pyrepl import input @@ -379,7 +383,7 @@ class QITrans(object): def push(self, evt): self.evt = evt def get(self): - return ('qIHelp', self.evt.raw) + return ('qIHelp', self.evt.data) class quoted_insert(Command): kills_digit_arg = 0 diff --git a/pyrepl/completer.py b/pyrepl/completer.py index 45f40c1..a6a67d0 100644 --- a/pyrepl/completer.py +++ b/pyrepl/completer.py @@ -19,12 +19,10 @@ try: import __builtin__ as builtins - builtins # silence broken pyflakes except ImportError: import builtins - -class Completer(object): +class Completer: def __init__(self, ns): self.ns = ns @@ -81,10 +79,11 @@ def attr_matches(self, text): matches.append("%s.%s" % (expr, word)) return matches - def get_class_members(klass): ret = dir(klass) if hasattr(klass, '__bases__'): for base in klass.__bases__: ret = ret + get_class_members(base) return ret + + diff --git a/pyrepl/completing_reader.py b/pyrepl/completing_reader.py index 56f13ca..6deed9c 100644 --- a/pyrepl/completing_reader.py +++ b/pyrepl/completing_reader.py @@ -40,6 +40,7 @@ def prefix(wordlist, j=0): STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]") + def stripcolor(s): return STRIPCOLOR_REGEX.sub('', s) @@ -65,8 +66,8 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): item = "%s " padding = 2 maxlen = min(max(map(real_len, wordlist)), cons.width - padding) - cols = int(cons.width / (maxlen + padding)) - rows = int((len(wordlist) - 1)/cols + 1) + cols = cons.width // (maxlen + padding) + rows = (len(wordlist) - 1) // cols + 1 if sort_in_column: # sort_in_column=False (default) sort_in_column=True @@ -104,7 +105,7 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): # To summarise the summary of the summary:- people are a problem. # -- The Hitch-Hikers Guide to the Galaxy, Episode 12 -#### Desired behaviour of the completions commands. +# Desired behaviour of the completions commands. # the considerations are: # (1) how many completions are possible # (2) whether the last command was a completion @@ -275,7 +276,7 @@ def get_completions(self, stem): reader.ps1 = "c**> " reader.ps2 = "c/*> " reader.ps3 = "c|*> " - reader.ps4 = "c\*> " + reader.ps4 = r"c\*> " while reader.readline(): pass diff --git a/pyrepl/console.py b/pyrepl/console.py index cfbcbe9..ac242a8 100644 --- a/pyrepl/console.py +++ b/pyrepl/console.py @@ -17,7 +17,6 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - class Event(object): """An Event. `evt' is 'key' or somesuch.""" __slots__ = 'evt', 'data', 'raw' @@ -42,7 +41,7 @@ class Console(object): height, width, """ - + def refresh(self, screen, xy): pass diff --git a/pyrepl/curses.py b/pyrepl/curses.py index 0331ce0..842fa0e 100644 --- a/pyrepl/curses.py +++ b/pyrepl/curses.py @@ -19,5 +19,26 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# If we are running on top of pypy, we import only _minimal_curses. +# Don't try to fall back to _curses, because that's going to use cffi +# and fall again more loudly. +import sys +if '__pypy__' in sys.builtin_module_names: + # pypy case + import _minimal_curses as _curses +else: + # cpython case + try: + import _curses + except ImportError: + # Who knows, maybe some environment has "curses" but not "_curses". + # If not, at least the following import gives a clean ImportError. + try: + import curses as _curses + except ImportError: + import _curses -from ._minimal_curses import setupterm, tigetstr, tparm, error +setupterm = _curses.setupterm +tigetstr = _curses.tigetstr +tparm = _curses.tparm +error = _curses.error diff --git a/pyrepl/historical_reader.py b/pyrepl/historical_reader.py index 5b75bfb..c1817ae 100644 --- a/pyrepl/historical_reader.py +++ b/pyrepl/historical_reader.py @@ -17,7 +17,7 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -from pyrepl import reader, commands +from pyrepl import reader, commands, input from pyrepl.reader import Reader as R isearch_keymap = tuple( @@ -215,7 +215,6 @@ def __init__(self, console): isearch_forwards, isearch_backwards, operate_and_get_next]: self.commands[c.__name__] = c self.commands[c.__name__.replace('_', '-')] = c - from pyrepl import input self.isearch_trans = input.KeymapTranslator( isearch_keymap, invalid_cls=isearch_end, character_cls=isearch_add_character) diff --git a/pyrepl/keymaps.py b/pyrepl/keymaps.py index 76ba896..97f106f 100644 --- a/pyrepl/keymaps.py +++ b/pyrepl/keymaps.py @@ -62,7 +62,7 @@ (r'\M-\n', 'self-insert'), (r'\', 'self-insert')] + \ [(c, 'self-insert') - for c in map(chr, range(32, 127)) if c <> '\\'] + \ + for c in map(chr, range(32, 127)) if c != '\\'] + \ [(c, 'self-insert') for c in map(chr, range(128, 256)) if c.isalpha()] + \ [(r'\', 'up'), @@ -101,7 +101,7 @@ reader_vi_insert_keymap = tuple( [(c, 'self-insert') - for c in map(chr, range(32, 127)) if c <> '\\'] + \ + for c in map(chr, range(32, 127)) if c != '\\'] + \ [(c, 'self-insert') for c in map(chr, range(128, 256)) if c.isalpha()] + \ [(r'\C-d', 'delete'), diff --git a/pyrepl/module_lister.py b/pyrepl/module_lister.py index f3d7b0f..327f524 100644 --- a/pyrepl/module_lister.py +++ b/pyrepl/module_lister.py @@ -40,8 +40,8 @@ def _make_module_list_dir(dir, suffs, prefix=''): return sorted(set(l)) def _make_module_list(): - import imp - suffs = [x[0] for x in imp.get_suffixes() if x[0] != '.pyc'] + import importlib.machinery + suffs = [x for x in importlib.machinery.all_suffixes() if x != '.pyc'] suffs.sort(reverse=True) _packages[''] = list(sys.builtin_module_names) for dir in sys.path: diff --git a/pyrepl/pygame_console.py b/pyrepl/pygame_console.py index cb90b8b..e52de17 100644 --- a/pyrepl/pygame_console.py +++ b/pyrepl/pygame_console.py @@ -130,7 +130,7 @@ def paint_margin(self): s.fill(c, [0, 600 - bmargin, 800, bmargin]) s.fill(c, [800 - rmargin, 0, lmargin, 600]) - def refresh(self, screen, (cx, cy)): + def refresh(self, screen, cxy): self.screen = screen self.pygame_screen.fill(colors.bg, [0, tmargin + self.cur_top + self.scroll, @@ -139,6 +139,7 @@ def refresh(self, screen, (cx, cy)): line_top = self.cur_top width, height = self.fontsize + cx, cy = cxy self.cxy = (cx, cy) cp = self.char_pos(cx, cy) if cp[1] < tmargin: @@ -282,7 +283,7 @@ def flushoutput(self): def forgetinput(self): """Forget all pending, but not yet processed input.""" - while pygame.event.poll().type <> NOEVENT: + while pygame.event.poll().type != NOEVENT: pass def getpending(self): @@ -299,7 +300,7 @@ def getpending(self): def wait(self): """Wait for an event.""" - raise Exception, "erp!" + raise Exception("erp!") def repaint(self): # perhaps we should consolidate grobs? diff --git a/pyrepl/pygame_keymap.py b/pyrepl/pygame_keymap.py index 5531f1c..3eedc7d 100644 --- a/pyrepl/pygame_keymap.py +++ b/pyrepl/pygame_keymap.py @@ -90,22 +90,22 @@ def _parse_key1(key, s): s += 2 elif c == "c": if key[s + 2] != '-': - raise KeySpecError, \ + raise KeySpecError( "\\C must be followed by `-' (char %d of %s)"%( - s + 2, repr(key)) + s + 2, repr(key))) if ctrl: - raise KeySpecError, "doubled \\C- (char %d of %s)"%( - s + 1, repr(key)) + raise KeySpecError("doubled \\C- (char %d of %s)"%( + s + 1, repr(key))) ctrl = 1 s += 3 elif c == "m": if key[s + 2] != '-': - raise KeySpecError, \ + raise KeySpecError( "\\M must be followed by `-' (char %d of %s)"%( - s + 2, repr(key)) + s + 2, repr(key))) if meta: - raise KeySpecError, "doubled \\M- (char %d of %s)"%( - s + 1, repr(key)) + raise KeySpecError("doubled \\M- (char %d of %s)"%( + s + 1, repr(key))) meta = 1 s += 3 elif c.isdigit(): @@ -119,22 +119,22 @@ def _parse_key1(key, s): elif c == '<': t = key.find('>', s) if t == -1: - raise KeySpecError, \ + raise KeySpecError( "unterminated \\< starting at char %d of %s"%( - s + 1, repr(key)) + s + 1, repr(key))) try: ret = _keynames[key[s+2:t].lower()] s = t + 1 except KeyError: - raise KeySpecError, \ + raise KeySpecError( "unrecognised keyname `%s' at char %d of %s"%( - key[s+2:t], s + 2, repr(key)) + key[s+2:t], s + 2, repr(key))) if ret is None: return None, s else: - raise KeySpecError, \ + raise KeySpecError( "unknown backslash escape %s at char %d of %s"%( - `c`, s + 2, repr(key)) + repr(c), s + 2, repr(key))) else: if ctrl: ret = chr(ord(key[s]) & 0x1f) # curses.ascii.ctrl() @@ -160,9 +160,9 @@ def _compile_keymap(keymap): r.setdefault(key[0], {})[key[1:]] = value for key, value in r.items(): if value.has_key(()): - if len(value) <> 1: - raise KeySpecError, \ - "key definitions for %s clash"%(value.values(),) + if len(value) != 1: + raise KeySpecError( + "key definitions for %s clash"%(value.values(),)) else: r[key] = value[()] else: @@ -202,7 +202,7 @@ def unparse_key(keyseq): return '' name, s = keyname(keyseq) if name: - if name <> 'escape' or s == len(keyseq): + if name != 'escape' or s == len(keyseq): return '\\<' + name + '>' + unparse_key(keyseq[s:]) else: return '\\M-' + unparse_key(keyseq[1:]) @@ -226,7 +226,7 @@ def _unparse_keyf(keyseq): return [] name, s = keyname(keyseq) if name: - if name <> 'escape' or s == len(keyseq): + if name != 'escape' or s == len(keyseq): return [name] + _unparse_keyf(keyseq[s:]) else: rest = _unparse_keyf(keyseq[1:]) diff --git a/pyrepl/python_reader.py b/pyrepl/python_reader.py index c3f4092..9e97a21 100644 --- a/pyrepl/python_reader.py +++ b/pyrepl/python_reader.py @@ -189,7 +189,7 @@ def execute(self, text): # ooh, look at the hack: code = self.compile(text, '', 'single') except (OverflowError, SyntaxError, ValueError): - self.showsyntaxerror("") + self.showsyntaxerror('') else: self.runcode(code) if sys.stdout and not sys.stdout.closed: diff --git a/pyrepl/reader.py b/pyrepl/reader.py index 587e3b1..a44e7f6 100644 --- a/pyrepl/reader.py +++ b/pyrepl/reader.py @@ -20,6 +20,7 @@ # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. from __future__ import unicode_literals +import re import unicodedata from pyrepl import commands from pyrepl import input @@ -28,34 +29,36 @@ except NameError: unicode = str unichr = chr - basestring = bytes, str +_r_csi_seq = re.compile(r"\033\[[ -@]*[A-~]") + def _make_unctrl_map(): uc_map = {} - for c in map(unichr, range(256)): + for i in range(256): + c = unichr(i) if unicodedata.category(c)[0] != 'C': - uc_map[c] = c + uc_map[i] = c for i in range(32): - c = unichr(i) - uc_map[c] = '^' + unichr(ord('A') + i - 1) - uc_map[b'\t'] = ' ' # display TABs as 4 characters - uc_map[b'\177'] = unicode('^?') + uc_map[i] = '^' + unichr(ord('A') + i - 1) + uc_map[ord(b'\t')] = ' ' # display TABs as 4 characters + uc_map[ord(b'\177')] = unicode('^?') for i in range(256): - c = unichr(i) - if c not in uc_map: - uc_map[c] = unicode('\\%03o') % i + if i not in uc_map: + uc_map[i] = unicode('\\%03o') % i return uc_map def _my_unctrl(c, u=_make_unctrl_map()): + # takes an integer, returns a unicode + assert isinstance(c, int) if c in u: return u[c] else: if unicodedata.category(c).startswith('C'): - return b'\u%04x' % ord(c) + return '\\u%04x' % c else: - return c + return c # XXX: does not "return a unicode"?! def disp_str(buffer, join=''.join, uc=_my_unctrl): @@ -72,7 +75,7 @@ def disp_str(buffer, join=''.join, uc=_my_unctrl): go higher as and when unicode support happens.""" # disp_str proved to be a bottleneck for large inputs, # so it needs to be rewritten in C; it's not required though. - s = [uc(x) for x in buffer] + s = [uc(ord(x)) for x in buffer] b = [] # XXX: bytearray for x in s: b.append(1) @@ -95,7 +98,7 @@ def make_default_syntax_table(): st = {} for c in map(unichr, range(256)): st[c] = SYNTAX_SYMBOL - for c in [a for a in map(unichr, range(256)) if a.isalpha()]: + for c in [a for a in map(unichr, range(256)) if a.isalnum()]: st[c] = SYNTAX_WORD st[unicode('\n')] = st[unicode(' ')] = SYNTAX_WHITESPACE return st @@ -143,11 +146,11 @@ def make_default_syntax_table(): (r'\M-8', 'digit-arg'), (r'\M-9', 'digit-arg'), #(r'\M-\n', 'insert-nl'), - ('\\\\', 'self-insert')] + + ('\\\\', 'self-insert')] + \ [(c, 'self-insert') - for c in map(chr, range(32, 127)) if c != '\\'] + + for c in map(chr, range(32, 127)) if c != '\\'] + \ [(c, 'self-insert') - for c in map(chr, range(128, 256)) if c.isalpha()] + + for c in map(chr, range(128, 256)) if c.isalpha()] + \ [(r'\', 'up'), (r'\', 'down'), (r'\', 'left'), @@ -235,6 +238,10 @@ class Reader(object): def __init__(self, console): self.buffer = [] + # Enable the use of `insert` without a `prepare` call - necessary to + # facilitate the tab completion hack implemented for + # . + self.pos = 0 self.ps1 = "->> " self.ps2 = "/>> " self.ps3 = "|.. " @@ -246,9 +253,9 @@ def __init__(self, console): self.commands = {} self.msg = '' for v in vars(commands).values(): - if (isinstance(v, type) and - issubclass(v, commands.Command) and - v.__name__[0].islower()): + if (isinstance(v, type) + and issubclass(v, commands.Command) + and v.__name__[0].islower()): self.commands[v.__name__] = v self.commands[v.__name__.replace('_', '-')] = v self.syntax_table = make_default_syntax_table() @@ -317,6 +324,10 @@ def process_prompt(self, prompt): excluded from the length calculation. So also a copy of the prompt is returned with these control characters removed. """ + # The logic below also ignores the length of common escape + # sequences if they were not explicitly within \x01...\x02. + # They are CSI (or ANSI) sequences ( ESC [ ... LETTER ) + out_prompt = '' l = len(prompt) pos = 0 @@ -328,10 +339,14 @@ def process_prompt(self, prompt): if e == -1: break # Found start and end brackets, subtract from string length - l = l - (e - s + 1) - out_prompt += prompt[pos:s] + prompt[s + 1:e] - pos = e + 1 - out_prompt += prompt[pos:] + l = l - (e-s+1) + keep = prompt[pos:s] + l -= sum(map(len, _r_csi_seq.findall(keep))) + out_prompt += keep + prompt[s+1:e] + pos = e+1 + keep = prompt[pos:] + l -= sum(map(len, _r_csi_seq.findall(keep))) + out_prompt += keep return out_prompt, l def bow(self, p=None): @@ -523,8 +538,7 @@ def refresh(self): def do_cmd(self, cmd): #print cmd - if isinstance(cmd[0], basestring): - #XXX: unify to text + if isinstance(cmd[0], (str, unicode)): cmd = self.commands.get(cmd[0], commands.invalid_command)(self, *cmd) elif isinstance(cmd[0], type): @@ -619,7 +633,7 @@ def bind(self, spec, command): def get_buffer(self, encoding=None): if encoding is None: encoding = self.console.encoding - return unicode('').join(self.buffer).encode(self.console.encoding) + return self.get_unicode().encode(encoding) def get_unicode(self): """Return the current buffer as a unicode string.""" diff --git a/pyrepl/readline.py b/pyrepl/readline.py index 3cfb054..14a7d82 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -34,13 +34,10 @@ from pyrepl.unix_console import UnixConsole, _error try: unicode - PY3 = False + PY2 = True except NameError: - PY3 = True + PY2 = False unicode = str - unichr = chr - basestring = bytes, str - ENCODING = sys.getfilesystemencoding() or 'latin1' # XXX review @@ -97,6 +94,13 @@ def get_stem(self): return ''.join(b[p+1:self.pos]) def get_completions(self, stem): + if len(stem) == 0 and self.more_lines is not None: + b = self.buffer + p = self.pos + while p > 0 and b[p - 1] != '\n': + p -= 1 + num_spaces = 4 - ((self.pos - p) % 4) + return [' ' * num_spaces] result = [] function = self.config.readline_completer if function is not None: @@ -144,12 +148,16 @@ def get_trimmed_history(self, maxlength): def collect_keymap(self): return super(ReadlineAlikeReader, self).collect_keymap() + ( - (r'\n', 'maybe-accept'),) + (r'\n', 'maybe-accept'), + (r'\', 'backspace-dedent'), + ) def __init__(self, console): super(ReadlineAlikeReader, self).__init__(console) self.commands['maybe_accept'] = maybe_accept self.commands['maybe-accept'] = maybe_accept + self.commands['backspace_dedent'] = backspace_dedent + self.commands['backspace-dedent'] = backspace_dedent def after_command(self, cmd): super(ReadlineAlikeReader, self).after_command(cmd) @@ -167,6 +175,27 @@ def after_command(self, cmd): if self.pos > len(self.buffer): self.pos = len(self.buffer) +def _get_this_line_indent(buffer, pos): + indent = 0 + while pos > 0 and buffer[pos - 1] in " \t": + indent += 1 + pos -= 1 + if pos > 0 and buffer[pos - 1] == "\n": + return indent + return 0 + +def _get_previous_line_indent(buffer, pos): + prevlinestart = pos + while prevlinestart > 0 and buffer[prevlinestart - 1] != "\n": + prevlinestart -= 1 + prevlinetext = prevlinestart + while prevlinetext < pos and buffer[prevlinetext] in " \t": + prevlinetext += 1 + if prevlinetext == pos: + indent = None + else: + indent = prevlinetext - prevlinestart + return prevlinestart, indent class maybe_accept(commands.Command): def do(self): @@ -176,13 +205,40 @@ def do(self): # if there are already several lines and the cursor # is not on the last one, always insert a new \n. text = r.get_unicode() - if "\n" in r.buffer[r.pos:]: - r.insert("\n") - elif r.more_lines is not None and r.more_lines(text): + if ("\n" in r.buffer[r.pos:] or + (r.more_lines is not None and r.more_lines(text))): + # + # auto-indent the next line like the previous line + prevlinestart, indent = _get_previous_line_indent(r.buffer, r.pos) r.insert("\n") + if indent: + for i in range(prevlinestart, prevlinestart + indent): + r.insert(r.buffer[i]) else: self.finish = 1 +class backspace_dedent(commands.Command): + def do(self): + r = self.reader + b = r.buffer + if r.pos > 0: + repeat = 1 + if b[r.pos - 1] != "\n": + indent = _get_this_line_indent(b, r.pos) + if indent > 0: + ls = r.pos - indent + while ls > 0: + ls, pi = _get_previous_line_indent(b, ls - 1) + if pi is not None and pi < indent: + repeat = indent - pi + break + r.pos -= repeat + del b[r.pos:r.pos + repeat] + r.dirty = 1 + else: + self.reader.error("can't backspace at start") + +# ____________________________________________________________ class _ReadlineWrapper(object): reader = None @@ -207,14 +263,8 @@ def raw_input(self, prompt=''): except _error: return _old_raw_input(prompt) reader.ps1 = prompt - - ret = reader.readline(startup_hook=self.startup_hook) - if not PY3: - return ret - - # Unicode/str is required for Python 3 (3.5.2). - # Ref: https://bitbucket.org/pypy/pyrepl/issues/20/#comment-30647029 - return unicode(ret, ENCODING) + return reader.readline(returns_unicode=True, + startup_hook=self.startup_hook) def multiline_input(self, more_lines, ps1, ps2, returns_unicode=False): """Read an input on possibly multiple lines, asking for more @@ -250,13 +300,9 @@ def get_completer_delims(self): def _histline(self, line): line = line.rstrip('\n') - if PY3: - return line - - try: - return unicode(line, ENCODING) - except UnicodeDecodeError: # bah, silently fall back... - return unicode(line, 'utf-8', 'replace') + if isinstance(line, unicode): + return line # on py3k + return unicode(line, 'utf-8', 'replace') def get_history_length(self): return self.saved_history_length @@ -273,7 +319,11 @@ def read_history_file(self, filename='~/.history'): # history item: we use \r\n instead of just \n. If the history # file is passed to GNU readline, the extra \r are just ignored. history = self.get_reader().history - f = open(os.path.expanduser(filename), 'r') + if PY2: + f = open(os.path.expanduser(filename), 'r') + else: + f = open(os.path.expanduser(filename), 'r', encoding='utf-8', + errors='replace') buffer = [] for line in f: if line.endswith('\r\n'): @@ -290,15 +340,21 @@ def read_history_file(self, filename='~/.history'): def write_history_file(self, filename='~/.history'): maxlength = self.saved_history_length history = self.get_reader().get_trimmed_history(maxlength) - f = open(os.path.expanduser(filename), 'w') + entries = '' for entry in history: - if isinstance(entry, unicode): - try: - entry = entry.encode(ENCODING) - except UnicodeEncodeError: # bah, silently fall back... - entry = entry.encode('utf-8') + # if we are on py3k, we don't need to encode strings before + # writing it to a file + if isinstance(entry, unicode) and PY2: + entry = entry.encode('utf-8') entry = entry.replace('\n', '\r\n') # multiline history support - f.write(entry + '\n') + entries += entry + '\n' + + fname = os.path.expanduser(filename) + if PY2: + f = open(fname, 'w') + else: + f = open(fname, 'w', encoding='utf-8') + f.write(entries) f.close() def clear_history(self): @@ -410,8 +466,8 @@ def stub(*args, **kwds): def _setup(): global _old_raw_input if _old_raw_input is not None: + # Don't run _setup twice. return - # don't run _setup twice try: f_in = sys.stdin.fileno() @@ -436,16 +492,16 @@ def _old_raw_input(prompt=''): del sys.__raw_input__ except AttributeError: pass - return raw_input(prompt) + return input(prompt) sys.__raw_input__ = _wrapper.raw_input else: # this is not really what readline.c does. Better than nothing I guess - try: + if sys.version_info < (3,): import __builtin__ _old_raw_input = __builtin__.raw_input __builtin__.raw_input = _wrapper.raw_input - except ImportError: + else: import builtins _old_raw_input = builtins.input builtins.input = _wrapper.raw_input diff --git a/pyrepl/simple_interact.py b/pyrepl/simple_interact.py index 3b84a15..48cf75a 100644 --- a/pyrepl/simple_interact.py +++ b/pyrepl/simple_interact.py @@ -26,7 +26,6 @@ import sys from pyrepl.readline import multiline_input, _error, _get_reader - def check(): # returns False if there is a problem initializing the state try: _get_reader() @@ -34,6 +33,15 @@ def check(): # returns False if there is a problem initializing the state return False return True +def _strip_final_indent(text): + # kill spaces and tabs at the end, but only if they follow '\n'. + # meant to remove the auto-indentation only (although it would of + # course also remove explicitly-added indentation). + short = text.rstrip(' \t') + n = len(short) + if n > 0 and text[n-1] == '\n': + return short + return text def run_multiline_interactive_console(mainmodule=None, future_flags=0): import code @@ -44,11 +52,11 @@ def run_multiline_interactive_console(mainmodule=None, future_flags=0): console.compile.compiler.flags |= future_flags def more_lines(unicodetext): - if sys.version_info < (3, ): - # ooh, look at the hack: - src = "#coding:utf-8\n"+unicodetext.encode('utf-8') + # ooh, look at the hack: + if sys.version_info < (3,): + src = "#coding:utf-8\n"+_strip_final_indent(unicodetext).encode('utf-8') else: - src = unicodetext + src = _strip_final_indent(unicodetext) try: code = console.compile(src, '', 'single') except (OverflowError, SyntaxError, ValueError): @@ -58,6 +66,10 @@ def more_lines(unicodetext): while 1: try: + try: + sys.stdout.flush() + except: + pass ps1 = getattr(sys, 'ps1', '>>> ') ps2 = getattr(sys, 'ps2', '... ') try: @@ -65,8 +77,11 @@ def more_lines(unicodetext): returns_unicode=True) except EOFError: break - more = console.push(statement) + more = console.push(_strip_final_indent(statement)) assert not more except KeyboardInterrupt: console.write("\nKeyboardInterrupt\n") console.resetbuffer() + except MemoryError: + console.write("\nMemoryError\n") + console.resetbuffer() diff --git a/pyrepl/unix_console.py b/pyrepl/unix_console.py index 1aded8c..cfa356d 100644 --- a/pyrepl/unix_console.py +++ b/pyrepl/unix_console.py @@ -34,6 +34,11 @@ from .console import Console, Event from .unix_eventqueue import EventQueue from .trace import trace +try: + from __pypy__ import pyos_inputhook +except ImportError: + def pyos_inputhook(): + pass class InvalidTerminal(RuntimeError): @@ -252,7 +257,6 @@ def __write_changed_line(self, y, oldline, newline, px): # reuse the oldline as much as possible, but stop as soon as we # encounter an ESCAPE, because it might be the start of an escape # sequene - #XXX unicode check! while x < minlen and oldline[x] == newline[x] and newline[x] != '\x1b': x += 1 if oldline[x:] == newline[x+1:] and self.ich1: @@ -286,7 +290,6 @@ def __write_changed_line(self, y, oldline, newline, px): self.__write(newline[x:]) self.__posxy = len(newline), y - #XXX: check for unicode mess if '\x1b' in newline: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor @@ -407,6 +410,7 @@ def get_event(self, block=1): while self.event_queue.empty(): while 1: # All hail Unix! + pyos_inputhook() try: self.push_char(os.read(self.input_fd, 1)) except (IOError, OSError) as err: @@ -534,7 +538,6 @@ def getpending(self): "i", ioctl(self.input_fd, FIONREAD, "\0\0\0\0"))[0] data = os.read(self.input_fd, amount) raw = unicode(data, self.encoding, 'replace') - #XXX: something is wrong here e.data += raw e.raw += raw return e @@ -550,7 +553,6 @@ def getpending(self): amount = 10000 data = os.read(self.input_fd, amount) raw = unicode(data, self.encoding, 'replace') - #XXX: something is wrong here e.data += raw e.raw += raw return e diff --git a/pyrepl/unix_eventqueue.py b/pyrepl/unix_eventqueue.py index 332d952..3a9c77d 100644 --- a/pyrepl/unix_eventqueue.py +++ b/pyrepl/unix_eventqueue.py @@ -119,7 +119,7 @@ def insert(self, event): self.events.append(event) def push(self, char): - ord_char = char if isinstance(char, int) else ord(char) + ord_char = ord(char) char = bytes(bytearray((ord_char,))) self.buf.append(ord_char) if char in self.k: diff --git a/testing/infrastructure.py b/testing/infrastructure.py index 51c6f3c..ca8784e 100644 --- a/testing/infrastructure.py +++ b/testing/infrastructure.py @@ -18,6 +18,9 @@ # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. from __future__ import print_function +from contextlib import contextmanager +import os + from pyrepl.reader import Reader from pyrepl.console import Console, Event @@ -55,8 +58,12 @@ def get_event(self, block=1): print("event", ev) return Event(*ev) + def getpending(self): + """Nothing pending, but do not return None here.""" + return Event('key', '', b'') + -class TestReader(Reader): +class BaseTestReader(Reader): def get_prompt(self, lineno, cursor_on_line): return '' @@ -66,8 +73,19 @@ def refresh(self): self.dirty = True -def read_spec(test_spec, reader_class=TestReader): +def read_spec(test_spec, reader_class=BaseTestReader): # remember to finish your test_spec with 'accept' or similar! con = TestConsole(test_spec, verbose=True) reader = reader_class(con) reader.readline() + + +@contextmanager +def sane_term(): + """Ensure a TERM that supports clear""" + old_term, os.environ['TERM'] = os.environ.get('TERM'), 'xterm' + yield + if old_term is not None: + os.environ['TERM'] = old_term + else: + del os.environ['TERM'] diff --git a/testing/test_basic.py b/testing/test_basic.py index 66d53ca..1c69636 100644 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -76,6 +76,8 @@ def test_yank_pop(): ( 'accept', ['cd '])]) +# interrupt uses os.kill which doesn't go through signal handlers on windows +@pytest.mark.skipif("os.name == 'nt'") def test_interrupt(): with pytest.raises(KeyboardInterrupt): read_spec([('interrupt', [''])]) diff --git a/testing/test_bugs.py b/testing/test_bugs.py index bc7367c..2c4fc7c 100644 --- a/testing/test_bugs.py +++ b/testing/test_bugs.py @@ -18,7 +18,7 @@ # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. from pyrepl.historical_reader import HistoricalReader -from .infrastructure import EA, TestReader, read_spec +from .infrastructure import EA, BaseTestReader, sane_term, read_spec # this test case should contain as-verbatim-as-possible versions of # (applicable) bug reports @@ -26,7 +26,7 @@ import pytest -class HistoricalTestReader(HistoricalReader, TestReader): +class HistoricalTestReader(HistoricalReader, BaseTestReader): pass @@ -46,6 +46,8 @@ def test_cmd_instantiation_crash(): read_spec(spec, HistoricalTestReader) +@pytest.mark.skipif("os.name != 'posix' or 'darwin' in sys.platform or " + "'kfreebsd' in sys.platform") def test_signal_failure(monkeypatch): import os import pty @@ -60,13 +62,14 @@ def really_failing_signal(a, b): mfd, sfd = pty.openpty() try: - c = UnixConsole(sfd, sfd) - c.prepare() - c.restore() - monkeypatch.setattr(signal, 'signal', failing_signal) - c.prepare() - monkeypatch.setattr(signal, 'signal', really_failing_signal) - c.restore() + with sane_term(): + c = UnixConsole(sfd, sfd) + c.prepare() + c.restore() + monkeypatch.setattr(signal, 'signal', failing_signal) + c.prepare() + monkeypatch.setattr(signal, 'signal', really_failing_signal) + c.restore() finally: os.close(mfd) os.close(sfd) diff --git a/testing/test_functional.py b/testing/test_functional.py index 1e491ec..7ed65a2 100644 --- a/testing/test_functional.py +++ b/testing/test_functional.py @@ -7,17 +7,16 @@ import sys -@pytest.fixture -def child(request): +@pytest.fixture() +def child(): try: - pexpect = pytest.importorskip('pexpect') + import pexpect + except ImportError: + pytest.skip("no pexpect module") except SyntaxError: pytest.skip('pexpect wont work on py3k') child = pexpect.spawn(sys.executable, ['-S'], timeout=10) - if sys.version_info >= (3, ): - child.logfile = sys.stdout.buffer - else: - child.logfile = sys.stdout + child.logfile = sys.stdout child.sendline('from pyrepl.python_reader import main') child.sendline('main()') return child diff --git a/testing/test_reader.py b/testing/test_reader.py new file mode 100644 index 0000000..4b93ffa --- /dev/null +++ b/testing/test_reader.py @@ -0,0 +1,9 @@ + +def test_process_prompt(): + from pyrepl.reader import Reader + r = Reader(None) + assert r.process_prompt("hi!") == ("hi!", 3) + assert r.process_prompt("h\x01i\x02!") == ("hi!", 2) + assert r.process_prompt("hi\033[11m!") == ("hi\033[11m!", 3) + assert r.process_prompt("h\x01i\033[11m!\x02") == ("hi\033[11m!", 1) + assert r.process_prompt("h\033[11m\x01i\x02!") == ("h\033[11mi!", 2) diff --git a/testing/test_readline.py b/testing/test_readline.py index 339ff44..21b26bd 100644 --- a/testing/test_readline.py +++ b/testing/test_readline.py @@ -5,12 +5,7 @@ import pytest from pyrepl.readline import _ReadlineWrapper - -@pytest.fixture -def readline_wrapper(): - master, slave = pty.openpty() - return _ReadlineWrapper(slave, slave) - +from .infrastructure import sane_term if sys.version_info < (3, ): bytes_type = str @@ -20,6 +15,12 @@ def readline_wrapper(): unicode_type = str +@pytest.fixture +def readline_wrapper(): + master, slave = pty.openpty() + return _ReadlineWrapper(slave, slave) + + def test_readline(): master, slave = pty.openpty() readline_wrapper = _ReadlineWrapper(slave, slave) @@ -40,18 +41,17 @@ def test_readline_returns_unicode(): assert isinstance(result, unicode_type) +@pytest.mark.skipif("os.name != 'posix' or 'darwin' in sys.platform or " + "'freebsd' in sys.platform") def test_raw_input(): master, slave = pty.openpty() readline_wrapper = _ReadlineWrapper(slave, slave) os.write(master, b'input\n') - result = readline_wrapper.raw_input('prompt:') - if sys.version_info < (3, ): - assert result == b'input' - assert isinstance(result, bytes_type) - else: - assert result == 'input' - assert isinstance(result, unicode_type) + with sane_term(): + result = readline_wrapper.raw_input('prompt:') + assert result == 'input' + assert isinstance(result, unicode_type) def test_read_history_file(readline_wrapper, tmp_path): @@ -66,3 +66,40 @@ def test_read_history_file(readline_wrapper, tmp_path): histfile.write_bytes(b"foo\nbar\n") readline_wrapper.read_history_file(str(histfile)) assert readline_wrapper.reader.history == ["foo", "bar"] + + +def test_write_history_file(readline_wrapper, tmp_path): + histfile = tmp_path / "history" + + reader = readline_wrapper.get_reader() + history = reader.history + assert history == [] + history.extend(["foo", "bar"]) + + readline_wrapper.write_history_file(str(histfile)) + + assert open(str(histfile), "r").readlines() == ["foo\n", "bar\n"] + + +def test_write_history_file_with_exception(readline_wrapper, tmp_path): + """The history file should not get nuked on inner exceptions. + + This was the case with unicode decoding previously.""" + histfile = tmp_path / "history" + histfile.write_bytes(b"foo\nbar\n") + + class BadEntryException(Exception): + pass + + class BadEntry(object): + @classmethod + def replace(cls, *args): + raise BadEntryException + + history = readline_wrapper.get_reader().history + history.extend([BadEntry]) + + with pytest.raises(BadEntryException): + readline_wrapper.write_history_file(str(histfile)) + + assert open(str(histfile), "r").readlines() == ["foo\n", "bar\n"] diff --git a/testing/test_unix_reader.py b/testing/test_unix_reader.py index 9fbcb2c..6611a3e 100644 --- a/testing/test_unix_reader.py +++ b/testing/test_unix_reader.py @@ -7,8 +7,8 @@ def test_simple(): a = u'\u1234' b = a.encode('utf-8') - for c in b: - q.push(c) + for c in bytearray(a, 'utf-8'): + q.push(chr(c)) event = q.get() assert q.get() is None @@ -18,8 +18,8 @@ def test_simple(): def test_propagate_escape(): def send(keys): - for c in keys: - q.push(c) + for c in bytearray(keys): + q.push(chr(c)) events = [] while True: @@ -27,7 +27,7 @@ def send(keys): if event is None: break events.append(event) - return events + return events keymap = { b'\033': {b'U': 'up', b'D': 'down'}, diff --git a/testing/test_wishes.py b/testing/test_wishes.py index 650dff7..d0c1e6f 100644 --- a/testing/test_wishes.py +++ b/testing/test_wishes.py @@ -27,5 +27,5 @@ def test_quoted_insert_repeat(): read_spec([ (('digit-arg', '3'), ['']), (('quoted-insert', None), ['']), - (('self-insert', '\033'), ['^[^[^[']), + (('key', '\033'), ['^[^[^[']), (('accept', None), None)])