From f620a2fac2cb2c56fcb7e7dc7db02b83ca048b07 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 10 Mar 2019 20:31:54 +0100 Subject: [PATCH 01/31] Copy pyrepl from PyPy pypy/lib_pypy/pyrepl => pyrepl pypy/extra_tests/test_pyrepl => testing py3.6 branch Ref: https://bitbucket.org/pypy/pypy/issues/2971 --- pyrepl/_minimal_curses.py | 15 +-- pyrepl/cmdrepl.py | 53 +++++------ pyrepl/commands.py | 7 +- pyrepl/completer.py | 7 +- pyrepl/completing_reader.py | 78 +++++++--------- pyrepl/console.py | 10 +- pyrepl/copy_code.py | 73 +++++++++++++++ pyrepl/curses.py | 23 ++++- pyrepl/historical_reader.py | 3 +- pyrepl/input.py | 18 ++-- pyrepl/keymap.py | 2 - pyrepl/keymaps.py | 4 +- pyrepl/module_lister.py | 4 +- pyrepl/pygame_console.py | 7 +- pyrepl/pygame_keymap.py | 38 ++++---- pyrepl/python_reader.py | 2 +- pyrepl/reader.py | 70 ++++++++------ pyrepl/readline.py | 180 ++++++++++++++++++++++-------------- pyrepl/simple_interact.py | 27 ++++-- pyrepl/unicodedata_.py | 59 ++++++++++++ pyrepl/unix_console.py | 145 +++++++++++++++-------------- pyrepl/unix_eventqueue.py | 85 ++++++----------- testing/__init__.py | 1 + testing/conftest.py | 8 ++ testing/infrastructure.py | 18 +++- testing/test_basic.py | 2 + testing/test_bugs.py | 21 +++-- testing/test_functional.py | 13 ++- testing/test_reader.py | 9 ++ testing/test_readline.py | 70 +++----------- testing/test_unix_reader.py | 47 ---------- 31 files changed, 607 insertions(+), 492 deletions(-) create mode 100644 pyrepl/copy_code.py create mode 100644 pyrepl/unicodedata_.py create mode 100644 testing/conftest.py create mode 100644 testing/test_reader.py delete mode 100644 testing/test_unix_reader.py diff --git a/pyrepl/_minimal_curses.py b/pyrepl/_minimal_curses.py index 6dade44..7aa5abf 100644 --- a/pyrepl/_minimal_curses.py +++ b/pyrepl/_minimal_curses.py @@ -9,15 +9,14 @@ hide this one if compiled in. """ -import ctypes.util - +import ctypes, ctypes.util class error(Exception): pass def _find_clib(): - trylibs = ['ncursesw', 'ncurses', 'curses'] + trylibs = ['ncurses', 'curses'] for lib in trylibs: path = ctypes.util.find_library(lib) @@ -43,12 +42,8 @@ def _find_clib(): # ____________________________________________________________ -try: - from __pypy__ import builtinify - builtinify # silence broken pyflakes -except ImportError: - builtinify = lambda f: f - +try: from __pypy__ import builtinify +except ImportError: builtinify = lambda f: f @builtinify def setupterm(termstr, fd): @@ -57,7 +52,6 @@ def setupterm(termstr, fd): if result == ERR: raise error("setupterm() failed (err=%d)" % err.value) - @builtinify def tigetstr(cap): if not isinstance(cap, bytes): @@ -67,7 +61,6 @@ def tigetstr(cap): 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) diff --git a/pyrepl/cmdrepl.py b/pyrepl/cmdrepl.py index 8b500b0..ce3eb23 100644 --- a/pyrepl/cmdrepl.py +++ b/pyrepl/cmdrepl.py @@ -35,28 +35,26 @@ from __future__ import print_function -from pyrepl import completer +from pyrepl import completing_reader as cr, reader, completer from pyrepl.completing_reader import CompletingReader as CR import cmd - class CmdReader(CR): def collect_keymap(self): return super(CmdReader, self).collect_keymap() + ( ("\\M-\\n", "invalid-key"), ("\\n", "accept")) - + + CR_init = CR.__init__ def __init__(self, completions): - super(CmdReader, self).__init__() + self.CR_init(self) self.completions = completions def get_completions(self, stem): if len(stem) != self.pos: return [] - return sorted(set(s - for s in self.completions - if s.startswith(stem))) - + return sorted(set(s for s in self.completions + if s.startswith(stem))) def replize(klass, history_across_invocations=1): @@ -73,25 +71,26 @@ def replize(klass, history_across_invocations=1): for s in completer.get_class_members(klass) if s.startswith("do_")] - assert issubclass(klass, cmd.Cmd) + if not issubclass(klass, cmd.Cmd): + raise Exception # if klass.cmdloop.im_class is not cmd.Cmd: # print "this may not work" - class MultiHist(object): - __history = [] - - def __init__(self, *args, **kw): - super(MultiHist, self).__init__(*args, **kw) - self.__reader = CmdReader(completions) - self.__reader.history = self.__history - self.__reader.historyi = len(self.__history) - - class SimpleHist(object): - def __init__(self, *args, **kw): - super(SimpleHist, self).__init__(*args, **kw) - self.__reader = CmdReader(completions) - - class CmdLoopMixin(object): + class CmdRepl(klass): + k_init = klass.__init__ + + if history_across_invocations: + _CmdRepl__history = [] + def __init__(self, *args, **kw): + self.k_init(*args, **kw) + self.__reader = CmdReader(completions) + self.__reader.history = CmdRepl._CmdRepl__history + self.__reader.historyi = len(CmdRepl._CmdRepl__history) + else: + def __init__(self, *args, **kw): + self.k_init(*args, **kw) + self.__reader = CmdReader(completions) + def cmdloop(self, intro=None): self.preloop() if intro is not None: @@ -114,8 +113,6 @@ def cmdloop(self, intro=None): stop = self.postcmd(stop, line) self.postloop() - hist = MultiHist if history_across_invocations else SimpleHist - - class CmdRepl(hist, CmdLoopMixin, klass): - __name__ = "replize(%s.%s)" % (klass.__module__, klass.__name__) + CmdRepl.__name__ = "replize(%s.%s)"%(klass.__module__, klass.__name__) return CmdRepl + diff --git a/pyrepl/commands.py b/pyrepl/commands.py index af71c70..0cd87fd 100644 --- a/pyrepl/commands.py +++ b/pyrepl/commands.py @@ -370,7 +370,7 @@ def do(self): class qIHelp(Command): def do(self): r = self.reader - r.insert((bytes(self.event) + r.console.getpending().data) * r.get_arg()) + r.insert((self.event + r.console.getpending().data) * r.get_arg()) r.pop_input_trans() from pyrepl import input @@ -384,4 +384,7 @@ def get(self): class quoted_insert(Command): kills_digit_arg = 0 def do(self): - self.reader.push_input_trans(QITrans()) + # XXX in Python 3, processing insert/C-q/C-v keys crashes + # because of a mixture of str and bytes. Disable these keys. + pass + #self.reader.push_input_trans(QITrans()) 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..bcdea19 100644 --- a/pyrepl/completing_reader.py +++ b/pyrepl/completing_reader.py @@ -18,12 +18,11 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import re from pyrepl import commands, reader from pyrepl.reader import Reader -def prefix(wordlist, j=0): +def prefix(wordlist, j = 0): d = {} i = j try: @@ -37,17 +36,14 @@ def prefix(wordlist, j=0): except IndexError: return wordlist[0][j:i] - -STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]") - +import re def stripcolor(s): - return STRIPCOLOR_REGEX.sub('', s) - + return stripcolor.regexp.sub('', s) +stripcolor.regexp = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]") def real_len(s): return len(stripcolor(s)) - def left_align(s, maxlen): stripped = stripcolor(s) if len(stripped) > maxlen: @@ -56,7 +52,6 @@ def left_align(s, maxlen): padding = maxlen - len(stripped) return s + ' '*padding - def build_menu(cons, wordlist, start, use_brackets, sort_in_column): if use_brackets: item = "[ %s ]" @@ -65,20 +60,20 @@ 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 # A B C A D G - # D E F B E + # D E F B E # G C F # # "fill" the table with empty words, so we always have the same amout # of rows for each column missing = cols*rows - len(wordlist) wordlist = wordlist + ['']*missing - indexes = [(i % cols) * rows + i // cols for i in range(len(wordlist))] + indexes = [(i%cols)*rows + i//cols for i in range(len(wordlist))] wordlist = [wordlist[i] for i in indexes] menu = [] i = start @@ -89,14 +84,14 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): i += 1 if i >= len(wordlist): break - menu.append(''.join(row)) + menu.append( ''.join(row) ) if i >= len(wordlist): i = 0 break if r + 5 > cons.height: - menu.append(" %d more... " % (len(wordlist) - i)) + menu.append(" %d more... "%(len(wordlist) - i)) break - return menu, i + return menu, i # this gets somewhat user interface-y, and as a result the logic gets # very convoluted. @@ -123,7 +118,7 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): # only if the ``assume_immutable_completions`` is True. # # now it gets complicated. -# +# # for the first press of a completion key: # if there's a common prefix, stick it in. @@ -145,22 +140,22 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): # for subsequent bangs, rotate the menu around (if there are sufficient # choices). - class complete(commands.Command): def do(self): r = self.reader - last_is_completer = r.last_command_is(self.__class__) - immutable_completions = r.assume_immutable_completions - completions_unchangable = last_is_completer and immutable_completions stem = r.get_stem() - if not completions_unchangable: - r.cmpltn_menu_choices = r.get_completions(stem) - - completions = r.cmpltn_menu_choices - if not completions: + if r.assume_immutable_completions and \ + r.last_command_is(self.__class__): + completions = r.cmpltn_menu_choices + else: + r.cmpltn_menu_choices = completions = \ + r.get_completions(stem) + if len(completions) == 0: r.error("no matches") elif len(completions) == 1: - if completions_unchangable and len(completions[0]) == len(stem): + if r.assume_immutable_completions and \ + len(completions[0]) == len(stem) and \ + r.last_command_is(self.__class__): r.msg = "[ sole completion ]" r.dirty = 1 r.insert(completions[0][len(stem):]) @@ -168,7 +163,7 @@ def do(self): p = prefix(completions, len(stem)) if p: r.insert(p) - if last_is_completer: + if r.last_command_is(self.__class__): if not r.cmpltn_menu_vis: r.cmpltn_menu_vis = 1 r.cmpltn_menu, r.cmpltn_menu_end = build_menu( @@ -182,7 +177,6 @@ def do(self): r.msg = "[ not unique ]" r.dirty = 1 - class self_insert(commands.self_insert): def do(self): commands.self_insert.do(self) @@ -201,7 +195,6 @@ def do(self): else: r.cmpltn_reset() - class CompletingReader(Reader): """Adds completion support @@ -211,25 +204,26 @@ class CompletingReader(Reader): """ # see the comment for the complete command assume_immutable_completions = True - use_brackets = True # display completions inside [] + use_brackets = True # display completions inside [] sort_in_column = False - + def collect_keymap(self): return super(CompletingReader, self).collect_keymap() + ( (r'\t', 'complete'),) - + def __init__(self, console): super(CompletingReader, self).__init__(console) self.cmpltn_menu = ["[ menu 1 ]", "[ menu 2 ]"] self.cmpltn_menu_vis = 0 self.cmpltn_menu_end = 0 - for c in (complete, self_insert): + for c in [complete, self_insert]: self.commands[c.__name__] = c - self.commands[c.__name__.replace('_', '-')] = c + self.commands[c.__name__.replace('_', '-')] = c def after_command(self, cmd): super(CompletingReader, self).after_command(cmd) - if not isinstance(cmd, (complete, self_insert)): + if not isinstance(cmd, self.commands['complete']) \ + and not isinstance(cmd, self.commands['self_insert']): self.cmpltn_reset() def calc_screen(self): @@ -249,7 +243,7 @@ def cmpltn_reset(self): self.cmpltn_menu = [] self.cmpltn_menu_vis = 0 self.cmpltn_menu_end = 0 - self.cmpltn_menu_choices = [] + self.cmpltn_menu_choices = [] def get_stem(self): st = self.syntax_table @@ -263,14 +257,11 @@ def get_stem(self): def get_completions(self, stem): return [] - def test(): class TestReader(CompletingReader): def get_completions(self, stem): - return [s for l in self.history - for s in l.split() - if s and s.startswith(stem)] - + return [s for l in map(lambda x:x.split(),self.history) + for s in l if s and s.startswith(stem)] reader = TestReader() reader.ps1 = "c**> " reader.ps2 = "c/*> " @@ -279,6 +270,5 @@ def get_completions(self, stem): while reader.readline(): pass - -if __name__ == '__main__': +if __name__=='__main__': test() diff --git a/pyrepl/console.py b/pyrepl/console.py index cfbcbe9..2891be7 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' @@ -28,12 +27,7 @@ def __init__(self, evt, data, raw=''): self.raw = raw def __repr__(self): - return 'Event(%r, %r)' % (self.evt, self.data) - - def __eq__(self, other): - return (self.evt == other.evt and - self.data == other.data and - self.raw == other.raw) + return 'Event(%r, %r)'%(self.evt, self.data) class Console(object): """Attributes: @@ -42,7 +36,7 @@ class Console(object): height, width, """ - + def refresh(self, screen, xy): pass diff --git a/pyrepl/copy_code.py b/pyrepl/copy_code.py new file mode 100644 index 0000000..9a0ec63 --- /dev/null +++ b/pyrepl/copy_code.py @@ -0,0 +1,73 @@ +# Copyright 2000-2004 Michael Hudson-Doyle +# +# All Rights Reserved +# +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose is hereby granted without fee, +# provided that the above copyright notice appear in all copies and +# that both that copyright notice and this permission notice appear in +# supporting documentation. +# +# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO +# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, +# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from types import CodeType + +def copy_code_with_changes(codeobject, + argcount=None, + nlocals=None, + stacksize=None, + flags=None, + code=None, + consts=None, + names=None, + varnames=None, + filename=None, + name=None, + firstlineno=None, + lnotab=None): + if argcount is None: argcount = codeobject.co_argcount + if nlocals is None: nlocals = codeobject.co_nlocals + if stacksize is None: stacksize = codeobject.co_stacksize + if flags is None: flags = codeobject.co_flags + if code is None: code = codeobject.co_code + if consts is None: consts = codeobject.co_consts + if names is None: names = codeobject.co_names + if varnames is None: varnames = codeobject.co_varnames + if filename is None: filename = codeobject.co_filename + if name is None: name = codeobject.co_name + if firstlineno is None: firstlineno = codeobject.co_firstlineno + if lnotab is None: lnotab = codeobject.co_lnotab + return CodeType(argcount, + nlocals, + stacksize, + flags, + code, + consts, + names, + varnames, + filename, + name, + firstlineno, + lnotab) + +code_attrs=['argcount', + 'nlocals', + 'stacksize', + 'flags', + 'code', + 'consts', + 'names', + 'varnames', + 'filename', + 'name', + 'firstlineno', + 'lnotab'] + + 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/input.py b/pyrepl/input.py index e92adbd..47e22ca 100644 --- a/pyrepl/input.py +++ b/pyrepl/input.py @@ -35,8 +35,6 @@ from __future__ import print_function import unicodedata from collections import deque -import pprint -from .trace import trace class InputTranslator(object): @@ -61,23 +59,26 @@ def __init__(self, keymap, verbose=0, for keyspec, command in keymap: keyseq = tuple(parse_keys(keyspec)) d[keyseq] = command - if verbose: - trace('[input] keymap: {}', pprint.pformat(d)) + if self.verbose: + print(d) self.k = self.ck = compile_keymap(d, ()) self.results = deque() self.stack = [] def push(self, evt): - trace("[input] pushed {!r}", evt.data) + if self.verbose: + print("pushed", evt.data, end='') key = evt.data d = self.k.get(key) if isinstance(d, dict): - trace("[input] transition") + if self.verbose: + print("transition") self.stack.append(key) self.k = d else: if d is None: - trace("[input] invalid") + if self.verbose: + print("invalid") if self.stack or len(key) > 1 or unicodedata.category(key) == 'C': self.results.append( (self.invalid_cls, self.stack + [key])) @@ -87,7 +88,8 @@ def push(self, evt): self.results.append( (self.character_cls, [key])) else: - trace("[input] matched {}", d) + if self.verbose: + print("matched", d) self.results.append((d, self.stack + [key])) self.stack = [] self.k = self.ck diff --git a/pyrepl/keymap.py b/pyrepl/keymap.py index 901f7ff..187c539 100644 --- a/pyrepl/keymap.py +++ b/pyrepl/keymap.py @@ -89,8 +89,6 @@ 'space': ' ', 'tab': '\t', 'up': 'up', - 'ctrl left': 'ctrl left', - 'ctrl right': 'ctrl right', } class KeySpecError(Exception): 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..32db42a 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,35 +29,41 @@ 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 if c in u: return u[c] else: if unicodedata.category(c).startswith('C'): - return b'\u%04x' % ord(c) + return r'\u%04x' % ord(c) else: return c +if 'a'[0] == b'a': + # When running tests with python2, bytes characters are bytes. + def _my_unctrl(c, uc=_my_unctrl): + return uc(ord(c)) + def disp_str(buffer, join=''.join, uc=_my_unctrl): """ disp_str(buffer:string) -> (string, [int]) @@ -95,7 +102,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 +150,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'), @@ -162,8 +169,6 @@ def make_default_syntax_table(): (r'\EOF', 'end'), # the entries in the terminfo database for xterms (r'\EOH', 'home'), # seem to be wrong. this is a less than ideal # workaround - (r'\', 'backward-word'), - (r'\', 'forward-word'), ]) if 'c' in globals(): # only on python 2.x @@ -235,6 +240,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 +255,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 +326,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 +341,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 +540,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 +635,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..593297a 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -26,61 +26,54 @@ extensions for multiline input. """ -import sys -import os +import sys, os from pyrepl import commands from pyrepl.historical_reader import HistoricalReader from pyrepl.completing_reader import CompletingReader from pyrepl.unix_console import UnixConsole, _error + try: unicode - PY3 = False except NameError: - PY3 = True unicode = str - unichr = chr - basestring = bytes, str - ENCODING = sys.getfilesystemencoding() or 'latin1' # XXX review -__all__ = [ - 'add_history', - 'clear_history', - 'get_begidx', - 'get_completer', - 'get_completer_delims', - 'get_current_history_length', - 'get_endidx', - 'get_history_item', - 'get_history_length', - 'get_line_buffer', - 'insert_text', - 'parse_and_bind', - 'read_history_file', - 'read_init_file', - 'redisplay', - 'remove_history_item', - 'replace_history_item', - 'set_completer', - 'set_completer_delims', - 'set_history_length', - 'set_pre_input_hook', - 'set_startup_hook', - 'write_history_file', - # ---- multiline extensions ---- - 'multiline_input', -] +__all__ = ['add_history', + 'clear_history', + 'get_begidx', + 'get_completer', + 'get_completer_delims', + 'get_current_history_length', + 'get_endidx', + 'get_history_item', + 'get_history_length', + 'get_line_buffer', + 'insert_text', + 'parse_and_bind', + 'read_history_file', + 'read_init_file', + 'redisplay', + 'remove_history_item', + 'replace_history_item', + 'set_completer', + 'set_completer_delims', + 'set_history_length', + 'set_pre_input_hook', + 'set_startup_hook', + 'write_history_file', + # ---- multiline extensions ---- + 'multiline_input', + ] # ____________________________________________________________ - class ReadlineConfig(object): readline_completer = None completer_delims = dict.fromkeys(' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?') - class ReadlineAlikeReader(HistoricalReader, CompletingReader): + assume_immutable_completions = False use_brackets = False sort_in_column = True @@ -97,6 +90,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 +144,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,22 +171,70 @@ 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): r = self.reader - r.dirty = 1 # this is needed to hide the completion menu, if visible + r.dirty = 1 # this is needed to hide the completion menu, if visible # # 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 +259,7 @@ 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(reader, 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 +295,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 +314,8 @@ 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') + f = open(os.path.expanduser(filename), 'r', encoding='utf-8', + errors='replace') buffer = [] for line in f: if line.endswith('\r\n'): @@ -290,13 +332,12 @@ 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') + f = open(os.path.expanduser(filename), 'w', encoding='utf-8') 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 sys.version_info < (3,): + entry = entry.encode('utf-8') entry = entry.replace('\n', '\r\n') # multiline history support f.write(entry + '\n') f.close() @@ -390,7 +431,6 @@ def insert_text(self, text): # ____________________________________________________________ # Stubs - def _make_stub(_name, _ret): def stub(*args, **kwds): import warnings @@ -402,16 +442,16 @@ def stub(*args, **kwds): ('read_init_file', None), ('redisplay', None), ('set_pre_input_hook', None), -]: + ]: assert _name not in globals(), _name _make_stub(_name, _ret) +# ____________________________________________________________ def _setup(): global _old_raw_input if _old_raw_input is not None: - return - # don't run _setup twice + return # don't run _setup twice try: f_in = sys.stdin.fileno() @@ -436,16 +476,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/unicodedata_.py b/pyrepl/unicodedata_.py new file mode 100644 index 0000000..04e6c97 --- /dev/null +++ b/pyrepl/unicodedata_.py @@ -0,0 +1,59 @@ +try: + from unicodedata import * +except ImportError: + + def category(ch): + """ + ASCII only implementation + """ + if type(ch) is not unicode: + raise TypeError + if len(ch) != 1: + raise TypeError + return _categories.get(ord(ch), 'Co') # "Other, private use" + + _categories = { + 0: 'Cc', 1: 'Cc', 2: 'Cc', 3: 'Cc', 4: 'Cc', 5: 'Cc', + 6: 'Cc', 7: 'Cc', 8: 'Cc', 9: 'Cc', 10: 'Cc', 11: 'Cc', + 12: 'Cc', 13: 'Cc', 14: 'Cc', 15: 'Cc', 16: 'Cc', 17: 'Cc', + 18: 'Cc', 19: 'Cc', 20: 'Cc', 21: 'Cc', 22: 'Cc', 23: 'Cc', + 24: 'Cc', 25: 'Cc', 26: 'Cc', 27: 'Cc', 28: 'Cc', 29: 'Cc', + 30: 'Cc', 31: 'Cc', 32: 'Zs', 33: 'Po', 34: 'Po', 35: 'Po', + 36: 'Sc', 37: 'Po', 38: 'Po', 39: 'Po', 40: 'Ps', 41: 'Pe', + 42: 'Po', 43: 'Sm', 44: 'Po', 45: 'Pd', 46: 'Po', 47: 'Po', + 48: 'Nd', 49: 'Nd', 50: 'Nd', 51: 'Nd', 52: 'Nd', 53: 'Nd', + 54: 'Nd', 55: 'Nd', 56: 'Nd', 57: 'Nd', 58: 'Po', 59: 'Po', + 60: 'Sm', 61: 'Sm', 62: 'Sm', 63: 'Po', 64: 'Po', 65: 'Lu', + 66: 'Lu', 67: 'Lu', 68: 'Lu', 69: 'Lu', 70: 'Lu', 71: 'Lu', + 72: 'Lu', 73: 'Lu', 74: 'Lu', 75: 'Lu', 76: 'Lu', 77: 'Lu', + 78: 'Lu', 79: 'Lu', 80: 'Lu', 81: 'Lu', 82: 'Lu', 83: 'Lu', + 84: 'Lu', 85: 'Lu', 86: 'Lu', 87: 'Lu', 88: 'Lu', 89: 'Lu', + 90: 'Lu', 91: 'Ps', 92: 'Po', 93: 'Pe', 94: 'Sk', 95: 'Pc', + 96: 'Sk', 97: 'Ll', 98: 'Ll', 99: 'Ll', 100: 'Ll', 101: 'Ll', + 102: 'Ll', 103: 'Ll', 104: 'Ll', 105: 'Ll', 106: 'Ll', 107: 'Ll', + 108: 'Ll', 109: 'Ll', 110: 'Ll', 111: 'Ll', 112: 'Ll', 113: 'Ll', + 114: 'Ll', 115: 'Ll', 116: 'Ll', 117: 'Ll', 118: 'Ll', 119: 'Ll', + 120: 'Ll', 121: 'Ll', 122: 'Ll', 123: 'Ps', 124: 'Sm', 125: 'Pe', + 126: 'Sm', 127: 'Cc', 128: 'Cc', 129: 'Cc', 130: 'Cc', 131: 'Cc', + 132: 'Cc', 133: 'Cc', 134: 'Cc', 135: 'Cc', 136: 'Cc', 137: 'Cc', + 138: 'Cc', 139: 'Cc', 140: 'Cc', 141: 'Cc', 142: 'Cc', 143: 'Cc', + 144: 'Cc', 145: 'Cc', 146: 'Cc', 147: 'Cc', 148: 'Cc', 149: 'Cc', + 150: 'Cc', 151: 'Cc', 152: 'Cc', 153: 'Cc', 154: 'Cc', 155: 'Cc', + 156: 'Cc', 157: 'Cc', 158: 'Cc', 159: 'Cc', 160: 'Zs', 161: 'Po', + 162: 'Sc', 163: 'Sc', 164: 'Sc', 165: 'Sc', 166: 'So', 167: 'So', + 168: 'Sk', 169: 'So', 170: 'Ll', 171: 'Pi', 172: 'Sm', 173: 'Cf', + 174: 'So', 175: 'Sk', 176: 'So', 177: 'Sm', 178: 'No', 179: 'No', + 180: 'Sk', 181: 'Ll', 182: 'So', 183: 'Po', 184: 'Sk', 185: 'No', + 186: 'Ll', 187: 'Pf', 188: 'No', 189: 'No', 190: 'No', 191: 'Po', + 192: 'Lu', 193: 'Lu', 194: 'Lu', 195: 'Lu', 196: 'Lu', 197: 'Lu', + 198: 'Lu', 199: 'Lu', 200: 'Lu', 201: 'Lu', 202: 'Lu', 203: 'Lu', + 204: 'Lu', 205: 'Lu', 206: 'Lu', 207: 'Lu', 208: 'Lu', 209: 'Lu', + 210: 'Lu', 211: 'Lu', 212: 'Lu', 213: 'Lu', 214: 'Lu', 215: 'Sm', + 216: 'Lu', 217: 'Lu', 218: 'Lu', 219: 'Lu', 220: 'Lu', 221: 'Lu', + 222: 'Lu', 223: 'Ll', 224: 'Ll', 225: 'Ll', 226: 'Ll', 227: 'Ll', + 228: 'Ll', 229: 'Ll', 230: 'Ll', 231: 'Ll', 232: 'Ll', 233: 'Ll', + 234: 'Ll', 235: 'Ll', 236: 'Ll', 237: 'Ll', 238: 'Ll', 239: 'Ll', + 240: 'Ll', 241: 'Ll', 242: 'Ll', 243: 'Ll', 244: 'Ll', 245: 'Ll', + 246: 'Ll', 247: 'Sm', 248: 'Ll', 249: 'Ll', 250: 'Ll', 251: 'Ll', + 252: 'Ll', 253: 'Ll', 254: 'Ll' + } diff --git a/pyrepl/unix_console.py b/pyrepl/unix_console.py index 1aded8c..ff228d0 100644 --- a/pyrepl/unix_console.py +++ b/pyrepl/unix_console.py @@ -19,21 +19,19 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import termios -import select -import os -import struct -import errno -import signal -import re -import time -import sys +import termios, select, os, struct, errno +import signal, re, time, sys from fcntl import ioctl from . import curses from .fancy_termios import tcgetattr, tcsetattr 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): @@ -52,18 +50,16 @@ class InvalidTerminal(RuntimeError): FIONREAD = getattr(termios, "FIONREAD", None) TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", None) - def _my_getstr(cap, optional=0): r = curses.tigetstr(cap) if not optional and r is None: raise InvalidTerminal( - "terminal doesn't have the required '%s' capability" % cap) + "terminal doesn't have the required '%s' capability"%cap) return r - # at this point, can we say: AAAAAAAAAAAAAAAAAAAAAARGH! def maybe_add_baudrate(dict, rate): - name = 'B%d' % rate + name = 'B%d'%rate if hasattr(termios, name): dict[getattr(termios, name)] = rate @@ -84,28 +80,19 @@ def maybe_add_baudrate(dict, rate): class poll: def __init__(self): pass - def register(self, fd, flag): self.fd = fd - - def poll(self, timeout=None): - r, w, e = select.select([self.fd], [], [], timeout) + def poll(self): # note: a 'timeout' argument would be *milliseconds* + r,w,e = select.select([self.fd],[],[]) return r POLLIN = getattr(select, "POLLIN", None) - -required_curses_tistrings = 'bel clear cup el' -optional_curses_tistrings = ( - 'civis cnorm cub cub1 cud cud1 cud cud1 cuf ' - 'cuf1 cuu cuu1 dch dch1 hpa ich ich1 ind pad ri rmkx smkx') - - class UnixConsole(Console): def __init__(self, f_in=0, f_out=1, term=None, encoding=None): if encoding is None: encoding = sys.getdefaultencoding() - + self.encoding = encoding if isinstance(f_in, int): @@ -117,21 +104,40 @@ def __init__(self, f_in=0, f_out=1, term=None, encoding=None): self.output_fd = f_out else: self.output_fd = f_out.fileno() + self.pollob = poll() self.pollob.register(self.input_fd, POLLIN) curses.setupterm(term, self.output_fd) self.term = term - - for name in required_curses_tistrings.split(): - setattr(self, '_' + name, _my_getstr(name)) - - for name in optional_curses_tistrings.split(): - setattr(self, '_' + name, _my_getstr(name, optional=1)) - + + self._bel = _my_getstr("bel") + self._civis = _my_getstr("civis", optional=1) + self._clear = _my_getstr("clear") + self._cnorm = _my_getstr("cnorm", optional=1) + self._cub = _my_getstr("cub", optional=1) + self._cub1 = _my_getstr("cub1", 1) + self._cud = _my_getstr("cud", 1) + self._cud1 = _my_getstr("cud1", 1) + self._cuf = _my_getstr("cuf", 1) + self._cuf1 = _my_getstr("cuf1", 1) + self._cup = _my_getstr("cup") + self._cuu = _my_getstr("cuu", 1) + self._cuu1 = _my_getstr("cuu1", 1) + self._dch1 = _my_getstr("dch1", 1) + self._dch = _my_getstr("dch", 1) + self._el = _my_getstr("el") + self._hpa = _my_getstr("hpa", 1) + self._ich = _my_getstr("ich", 1) + self._ich1 = _my_getstr("ich1", 1) + self._ind = _my_getstr("ind", 1) + self._pad = _my_getstr("pad", 1) + self._ri = _my_getstr("ri", 1) + self._rmkx = _my_getstr("rmkx", 1) + self._smkx = _my_getstr("smkx", 1) + ## work out how we're going to sling the cursor around - # hpa don't work in windows telnet :-( - if 0 and self._hpa: + if 0 and self._hpa: # hpa don't work in windows telnet :-( self.__move_x = self.__move_x_hpa elif self._cub and self._cuf: self.__move_x = self.__move_x_cub_cuf @@ -166,6 +172,9 @@ def __init__(self, f_in=0, f_out=1, term=None, encoding=None): self.event_queue = EventQueue(self.input_fd, self.encoding) self.cursor_visible = 1 + def change_encoding(self, encoding): + self.encoding = encoding + def refresh(self, screen, c_xy): # this function is still too long (over 90 lines) cx, cy = c_xy @@ -178,7 +187,7 @@ def refresh(self, screen, c_xy): self.screen.append("") else: while len(self.screen) < len(screen): - self.screen.append("") + self.screen.append("") if len(screen) > self.height: self.__gone_tall = 1 @@ -188,6 +197,7 @@ def refresh(self, screen, c_xy): old_offset = offset = self.__offset height = self.height + # we make sure the cursor is on the screen, and that we're # using all of the screen if we can if cy < offset: @@ -226,7 +236,7 @@ def refresh(self, screen, c_xy): newscr): if oldline != newline: self.__write_changed_line(y, oldline, newline, px) - + y = len(newscr) while y < len(oldscr): self.__hide_cursor() @@ -236,7 +246,7 @@ def refresh(self, screen, c_xy): y += 1 self.__show_cursor() - + self.screen = screen self.move_cursor(cx, cy) self.flushoutput() @@ -252,12 +262,11 @@ 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: - if (y == self.__posxy[1] and x > self.__posxy[0] and - oldline[px:x] == newline[px+1:x+1]): + if ( y == self.__posxy[1] and x > self.__posxy[0] + and oldline[px:x] == newline[px+1:x+1] ): x = px self.__move(x, y) self.__write_code(self.ich1) @@ -285,8 +294,7 @@ def __write_changed_line(self, y, oldline, newline, px): self.__write_code(self._el) 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 @@ -356,14 +364,13 @@ def prepare(self): # per-readline preparations: self.__svtermstate = tcgetattr(self.input_fd) raw = self.__svtermstate.copy() - raw.iflag |= termios.ICRNL - raw.iflag &= ~(termios.BRKINT | termios.INPCK | + raw.iflag &=~ (termios.BRKINT | termios.INPCK | termios.ISTRIP | termios.IXON) - raw.oflag &= ~termios.OPOST - raw.cflag &= ~(termios.CSIZE | termios.PARENB) - raw.cflag |= (termios.CS8) - raw.lflag &= ~(termios.ICANON | termios.ECHO | - termios.IEXTEN | (termios.ISIG * 1)) + raw.oflag &=~ (termios.OPOST) + raw.cflag &=~ (termios.CSIZE|termios.PARENB) + raw.cflag |= (termios.CS8) + raw.lflag &=~ (termios.ICANON|termios.ECHO| + termios.IEXTEN|(termios.ISIG*1)) raw.cc[termios.VMIN] = 1 raw.cc[termios.VTIME] = 0 tcsetattr(self.input_fd, termios.TCSADRAIN, raw) @@ -372,7 +379,7 @@ def prepare(self): self.height, self.width = self.getheightwidth() self.__buffer = [] - + self.__posxy = 0, 0 self.__gone_tall = 0 self.__move = self.__move_short @@ -402,11 +409,11 @@ def __sigwinch(self, signum, frame): def push_char(self, char): trace('push char {char!r}', char=char) self.event_queue.push(char) - + def get_event(self, block=1): while self.event_queue.empty(): - while 1: - # All hail Unix! + while 1: # All hail Unix! + pyos_inputhook() try: self.push_char(os.read(self.input_fd, 1)) except (IOError, OSError) as err: @@ -460,9 +467,8 @@ def getheightwidth(self): return int(os.environ["LINES"]), int(os.environ["COLUMNS"]) except KeyError: height, width = struct.unpack( - "hhhh", ioctl(self.input_fd, TIOCGWINSZ, "\000"*8))[0:2] - if not height: - return 25, 80 + "hhhh", ioctl(self.input_fd, TIOCGWINSZ, b"\000"*8))[0:2] + if not height: return 25, 80 return height, width else: def getheightwidth(self): @@ -501,7 +507,7 @@ def __tputs(self, fmt, prog=delayprog): os.write(self.output_fd, fmt[:x]) fmt = fmt[y:] delay = int(m.group(1)) - if '*' in m.group(2): + if b'*' in m.group(2): delay *= self.height if self._pad: nchars = (bps*delay)/1000 @@ -523,35 +529,33 @@ def beep(self): if FIONREAD: def getpending(self): - e = Event('key', '', '') + e = Event('key', '', b'') while not self.event_queue.empty(): e2 = self.event_queue.get() e.data += e2.data e.raw += e.raw - + amount = struct.unpack( - "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 + "i", ioctl(self.input_fd, FIONREAD, b"\0\0\0\0"))[0] + raw = os.read(self.input_fd, amount) + data = unicode(raw, self.encoding, 'replace') + e.data += data e.raw += raw return e else: def getpending(self): - e = Event('key', '', '') + e = Event('key', '', b'') while not self.event_queue.empty(): e2 = self.event_queue.get() e.data += e2.data e.raw += e.raw - + amount = 10000 - data = os.read(self.input_fd, amount) - raw = unicode(data, self.encoding, 'replace') - #XXX: something is wrong here - e.data += raw + raw = os.read(self.input_fd, amount) + data = unicode(raw, self.encoding, 'replace') + e.data += data e.raw += raw return e @@ -561,3 +565,4 @@ def clear(self): self.__move = self.__move_tall self.__posxy = 0, 0 self.screen = [] + diff --git a/pyrepl/unix_eventqueue.py b/pyrepl/unix_eventqueue.py index 332d952..cebe8fa 100644 --- a/pyrepl/unix_eventqueue.py +++ b/pyrepl/unix_eventqueue.py @@ -21,8 +21,6 @@ # Bah, this would be easier to test if curses/terminfo didn't have so # much non-introspectable global state. -from collections import deque - from pyrepl import keymap from pyrepl.console import Event from pyrepl import curses @@ -36,41 +34,23 @@ _keynames = { - "delete": "kdch1", - "down": "kcud1", - "end": "kend", - "enter": "kent", - "home": "khome", - "insert": "kich1", - "left": "kcub1", - "page down": "knp", - "page up": "kpp", - "right": "kcuf1", - "up": "kcuu1", -} - - -#function keys x in 1-20 -> fX: kfX -_keynames.update(('f%d' % i, 'kf%d' % i) for i in range(1, 21)) - -# this is a bit of a hack: CTRL-left and CTRL-right are not standardized -# termios sequences: each terminal emulator implements its own slightly -# different incarnation, and as far as I know, there is no way to know -# programmatically which sequences correspond to CTRL-left and -# CTRL-right. In bash, these keys usually work because there are bindings -# in ~/.inputrc, but pyrepl does not support it. The workaround is to -# hard-code here a bunch of known sequences, which will be seen as "ctrl -# left" and "ctrl right" keys, which can be finally be mapped to commands -# by the reader's keymaps. -# -CTRL_ARROW_KEYCODE = { - # for xterm, gnome-terminal, xfce terminal, etc. - b'\033[1;5D': 'ctrl left', - b'\033[1;5C': 'ctrl right', - # for rxvt - b'\033Od': 'ctrl left', - b'\033Oc': 'ctrl right', -} + "delete" : "kdch1", + "down" : "kcud1", + "end" : "kend", + "enter" : "kent", + "f1" : "kf1", "f2" : "kf2", "f3" : "kf3", "f4" : "kf4", + "f5" : "kf5", "f6" : "kf6", "f7" : "kf7", "f8" : "kf8", + "f9" : "kf9", "f10" : "kf10", "f11" : "kf11", "f12" : "kf12", + "f13" : "kf13", "f14" : "kf14", "f15" : "kf15", "f16" : "kf16", + "f17" : "kf17", "f18" : "kf18", "f19" : "kf19", "f20" : "kf20", + "home" : "khome", + "insert" : "kich1", + "left" : "kcub1", + "page down" : "knp", + "page up" : "kpp", + "right" : "kcuf1", + "up" : "kcuu1", + } def general_keycodes(): keycodes = {} @@ -79,10 +59,10 @@ def general_keycodes(): trace('key {key} tiname {tiname} keycode {keycode!r}', **locals()) if keycode: keycodes[keycode] = key - keycodes.update(CTRL_ARROW_KEYCODE) return keycodes + def EventQueue(fd, encoding): keycodes = general_keycodes() if os.isatty(fd): @@ -92,17 +72,16 @@ def EventQueue(fd, encoding): trace('keymap {k!r}', k=k) return EncodedQueue(k, encoding) - class EncodedQueue(object): def __init__(self, keymap, encoding): self.k = self.ck = keymap - self.events = deque() + self.events = [] self.buf = bytearray() - self.encoding = encoding + self.encoding=encoding def get(self): if self.events: - return self.events.popleft() + return self.events.pop(0) else: return None @@ -112,16 +91,14 @@ def empty(self): def flush_buf(self): old = self.buf self.buf = bytearray() - return old + return bytes(old) def insert(self, event): trace('added event {event}', event=event) self.events.append(event) def push(self, char): - ord_char = char if isinstance(char, int) else ord(char) - char = bytes(bytearray((ord_char,))) - self.buf.append(ord_char) + self.buf.append(ord(char)) if char in self.k: if self.k is self.ck: #sanity check, buffer is empty when a special key comes @@ -134,21 +111,11 @@ def push(self, char): self.insert(Event('key', k, self.flush_buf())) self.k = self.ck - elif self.buf and self.buf[0] == 27: # escape - # escape sequence not recognized by our keymap: propagate it - # outside so that i can be recognized as an M-... key (see also - # the docstring in keymap.py, in particular the line \\E. - trace('unrecognized escape sequence, propagating...') - self.k = self.ck - self.insert(Event('key', '\033', bytearray(b'\033'))) - for c in self.flush_buf()[1:]: - self.push(chr(c)) - else: try: decoded = bytes(self.buf).decode(self.encoding) - except UnicodeError: + except: return - else: - self.insert(Event('key', decoded, self.flush_buf())) + + self.insert(Event('key', decoded, self.flush_buf())) self.k = self.ck diff --git a/testing/__init__.py b/testing/__init__.py index e69de29..8b13789 100644 --- a/testing/__init__.py +++ b/testing/__init__.py @@ -0,0 +1 @@ + diff --git a/testing/conftest.py b/testing/conftest.py new file mode 100644 index 0000000..0ecad1f --- /dev/null +++ b/testing/conftest.py @@ -0,0 +1,8 @@ +import sys + +def pytest_ignore_collect(path): + if '__pypy__' not in sys.builtin_module_names: + try: + import pyrepl + except ImportError: + return True diff --git a/testing/infrastructure.py b/testing/infrastructure.py index 51c6f3c..b199e0d 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 @@ -56,7 +59,7 @@ def get_event(self, block=1): return Event(*ev) -class TestReader(Reader): +class BaseTestReader(Reader): def get_prompt(self, lineno, cursor_on_line): return '' @@ -66,8 +69,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..32c4033 100644 --- a/testing/test_readline.py +++ b/testing/test_readline.py @@ -1,68 +1,22 @@ -import os -import pty -import sys - import pytest -from pyrepl.readline import _ReadlineWrapper - - -@pytest.fixture -def readline_wrapper(): - master, slave = pty.openpty() - return _ReadlineWrapper(slave, slave) - - -if sys.version_info < (3, ): - bytes_type = str - unicode_type = unicode # noqa: F821 -else: - bytes_type = bytes - unicode_type = str +from .infrastructure import sane_term -def test_readline(): - master, slave = pty.openpty() - readline_wrapper = _ReadlineWrapper(slave, slave) - os.write(master, b'input\n') - - result = readline_wrapper.get_reader().readline() - assert result == b'input' - assert isinstance(result, bytes_type) +@pytest.mark.skipif("os.name != 'posix' or 'darwin' in sys.platform or " + "'freebsd' in sys.platform") +def test_raw_input(): + import os + import pty + from pyrepl.readline import _ReadlineWrapper -def test_readline_returns_unicode(): master, slave = pty.openpty() readline_wrapper = _ReadlineWrapper(slave, slave) os.write(master, b'input\n') - result = readline_wrapper.get_reader().readline(returns_unicode=True) + with sane_term(): + result = readline_wrapper.get_reader().readline() + #result = readline_wrapper.raw_input('prompt:') assert result == 'input' - assert isinstance(result, unicode_type) - - -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) - - -def test_read_history_file(readline_wrapper, tmp_path): - histfile = tmp_path / "history" - histfile.touch() - - assert readline_wrapper.reader is None - - readline_wrapper.read_history_file(str(histfile)) - assert readline_wrapper.reader.history == [] - - histfile.write_bytes(b"foo\nbar\n") - readline_wrapper.read_history_file(str(histfile)) - assert readline_wrapper.reader.history == ["foo", "bar"] + # A bytes string on python2, a unicode string on python3. + assert isinstance(result, str) diff --git a/testing/test_unix_reader.py b/testing/test_unix_reader.py deleted file mode 100644 index 9fbcb2c..0000000 --- a/testing/test_unix_reader.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import unicode_literals -from pyrepl.unix_eventqueue import EncodedQueue, Event - - -def test_simple(): - q = EncodedQueue({}, 'utf-8') - - a = u'\u1234' - b = a.encode('utf-8') - for c in b: - q.push(c) - - event = q.get() - assert q.get() is None - assert event.data == a - assert event.raw == b - - -def test_propagate_escape(): - def send(keys): - for c in keys: - q.push(c) - - events = [] - while True: - event = q.get() - if event is None: - break - events.append(event) - return events - - keymap = { - b'\033': {b'U': 'up', b'D': 'down'}, - b'\xf7': 'backspace', - } - q = EncodedQueue(keymap, 'utf-8') - - # normal behaviour - assert send(b'\033U') == [Event('key', 'up', bytearray(b'\033U'))] - assert send(b'\xf7') == [Event('key', 'backspace', bytearray(b'\xf7'))] - - # escape propagation: simulate M-backspace - events = send(b'\033\xf7') - assert events == [ - Event('key', '\033', bytearray(b'\033')), - Event('key', 'backspace', bytearray(b'\xf7')) - ] From cd77c0246eec252085c18de2c5500506ccd15f3a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 10 Mar 2019 20:44:00 +0100 Subject: [PATCH 02/31] tests: test_raw_input: test raw_input (again) --- testing/test_readline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/test_readline.py b/testing/test_readline.py index 32c4033..717aed0 100644 --- a/testing/test_readline.py +++ b/testing/test_readline.py @@ -15,8 +15,7 @@ def test_raw_input(): os.write(master, b'input\n') with sane_term(): - result = readline_wrapper.get_reader().readline() - #result = readline_wrapper.raw_input('prompt:') + result = readline_wrapper.raw_input('prompt:') assert result == 'input' # A bytes string on python2, a unicode string on python3. assert isinstance(result, str) From d7e72e6f501abd80ab3f1a289341c3ae7b02fbfc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 10 Mar 2019 20:52:13 +0100 Subject: [PATCH 03/31] raw_input: pass returns_unicode properly It was passing in `reader` for `returns_unicode` (which should be a bool). This does not change the behavior, but is less confusing. --- pyrepl/readline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrepl/readline.py b/pyrepl/readline.py index 593297a..0807098 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -259,7 +259,7 @@ def raw_input(self, prompt=''): except _error: return _old_raw_input(prompt) reader.ps1 = prompt - return reader.readline(reader, startup_hook=self.startup_hook) + 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 From 2c0e4893c5f885feaeb1de23fcbca1ed5d8b6f9d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 10 Mar 2019 22:41:47 +0100 Subject: [PATCH 04/31] Fix/re-enable quoted_insert: use self.evt.data --- pyrepl/commands.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyrepl/commands.py b/pyrepl/commands.py index 0cd87fd..67a718b 100644 --- a/pyrepl/commands.py +++ b/pyrepl/commands.py @@ -379,12 +379,9 @@ 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 def do(self): - # XXX in Python 3, processing insert/C-q/C-v keys crashes - # because of a mixture of str and bytes. Disable these keys. - pass - #self.reader.push_input_trans(QITrans()) + self.reader.push_input_trans(QITrans()) From 73a524983c7af647957c4276dd09e8aa9d5484bc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 10 Mar 2019 23:47:13 +0100 Subject: [PATCH 05/31] Fix/improve qIHelp (for quoted-insert) Fix test_quoted_insert_repeat to use "key" instead of "self-insert" to actually use qIHelp. --- pyrepl/commands.py | 6 +++++- testing/infrastructure.py | 4 ++++ testing/test_wishes.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pyrepl/commands.py b/pyrepl/commands.py index 67a718b..17a6190 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((self.event + r.console.getpending().data) * r.get_arg()) + pending = r.console.getpending().data + disp = disp_str((self.event + pending).encode())[0] + r.insert(disp * r.get_arg()) r.pop_input_trans() from pyrepl import input diff --git a/testing/infrastructure.py b/testing/infrastructure.py index b199e0d..ca8784e 100644 --- a/testing/infrastructure.py +++ b/testing/infrastructure.py @@ -58,6 +58,10 @@ 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 BaseTestReader(Reader): diff --git a/testing/test_wishes.py b/testing/test_wishes.py index 650dff7..28073a0 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)]) From 967eb9149c61518111a210525ec46e807a14d964 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 00:04:53 +0100 Subject: [PATCH 06/31] Keep py2 fixes from master --- pyrepl/reader.py | 2 +- testing/test_readline.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pyrepl/reader.py b/pyrepl/reader.py index 32db42a..8bc42c6 100644 --- a/pyrepl/reader.py +++ b/pyrepl/reader.py @@ -55,7 +55,7 @@ def _my_unctrl(c, u=_make_unctrl_map()): return u[c] else: if unicodedata.category(c).startswith('C'): - return r'\u%04x' % ord(c) + return b'\u%04x' % ord(c) else: return c diff --git a/testing/test_readline.py b/testing/test_readline.py index 717aed0..a84f8ba 100644 --- a/testing/test_readline.py +++ b/testing/test_readline.py @@ -1,7 +1,16 @@ +import sys + import pytest from .infrastructure import sane_term +if sys.version_info < (3, ): + bytes_type = str + unicode_type = unicode # noqa: F821 +else: + bytes_type = bytes + unicode_type = str + @pytest.mark.skipif("os.name != 'posix' or 'darwin' in sys.platform or " "'freebsd' in sys.platform") @@ -18,4 +27,4 @@ def test_raw_input(): result = readline_wrapper.raw_input('prompt:') assert result == 'input' # A bytes string on python2, a unicode string on python3. - assert isinstance(result, str) + assert isinstance(result, unicode_type) From 9c666bbaa36b0a91f577479b0aabfc37f12dad76 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 00:08:22 +0100 Subject: [PATCH 07/31] test_readline: keep tests from master --- testing/test_readline.py | 56 +++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/testing/test_readline.py b/testing/test_readline.py index a84f8ba..045aefc 100644 --- a/testing/test_readline.py +++ b/testing/test_readline.py @@ -1,6 +1,9 @@ +import os +import pty import sys import pytest +from pyrepl.readline import _ReadlineWrapper from .infrastructure import sane_term @@ -12,19 +15,58 @@ 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) + os.write(master, b'input\n') + + result = readline_wrapper.get_reader().readline() + assert result == b'input' + assert isinstance(result, bytes_type) + + +def test_readline_returns_unicode(): + master, slave = pty.openpty() + readline_wrapper = _ReadlineWrapper(slave, slave) + os.write(master, b'input\n') + + result = readline_wrapper.get_reader().readline(returns_unicode=True) + assert result == 'input' + 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(): - import os - import pty - from pyrepl.readline import _ReadlineWrapper - master, slave = pty.openpty() readline_wrapper = _ReadlineWrapper(slave, slave) os.write(master, b'input\n') with sane_term(): result = readline_wrapper.raw_input('prompt:') - assert result == 'input' - # A bytes string on python2, a unicode string on python3. - assert isinstance(result, unicode_type) + if sys.version_info < (3, ): + assert result == b'input' + assert isinstance(result, bytes_type) + else: + assert result == 'input' + assert isinstance(result, unicode_type) + + +def test_read_history_file(readline_wrapper, tmp_path): + histfile = tmp_path / "history" + histfile.touch() + + assert readline_wrapper.reader is None + + readline_wrapper.read_history_file(str(histfile)) + assert readline_wrapper.reader.history == [] + + histfile.write_bytes(b"foo\nbar\n") + readline_wrapper.read_history_file(str(histfile)) + assert readline_wrapper.reader.history == ["foo", "bar"] From fdcdd745366a08d0461e8a798cc9c093550f0e86 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 00:29:14 +0100 Subject: [PATCH 08/31] fixup! Keep py2 fixes from master --- pyrepl/reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrepl/reader.py b/pyrepl/reader.py index 8bc42c6..e2baa9a 100644 --- a/pyrepl/reader.py +++ b/pyrepl/reader.py @@ -55,9 +55,9 @@ def _my_unctrl(c, u=_make_unctrl_map()): return u[c] else: if unicodedata.category(c).startswith('C'): - return b'\u%04x' % ord(c) + return '\\u%04x' % ord(c) else: - return c + return c # XXX: does not "return a unicode"?! if 'a'[0] == b'a': # When running tests with python2, bytes characters are bytes. From ae41c26d706c1ef2bc59cfe455ce9b62a7b2ab01 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:06:36 +0100 Subject: [PATCH 09/31] fixup! test_readline: keep tests from master --- testing/test_readline.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/testing/test_readline.py b/testing/test_readline.py index 045aefc..5871d9e 100644 --- a/testing/test_readline.py +++ b/testing/test_readline.py @@ -50,12 +50,8 @@ def test_raw_input(): with sane_term(): 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) + assert result == 'input' + assert isinstance(result, unicode_type) def test_read_history_file(readline_wrapper, tmp_path): From 159e3a1204dc8be3a19791549fede9f8ae4023c0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:07:07 +0100 Subject: [PATCH 10/31] fixup! fixup! Keep py2 fixes from master --- pyrepl/reader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyrepl/reader.py b/pyrepl/reader.py index e2baa9a..0332363 100644 --- a/pyrepl/reader.py +++ b/pyrepl/reader.py @@ -51,11 +51,12 @@ def _make_unctrl_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 '\\u%04x' % ord(c) + return '\\u%04x' % c else: return c # XXX: does not "return a unicode"?! From a25b7b7617da74a43971eaca75437ac885110c99 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:07:45 +0100 Subject: [PATCH 11/31] Fix read_history_file/write_history_file for py2 --- pyrepl/readline.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pyrepl/readline.py b/pyrepl/readline.py index 0807098..75ea48e 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -34,7 +34,9 @@ try: unicode + PY2 = True except NameError: + PY2 = False unicode = str ENCODING = sys.getfilesystemencoding() or 'latin1' # XXX review @@ -314,8 +316,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', encoding='utf-8', - errors='replace') + 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'): @@ -332,11 +337,14 @@ 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', encoding='utf-8') + if PY2: + f = open(os.path.expanduser(filename), 'w') + else: + f = open(os.path.expanduser(filename), 'w', encoding='utf-8') for entry in history: # if we are on py3k, we don't need to encode strings before # writing it to a file - if isinstance(entry, unicode) and sys.version_info < (3,): + if isinstance(entry, unicode) and PY2: entry = entry.encode('utf-8') entry = entry.replace('\n', '\r\n') # multiline history support f.write(entry + '\n') From 998e3795ba00600f4fda402b64b12c8ea7c2e7e5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:08:31 +0100 Subject: [PATCH 12/31] fixup! fixup! fixup! Keep py2 fixes from master --- pyrepl/commands.py | 2 +- pyrepl/reader.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pyrepl/commands.py b/pyrepl/commands.py index 17a6190..c121f34 100644 --- a/pyrepl/commands.py +++ b/pyrepl/commands.py @@ -373,7 +373,7 @@ def do(self): r = self.reader pending = r.console.getpending().data - disp = disp_str((self.event + pending).encode())[0] + disp = disp_str((self.event + pending))[0] r.insert(disp * r.get_arg()) r.pop_input_trans() diff --git a/pyrepl/reader.py b/pyrepl/reader.py index 0332363..e7ab369 100644 --- a/pyrepl/reader.py +++ b/pyrepl/reader.py @@ -60,11 +60,6 @@ def _my_unctrl(c, u=_make_unctrl_map()): else: return c # XXX: does not "return a unicode"?! -if 'a'[0] == b'a': - # When running tests with python2, bytes characters are bytes. - def _my_unctrl(c, uc=_my_unctrl): - return uc(ord(c)) - def disp_str(buffer, join=''.join, uc=_my_unctrl): """ disp_str(buffer:string) -> (string, [int]) @@ -80,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) From ae4b8a2ab67e59ace204493ab86d8636ba97920a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:20:32 +0100 Subject: [PATCH 13/31] testing/test_unix_reader.py from master --- testing/test_unix_reader.py | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 testing/test_unix_reader.py diff --git a/testing/test_unix_reader.py b/testing/test_unix_reader.py new file mode 100644 index 0000000..9fbcb2c --- /dev/null +++ b/testing/test_unix_reader.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals +from pyrepl.unix_eventqueue import EncodedQueue, Event + + +def test_simple(): + q = EncodedQueue({}, 'utf-8') + + a = u'\u1234' + b = a.encode('utf-8') + for c in b: + q.push(c) + + event = q.get() + assert q.get() is None + assert event.data == a + assert event.raw == b + + +def test_propagate_escape(): + def send(keys): + for c in keys: + q.push(c) + + events = [] + while True: + event = q.get() + if event is None: + break + events.append(event) + return events + + keymap = { + b'\033': {b'U': 'up', b'D': 'down'}, + b'\xf7': 'backspace', + } + q = EncodedQueue(keymap, 'utf-8') + + # normal behaviour + assert send(b'\033U') == [Event('key', 'up', bytearray(b'\033U'))] + assert send(b'\xf7') == [Event('key', 'backspace', bytearray(b'\xf7'))] + + # escape propagation: simulate M-backspace + events = send(b'\033\xf7') + assert events == [ + Event('key', '\033', bytearray(b'\033')), + Event('key', 'backspace', bytearray(b'\xf7')) + ] From 9308882557169970521ab30da1351c27f9756424 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:42:44 +0100 Subject: [PATCH 14/31] keep things from master for test_unix_reader --- pyrepl/console.py | 7 ++++++- testing/test_unix_reader.py | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyrepl/console.py b/pyrepl/console.py index 2891be7..ac242a8 100644 --- a/pyrepl/console.py +++ b/pyrepl/console.py @@ -27,7 +27,12 @@ def __init__(self, evt, data, raw=''): self.raw = raw def __repr__(self): - return 'Event(%r, %r)'%(self.evt, self.data) + return 'Event(%r, %r)' % (self.evt, self.data) + + def __eq__(self, other): + return (self.evt == other.evt and + self.data == other.data and + self.raw == other.raw) class Console(object): """Attributes: 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'}, From 53d0a75a5233ec5cb45bfb31fd0c00c3ecc17ef9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:44:16 +0100 Subject: [PATCH 15/31] fixup! keep things from master for test_unix_reader --- pyrepl/unix_eventqueue.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pyrepl/unix_eventqueue.py b/pyrepl/unix_eventqueue.py index cebe8fa..5e82872 100644 --- a/pyrepl/unix_eventqueue.py +++ b/pyrepl/unix_eventqueue.py @@ -21,6 +21,8 @@ # Bah, this would be easier to test if curses/terminfo didn't have so # much non-introspectable global state. +from collections import deque + from pyrepl import keymap from pyrepl.console import Event from pyrepl import curses @@ -75,13 +77,13 @@ def EventQueue(fd, encoding): class EncodedQueue(object): def __init__(self, keymap, encoding): self.k = self.ck = keymap - self.events = [] + self.events = deque() self.buf = bytearray() - self.encoding=encoding + self.encoding = encoding def get(self): if self.events: - return self.events.pop(0) + return self.events.popleft() else: return None @@ -91,14 +93,16 @@ def empty(self): def flush_buf(self): old = self.buf self.buf = bytearray() - return bytes(old) + return old def insert(self, event): trace('added event {event}', event=event) self.events.append(event) def push(self, char): - self.buf.append(ord(char)) + ord_char = ord(char) + char = bytes(bytearray((ord_char,))) + self.buf.append(ord_char) if char in self.k: if self.k is self.ck: #sanity check, buffer is empty when a special key comes @@ -111,6 +115,16 @@ def push(self, char): self.insert(Event('key', k, self.flush_buf())) self.k = self.ck + elif self.buf and self.buf[0] == 27: # escape + # escape sequence not recognized by our keymap: propagate it + # outside so that i can be recognized as an M-... key (see also + # the docstring in keymap.py, in particular the line \\E. + trace('unrecognized escape sequence, propagating...') + self.k = self.ck + self.insert(Event('key', '\033', bytearray(b'\033'))) + for c in self.flush_buf()[1:]: + self.push(chr(c)) + else: try: decoded = bytes(self.buf).decode(self.encoding) From c91fcd237415e8d7492fb7152bfdcd1fc5697544 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:51:30 +0100 Subject: [PATCH 16/31] keep ncursesw fix --- pyrepl/_minimal_curses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrepl/_minimal_curses.py b/pyrepl/_minimal_curses.py index 7aa5abf..bc0dd42 100644 --- a/pyrepl/_minimal_curses.py +++ b/pyrepl/_minimal_curses.py @@ -16,7 +16,7 @@ class error(Exception): def _find_clib(): - trylibs = ['ncurses', 'curses'] + trylibs = ['ncursesw', 'ncurses', 'curses'] for lib in trylibs: path = ctypes.util.find_library(lib) From d8636d149088aeae5f3ef4c48dfd8617ab489b1b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:52:14 +0100 Subject: [PATCH 17/31] fixup! Fix/improve qIHelp (for quoted-insert) --- testing/test_wishes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_wishes.py b/testing/test_wishes.py index 28073a0..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), ['']), - (('key', '\033'), ['^[^[^[']), + (('key', '\033'), ['^[^[^[']), (('accept', None), None)]) From 07a784d47950d70c2d0dc0e77d53a3263c78a4ec Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:54:26 +0100 Subject: [PATCH 18/31] remove testing/conftest.py: pytest_ignore_collect should not be needed --- testing/conftest.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 testing/conftest.py diff --git a/testing/conftest.py b/testing/conftest.py deleted file mode 100644 index 0ecad1f..0000000 --- a/testing/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -def pytest_ignore_collect(path): - if '__pypy__' not in sys.builtin_module_names: - try: - import pyrepl - except ImportError: - return True From 93d686860ebb1ace12e21541dfd541aba4d2cc51 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:54:56 +0100 Subject: [PATCH 19/31] testing/__init__.py: no trailing newline --- testing/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/__init__.py b/testing/__init__.py index 8b13789..e69de29 100644 --- a/testing/__init__.py +++ b/testing/__init__.py @@ -1 +0,0 @@ - From 87b195143be48e9d6493379d46b83315919fd6b9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 01:57:16 +0100 Subject: [PATCH 20/31] keep pyrepl/unix_eventqueue.py mostly from master --- pyrepl/unix_eventqueue.py | 61 +++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/pyrepl/unix_eventqueue.py b/pyrepl/unix_eventqueue.py index 5e82872..3a9c77d 100644 --- a/pyrepl/unix_eventqueue.py +++ b/pyrepl/unix_eventqueue.py @@ -36,23 +36,41 @@ _keynames = { - "delete" : "kdch1", - "down" : "kcud1", - "end" : "kend", - "enter" : "kent", - "f1" : "kf1", "f2" : "kf2", "f3" : "kf3", "f4" : "kf4", - "f5" : "kf5", "f6" : "kf6", "f7" : "kf7", "f8" : "kf8", - "f9" : "kf9", "f10" : "kf10", "f11" : "kf11", "f12" : "kf12", - "f13" : "kf13", "f14" : "kf14", "f15" : "kf15", "f16" : "kf16", - "f17" : "kf17", "f18" : "kf18", "f19" : "kf19", "f20" : "kf20", - "home" : "khome", - "insert" : "kich1", - "left" : "kcub1", - "page down" : "knp", - "page up" : "kpp", - "right" : "kcuf1", - "up" : "kcuu1", - } + "delete": "kdch1", + "down": "kcud1", + "end": "kend", + "enter": "kent", + "home": "khome", + "insert": "kich1", + "left": "kcub1", + "page down": "knp", + "page up": "kpp", + "right": "kcuf1", + "up": "kcuu1", +} + + +#function keys x in 1-20 -> fX: kfX +_keynames.update(('f%d' % i, 'kf%d' % i) for i in range(1, 21)) + +# this is a bit of a hack: CTRL-left and CTRL-right are not standardized +# termios sequences: each terminal emulator implements its own slightly +# different incarnation, and as far as I know, there is no way to know +# programmatically which sequences correspond to CTRL-left and +# CTRL-right. In bash, these keys usually work because there are bindings +# in ~/.inputrc, but pyrepl does not support it. The workaround is to +# hard-code here a bunch of known sequences, which will be seen as "ctrl +# left" and "ctrl right" keys, which can be finally be mapped to commands +# by the reader's keymaps. +# +CTRL_ARROW_KEYCODE = { + # for xterm, gnome-terminal, xfce terminal, etc. + b'\033[1;5D': 'ctrl left', + b'\033[1;5C': 'ctrl right', + # for rxvt + b'\033Od': 'ctrl left', + b'\033Oc': 'ctrl right', +} def general_keycodes(): keycodes = {} @@ -61,10 +79,10 @@ def general_keycodes(): trace('key {key} tiname {tiname} keycode {keycode!r}', **locals()) if keycode: keycodes[keycode] = key + keycodes.update(CTRL_ARROW_KEYCODE) return keycodes - def EventQueue(fd, encoding): keycodes = general_keycodes() if os.isatty(fd): @@ -74,6 +92,7 @@ def EventQueue(fd, encoding): trace('keymap {k!r}', k=k) return EncodedQueue(k, encoding) + class EncodedQueue(object): def __init__(self, keymap, encoding): self.k = self.ck = keymap @@ -128,8 +147,8 @@ def push(self, char): else: try: decoded = bytes(self.buf).decode(self.encoding) - except: + except UnicodeError: return - - self.insert(Event('key', decoded, self.flush_buf())) + else: + self.insert(Event('key', decoded, self.flush_buf())) self.k = self.ck From e10e082d03453c9c99a3015654148af33bf415a5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 03:03:56 +0100 Subject: [PATCH 21/31] tests: add test_write_history_file (failing on master) --- testing/test_readline.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/testing/test_readline.py b/testing/test_readline.py index 5871d9e..f05e629 100644 --- a/testing/test_readline.py +++ b/testing/test_readline.py @@ -66,3 +66,17 @@ 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)) + + with open(str(histfile), "r") as f: + assert f.readlines() == ["foo\n", "bar\n"] From ff54181ff9e63c173470288d9bad28087eed1180 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 03:17:51 +0100 Subject: [PATCH 22/31] write_history_file: do not nuke history on errors --- pyrepl/readline.py | 14 ++++++++------ testing/test_readline.py | 27 +++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/pyrepl/readline.py b/pyrepl/readline.py index 75ea48e..9b9a5af 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -337,18 +337,20 @@ 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) - if PY2: - f = open(os.path.expanduser(filename), 'w') - else: - f = open(os.path.expanduser(filename), 'w', encoding='utf-8') + entries = '' for entry in history: # 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') - f.close() + entries += entry + '\n' + + fname = os.path.expanduser(filename) + if PY2: + open(fname, 'w').write(entries) + else: + open(fname, 'w', encoding='utf-8').write(entries) def clear_history(self): del self.get_reader().history[:] diff --git a/testing/test_readline.py b/testing/test_readline.py index f05e629..21b26bd 100644 --- a/testing/test_readline.py +++ b/testing/test_readline.py @@ -78,5 +78,28 @@ def test_write_history_file(readline_wrapper, tmp_path): readline_wrapper.write_history_file(str(histfile)) - with open(str(histfile), "r") as f: - assert f.readlines() == ["foo\n", "bar\n"] + 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"] From f5c935f7892859cfdb4a8e498e0fd299995c401c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 03:29:55 +0100 Subject: [PATCH 23/31] keep 33aac3f from master --- pyrepl/input.py | 18 ++++++++---------- pyrepl/keymap.py | 2 ++ pyrepl/reader.py | 2 ++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pyrepl/input.py b/pyrepl/input.py index 47e22ca..e92adbd 100644 --- a/pyrepl/input.py +++ b/pyrepl/input.py @@ -35,6 +35,8 @@ from __future__ import print_function import unicodedata from collections import deque +import pprint +from .trace import trace class InputTranslator(object): @@ -59,26 +61,23 @@ def __init__(self, keymap, verbose=0, for keyspec, command in keymap: keyseq = tuple(parse_keys(keyspec)) d[keyseq] = command - if self.verbose: - print(d) + if verbose: + trace('[input] keymap: {}', pprint.pformat(d)) self.k = self.ck = compile_keymap(d, ()) self.results = deque() self.stack = [] def push(self, evt): - if self.verbose: - print("pushed", evt.data, end='') + trace("[input] pushed {!r}", evt.data) key = evt.data d = self.k.get(key) if isinstance(d, dict): - if self.verbose: - print("transition") + trace("[input] transition") self.stack.append(key) self.k = d else: if d is None: - if self.verbose: - print("invalid") + trace("[input] invalid") if self.stack or len(key) > 1 or unicodedata.category(key) == 'C': self.results.append( (self.invalid_cls, self.stack + [key])) @@ -88,8 +87,7 @@ def push(self, evt): self.results.append( (self.character_cls, [key])) else: - if self.verbose: - print("matched", d) + trace("[input] matched {}", d) self.results.append((d, self.stack + [key])) self.stack = [] self.k = self.ck diff --git a/pyrepl/keymap.py b/pyrepl/keymap.py index 187c539..901f7ff 100644 --- a/pyrepl/keymap.py +++ b/pyrepl/keymap.py @@ -89,6 +89,8 @@ 'space': ' ', 'tab': '\t', 'up': 'up', + 'ctrl left': 'ctrl left', + 'ctrl right': 'ctrl right', } class KeySpecError(Exception): diff --git a/pyrepl/reader.py b/pyrepl/reader.py index e7ab369..a44e7f6 100644 --- a/pyrepl/reader.py +++ b/pyrepl/reader.py @@ -165,6 +165,8 @@ def make_default_syntax_table(): (r'\EOF', 'end'), # the entries in the terminfo database for xterms (r'\EOH', 'home'), # seem to be wrong. this is a less than ideal # workaround + (r'\', 'backward-word'), + (r'\', 'forward-word'), ]) if 'c' in globals(): # only on python 2.x From 4d3648cba3430856d0e7afee78abd5dfa0657051 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 03:47:45 +0100 Subject: [PATCH 24/31] Keep code cleanup from master --- pyrepl/_minimal_curses.py | 12 +++- pyrepl/unix_console.py | 135 +++++++++++++++++++------------------- 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/pyrepl/_minimal_curses.py b/pyrepl/_minimal_curses.py index bc0dd42..f61b04e 100644 --- a/pyrepl/_minimal_curses.py +++ b/pyrepl/_minimal_curses.py @@ -9,7 +9,8 @@ hide this one if compiled in. """ -import ctypes, ctypes.util +import ctypes.util + class error(Exception): pass @@ -42,8 +43,11 @@ def _find_clib(): # ____________________________________________________________ -try: from __pypy__ import builtinify -except ImportError: builtinify = lambda f: f +try: + from __pypy__ import builtinify +except ImportError: + builtinify = lambda f: f + @builtinify def setupterm(termstr, fd): @@ -52,6 +56,7 @@ def setupterm(termstr, fd): if result == ERR: raise error("setupterm() failed (err=%d)" % err.value) + @builtinify def tigetstr(cap): if not isinstance(cap, bytes): @@ -61,6 +66,7 @@ def tigetstr(cap): 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) diff --git a/pyrepl/unix_console.py b/pyrepl/unix_console.py index ff228d0..cfa356d 100644 --- a/pyrepl/unix_console.py +++ b/pyrepl/unix_console.py @@ -19,8 +19,15 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import termios, select, os, struct, errno -import signal, re, time, sys +import termios +import select +import os +import struct +import errno +import signal +import re +import time +import sys from fcntl import ioctl from . import curses from .fancy_termios import tcgetattr, tcsetattr @@ -50,16 +57,18 @@ class InvalidTerminal(RuntimeError): FIONREAD = getattr(termios, "FIONREAD", None) TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", None) + def _my_getstr(cap, optional=0): r = curses.tigetstr(cap) if not optional and r is None: raise InvalidTerminal( - "terminal doesn't have the required '%s' capability"%cap) + "terminal doesn't have the required '%s' capability" % cap) return r + # at this point, can we say: AAAAAAAAAAAAAAAAAAAAAARGH! def maybe_add_baudrate(dict, rate): - name = 'B%d'%rate + name = 'B%d' % rate if hasattr(termios, name): dict[getattr(termios, name)] = rate @@ -80,19 +89,28 @@ def maybe_add_baudrate(dict, rate): class poll: def __init__(self): pass + def register(self, fd, flag): self.fd = fd - def poll(self): # note: a 'timeout' argument would be *milliseconds* - r,w,e = select.select([self.fd],[],[]) + + def poll(self, timeout=None): + r, w, e = select.select([self.fd], [], [], timeout) return r POLLIN = getattr(select, "POLLIN", None) + +required_curses_tistrings = 'bel clear cup el' +optional_curses_tistrings = ( + 'civis cnorm cub cub1 cud cud1 cud cud1 cuf ' + 'cuf1 cuu cuu1 dch dch1 hpa ich ich1 ind pad ri rmkx smkx') + + class UnixConsole(Console): def __init__(self, f_in=0, f_out=1, term=None, encoding=None): if encoding is None: encoding = sys.getdefaultencoding() - + self.encoding = encoding if isinstance(f_in, int): @@ -104,40 +122,21 @@ def __init__(self, f_in=0, f_out=1, term=None, encoding=None): self.output_fd = f_out else: self.output_fd = f_out.fileno() - self.pollob = poll() self.pollob.register(self.input_fd, POLLIN) curses.setupterm(term, self.output_fd) self.term = term - - self._bel = _my_getstr("bel") - self._civis = _my_getstr("civis", optional=1) - self._clear = _my_getstr("clear") - self._cnorm = _my_getstr("cnorm", optional=1) - self._cub = _my_getstr("cub", optional=1) - self._cub1 = _my_getstr("cub1", 1) - self._cud = _my_getstr("cud", 1) - self._cud1 = _my_getstr("cud1", 1) - self._cuf = _my_getstr("cuf", 1) - self._cuf1 = _my_getstr("cuf1", 1) - self._cup = _my_getstr("cup") - self._cuu = _my_getstr("cuu", 1) - self._cuu1 = _my_getstr("cuu1", 1) - self._dch1 = _my_getstr("dch1", 1) - self._dch = _my_getstr("dch", 1) - self._el = _my_getstr("el") - self._hpa = _my_getstr("hpa", 1) - self._ich = _my_getstr("ich", 1) - self._ich1 = _my_getstr("ich1", 1) - self._ind = _my_getstr("ind", 1) - self._pad = _my_getstr("pad", 1) - self._ri = _my_getstr("ri", 1) - self._rmkx = _my_getstr("rmkx", 1) - self._smkx = _my_getstr("smkx", 1) - + + for name in required_curses_tistrings.split(): + setattr(self, '_' + name, _my_getstr(name)) + + for name in optional_curses_tistrings.split(): + setattr(self, '_' + name, _my_getstr(name, optional=1)) + ## work out how we're going to sling the cursor around - if 0 and self._hpa: # hpa don't work in windows telnet :-( + # hpa don't work in windows telnet :-( + if 0 and self._hpa: self.__move_x = self.__move_x_hpa elif self._cub and self._cuf: self.__move_x = self.__move_x_cub_cuf @@ -172,9 +171,6 @@ def __init__(self, f_in=0, f_out=1, term=None, encoding=None): self.event_queue = EventQueue(self.input_fd, self.encoding) self.cursor_visible = 1 - def change_encoding(self, encoding): - self.encoding = encoding - def refresh(self, screen, c_xy): # this function is still too long (over 90 lines) cx, cy = c_xy @@ -187,7 +183,7 @@ def refresh(self, screen, c_xy): self.screen.append("") else: while len(self.screen) < len(screen): - self.screen.append("") + self.screen.append("") if len(screen) > self.height: self.__gone_tall = 1 @@ -197,7 +193,6 @@ def refresh(self, screen, c_xy): old_offset = offset = self.__offset height = self.height - # we make sure the cursor is on the screen, and that we're # using all of the screen if we can if cy < offset: @@ -236,7 +231,7 @@ def refresh(self, screen, c_xy): newscr): if oldline != newline: self.__write_changed_line(y, oldline, newline, px) - + y = len(newscr) while y < len(oldscr): self.__hide_cursor() @@ -246,7 +241,7 @@ def refresh(self, screen, c_xy): y += 1 self.__show_cursor() - + self.screen = screen self.move_cursor(cx, cy) self.flushoutput() @@ -265,8 +260,8 @@ def __write_changed_line(self, y, oldline, newline, px): while x < minlen and oldline[x] == newline[x] and newline[x] != '\x1b': x += 1 if oldline[x:] == newline[x+1:] and self.ich1: - if ( y == self.__posxy[1] and x > self.__posxy[0] - and oldline[px:x] == newline[px+1:x+1] ): + if (y == self.__posxy[1] and x > self.__posxy[0] and + oldline[px:x] == newline[px+1:x+1]): x = px self.__move(x, y) self.__write_code(self.ich1) @@ -294,7 +289,7 @@ def __write_changed_line(self, y, oldline, newline, px): self.__write_code(self._el) self.__write(newline[x:]) self.__posxy = len(newline), y - + if '\x1b' in newline: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor @@ -364,13 +359,14 @@ def prepare(self): # per-readline preparations: self.__svtermstate = tcgetattr(self.input_fd) raw = self.__svtermstate.copy() - raw.iflag &=~ (termios.BRKINT | termios.INPCK | + raw.iflag |= termios.ICRNL + raw.iflag &= ~(termios.BRKINT | termios.INPCK | termios.ISTRIP | termios.IXON) - raw.oflag &=~ (termios.OPOST) - raw.cflag &=~ (termios.CSIZE|termios.PARENB) - raw.cflag |= (termios.CS8) - raw.lflag &=~ (termios.ICANON|termios.ECHO| - termios.IEXTEN|(termios.ISIG*1)) + raw.oflag &= ~termios.OPOST + raw.cflag &= ~(termios.CSIZE | termios.PARENB) + raw.cflag |= (termios.CS8) + raw.lflag &= ~(termios.ICANON | termios.ECHO | + termios.IEXTEN | (termios.ISIG * 1)) raw.cc[termios.VMIN] = 1 raw.cc[termios.VTIME] = 0 tcsetattr(self.input_fd, termios.TCSADRAIN, raw) @@ -379,7 +375,7 @@ def prepare(self): self.height, self.width = self.getheightwidth() self.__buffer = [] - + self.__posxy = 0, 0 self.__gone_tall = 0 self.__move = self.__move_short @@ -409,10 +405,11 @@ def __sigwinch(self, signum, frame): def push_char(self, char): trace('push char {char!r}', char=char) self.event_queue.push(char) - + def get_event(self, block=1): while self.event_queue.empty(): - while 1: # All hail Unix! + while 1: + # All hail Unix! pyos_inputhook() try: self.push_char(os.read(self.input_fd, 1)) @@ -467,8 +464,9 @@ def getheightwidth(self): return int(os.environ["LINES"]), int(os.environ["COLUMNS"]) except KeyError: height, width = struct.unpack( - "hhhh", ioctl(self.input_fd, TIOCGWINSZ, b"\000"*8))[0:2] - if not height: return 25, 80 + "hhhh", ioctl(self.input_fd, TIOCGWINSZ, "\000"*8))[0:2] + if not height: + return 25, 80 return height, width else: def getheightwidth(self): @@ -507,7 +505,7 @@ def __tputs(self, fmt, prog=delayprog): os.write(self.output_fd, fmt[:x]) fmt = fmt[y:] delay = int(m.group(1)) - if b'*' in m.group(2): + if '*' in m.group(2): delay *= self.height if self._pad: nchars = (bps*delay)/1000 @@ -529,33 +527,33 @@ def beep(self): if FIONREAD: def getpending(self): - e = Event('key', '', b'') + e = Event('key', '', '') while not self.event_queue.empty(): e2 = self.event_queue.get() e.data += e2.data e.raw += e.raw - + amount = struct.unpack( - "i", ioctl(self.input_fd, FIONREAD, b"\0\0\0\0"))[0] - raw = os.read(self.input_fd, amount) - data = unicode(raw, self.encoding, 'replace') - e.data += data + "i", ioctl(self.input_fd, FIONREAD, "\0\0\0\0"))[0] + data = os.read(self.input_fd, amount) + raw = unicode(data, self.encoding, 'replace') + e.data += raw e.raw += raw return e else: def getpending(self): - e = Event('key', '', b'') + e = Event('key', '', '') while not self.event_queue.empty(): e2 = self.event_queue.get() e.data += e2.data e.raw += e.raw - + amount = 10000 - raw = os.read(self.input_fd, amount) - data = unicode(raw, self.encoding, 'replace') - e.data += data + data = os.read(self.input_fd, amount) + raw = unicode(data, self.encoding, 'replace') + e.data += raw e.raw += raw return e @@ -565,4 +563,3 @@ def clear(self): self.__move = self.__move_tall self.__posxy = 0, 0 self.screen = [] - From 7f1a0492b0290586d0960b1c678333542f5e5a74 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 03:58:51 +0100 Subject: [PATCH 25/31] fixup! write_history_file: do not nuke history on errors --- pyrepl/readline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyrepl/readline.py b/pyrepl/readline.py index 9b9a5af..f5a864c 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -348,9 +348,11 @@ def write_history_file(self, filename='~/.history'): fname = os.path.expanduser(filename) if PY2: - open(fname, 'w').write(entries) + f = open(fname, 'w') else: - open(fname, 'w', encoding='utf-8').write(entries) + f = open(fname, 'w', encoding='utf-8') + f.write(entries) + f.close() def clear_history(self): del self.get_reader().history[:] From 2d61f103d341b25985371e798b56fe63c7eca479 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 04:01:38 +0100 Subject: [PATCH 26/31] fixup! raw_input: pass returns_unicode properly --- pyrepl/readline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyrepl/readline.py b/pyrepl/readline.py index f5a864c..6212e6a 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -261,7 +261,8 @@ def raw_input(self, prompt=''): except _error: return _old_raw_input(prompt) reader.ps1 = prompt - return reader.readline(returns_unicode=True, startup_hook=self.startup_hook) + 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 From 19d6636a01747355d492c62997f90cca9f601c84 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Mar 2019 04:05:09 +0100 Subject: [PATCH 27/31] fixup! Keep code cleanup from master --- pyrepl/copy_code.py | 73 --------------------------------------------- pyrepl/readline.py | 69 ++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 106 deletions(-) delete mode 100644 pyrepl/copy_code.py diff --git a/pyrepl/copy_code.py b/pyrepl/copy_code.py deleted file mode 100644 index 9a0ec63..0000000 --- a/pyrepl/copy_code.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2000-2004 Michael Hudson-Doyle -# -# All Rights Reserved -# -# -# Permission to use, copy, modify, and distribute this software and -# its documentation for any purpose is hereby granted without fee, -# provided that the above copyright notice appear in all copies and -# that both that copyright notice and this permission notice appear in -# supporting documentation. -# -# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO -# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, -# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from types import CodeType - -def copy_code_with_changes(codeobject, - argcount=None, - nlocals=None, - stacksize=None, - flags=None, - code=None, - consts=None, - names=None, - varnames=None, - filename=None, - name=None, - firstlineno=None, - lnotab=None): - if argcount is None: argcount = codeobject.co_argcount - if nlocals is None: nlocals = codeobject.co_nlocals - if stacksize is None: stacksize = codeobject.co_stacksize - if flags is None: flags = codeobject.co_flags - if code is None: code = codeobject.co_code - if consts is None: consts = codeobject.co_consts - if names is None: names = codeobject.co_names - if varnames is None: varnames = codeobject.co_varnames - if filename is None: filename = codeobject.co_filename - if name is None: name = codeobject.co_name - if firstlineno is None: firstlineno = codeobject.co_firstlineno - if lnotab is None: lnotab = codeobject.co_lnotab - return CodeType(argcount, - nlocals, - stacksize, - flags, - code, - consts, - names, - varnames, - filename, - name, - firstlineno, - lnotab) - -code_attrs=['argcount', - 'nlocals', - 'stacksize', - 'flags', - 'code', - 'consts', - 'names', - 'varnames', - 'filename', - 'name', - 'firstlineno', - 'lnotab'] - - diff --git a/pyrepl/readline.py b/pyrepl/readline.py index 6212e6a..14a7d82 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -26,12 +26,12 @@ extensions for multiline input. """ -import sys, os +import sys +import os from pyrepl import commands from pyrepl.historical_reader import HistoricalReader from pyrepl.completing_reader import CompletingReader from pyrepl.unix_console import UnixConsole, _error - try: unicode PY2 = True @@ -41,41 +41,43 @@ ENCODING = sys.getfilesystemencoding() or 'latin1' # XXX review -__all__ = ['add_history', - 'clear_history', - 'get_begidx', - 'get_completer', - 'get_completer_delims', - 'get_current_history_length', - 'get_endidx', - 'get_history_item', - 'get_history_length', - 'get_line_buffer', - 'insert_text', - 'parse_and_bind', - 'read_history_file', - 'read_init_file', - 'redisplay', - 'remove_history_item', - 'replace_history_item', - 'set_completer', - 'set_completer_delims', - 'set_history_length', - 'set_pre_input_hook', - 'set_startup_hook', - 'write_history_file', - # ---- multiline extensions ---- - 'multiline_input', - ] +__all__ = [ + 'add_history', + 'clear_history', + 'get_begidx', + 'get_completer', + 'get_completer_delims', + 'get_current_history_length', + 'get_endidx', + 'get_history_item', + 'get_history_length', + 'get_line_buffer', + 'insert_text', + 'parse_and_bind', + 'read_history_file', + 'read_init_file', + 'redisplay', + 'remove_history_item', + 'replace_history_item', + 'set_completer', + 'set_completer_delims', + 'set_history_length', + 'set_pre_input_hook', + 'set_startup_hook', + 'write_history_file', + # ---- multiline extensions ---- + 'multiline_input', +] # ____________________________________________________________ + class ReadlineConfig(object): readline_completer = None completer_delims = dict.fromkeys(' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?') -class ReadlineAlikeReader(HistoricalReader, CompletingReader): +class ReadlineAlikeReader(HistoricalReader, CompletingReader): assume_immutable_completions = False use_brackets = False sort_in_column = True @@ -198,7 +200,7 @@ def _get_previous_line_indent(buffer, pos): class maybe_accept(commands.Command): def do(self): r = self.reader - r.dirty = 1 # this is needed to hide the completion menu, if visible + r.dirty = 1 # this is needed to hide the completion menu, if visible # # if there are already several lines and the cursor # is not on the last one, always insert a new \n. @@ -444,6 +446,7 @@ def insert_text(self, text): # ____________________________________________________________ # Stubs + def _make_stub(_name, _ret): def stub(*args, **kwds): import warnings @@ -455,16 +458,16 @@ def stub(*args, **kwds): ('read_init_file', None), ('redisplay', None), ('set_pre_input_hook', None), - ]: +]: assert _name not in globals(), _name _make_stub(_name, _ret) -# ____________________________________________________________ def _setup(): global _old_raw_input if _old_raw_input is not None: - return # don't run _setup twice + # Don't run _setup twice. + return try: f_in = sys.stdin.fileno() From 40cb460247aaf5192dccf51c1072f1ef4fc1e633 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Mar 2019 12:31:53 +0100 Subject: [PATCH 28/31] pyrepl/completing_reader.py: keep most of master, flake8 clean --- pyrepl/completing_reader.py | 81 +++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/pyrepl/completing_reader.py b/pyrepl/completing_reader.py index bcdea19..6deed9c 100644 --- a/pyrepl/completing_reader.py +++ b/pyrepl/completing_reader.py @@ -18,11 +18,12 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +import re from pyrepl import commands, reader from pyrepl.reader import Reader -def prefix(wordlist, j = 0): +def prefix(wordlist, j=0): d = {} i = j try: @@ -36,14 +37,18 @@ def prefix(wordlist, j = 0): except IndexError: return wordlist[0][j:i] -import re + +STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]") + + def stripcolor(s): - return stripcolor.regexp.sub('', s) -stripcolor.regexp = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]") + return STRIPCOLOR_REGEX.sub('', s) + def real_len(s): return len(stripcolor(s)) + def left_align(s, maxlen): stripped = stripcolor(s) if len(stripped) > maxlen: @@ -52,6 +57,7 @@ def left_align(s, maxlen): padding = maxlen - len(stripped) return s + ' '*padding + def build_menu(cons, wordlist, start, use_brackets, sort_in_column): if use_brackets: item = "[ %s ]" @@ -61,19 +67,19 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): padding = 2 maxlen = min(max(map(real_len, wordlist)), cons.width - padding) cols = cons.width // (maxlen + padding) - rows = (len(wordlist) - 1)//cols + 1 + rows = (len(wordlist) - 1) // cols + 1 if sort_in_column: # sort_in_column=False (default) sort_in_column=True # A B C A D G - # D E F B E + # D E F B E # G C F # # "fill" the table with empty words, so we always have the same amout # of rows for each column missing = cols*rows - len(wordlist) wordlist = wordlist + ['']*missing - indexes = [(i%cols)*rows + i//cols for i in range(len(wordlist))] + indexes = [(i % cols) * rows + i // cols for i in range(len(wordlist))] wordlist = [wordlist[i] for i in indexes] menu = [] i = start @@ -84,14 +90,14 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): i += 1 if i >= len(wordlist): break - menu.append( ''.join(row) ) + menu.append(''.join(row)) if i >= len(wordlist): i = 0 break if r + 5 > cons.height: - menu.append(" %d more... "%(len(wordlist) - i)) + menu.append(" %d more... " % (len(wordlist) - i)) break - return menu, i + return menu, i # this gets somewhat user interface-y, and as a result the logic gets # very convoluted. @@ -99,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 @@ -118,7 +124,7 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): # only if the ``assume_immutable_completions`` is True. # # now it gets complicated. -# +# # for the first press of a completion key: # if there's a common prefix, stick it in. @@ -140,22 +146,22 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): # for subsequent bangs, rotate the menu around (if there are sufficient # choices). + class complete(commands.Command): def do(self): r = self.reader + last_is_completer = r.last_command_is(self.__class__) + immutable_completions = r.assume_immutable_completions + completions_unchangable = last_is_completer and immutable_completions stem = r.get_stem() - if r.assume_immutable_completions and \ - r.last_command_is(self.__class__): - completions = r.cmpltn_menu_choices - else: - r.cmpltn_menu_choices = completions = \ - r.get_completions(stem) - if len(completions) == 0: + if not completions_unchangable: + r.cmpltn_menu_choices = r.get_completions(stem) + + completions = r.cmpltn_menu_choices + if not completions: r.error("no matches") elif len(completions) == 1: - if r.assume_immutable_completions and \ - len(completions[0]) == len(stem) and \ - r.last_command_is(self.__class__): + if completions_unchangable and len(completions[0]) == len(stem): r.msg = "[ sole completion ]" r.dirty = 1 r.insert(completions[0][len(stem):]) @@ -163,7 +169,7 @@ def do(self): p = prefix(completions, len(stem)) if p: r.insert(p) - if r.last_command_is(self.__class__): + if last_is_completer: if not r.cmpltn_menu_vis: r.cmpltn_menu_vis = 1 r.cmpltn_menu, r.cmpltn_menu_end = build_menu( @@ -177,6 +183,7 @@ def do(self): r.msg = "[ not unique ]" r.dirty = 1 + class self_insert(commands.self_insert): def do(self): commands.self_insert.do(self) @@ -195,6 +202,7 @@ def do(self): else: r.cmpltn_reset() + class CompletingReader(Reader): """Adds completion support @@ -204,26 +212,25 @@ class CompletingReader(Reader): """ # see the comment for the complete command assume_immutable_completions = True - use_brackets = True # display completions inside [] + use_brackets = True # display completions inside [] sort_in_column = False - + def collect_keymap(self): return super(CompletingReader, self).collect_keymap() + ( (r'\t', 'complete'),) - + def __init__(self, console): super(CompletingReader, self).__init__(console) self.cmpltn_menu = ["[ menu 1 ]", "[ menu 2 ]"] self.cmpltn_menu_vis = 0 self.cmpltn_menu_end = 0 - for c in [complete, self_insert]: + for c in (complete, self_insert): self.commands[c.__name__] = c - self.commands[c.__name__.replace('_', '-')] = c + self.commands[c.__name__.replace('_', '-')] = c def after_command(self, cmd): super(CompletingReader, self).after_command(cmd) - if not isinstance(cmd, self.commands['complete']) \ - and not isinstance(cmd, self.commands['self_insert']): + if not isinstance(cmd, (complete, self_insert)): self.cmpltn_reset() def calc_screen(self): @@ -243,7 +250,7 @@ def cmpltn_reset(self): self.cmpltn_menu = [] self.cmpltn_menu_vis = 0 self.cmpltn_menu_end = 0 - self.cmpltn_menu_choices = [] + self.cmpltn_menu_choices = [] def get_stem(self): st = self.syntax_table @@ -257,18 +264,22 @@ def get_stem(self): def get_completions(self, stem): return [] + def test(): class TestReader(CompletingReader): def get_completions(self, stem): - return [s for l in map(lambda x:x.split(),self.history) - for s in l if s and s.startswith(stem)] + return [s for l in self.history + for s in l.split() + if s and s.startswith(stem)] + reader = TestReader() reader.ps1 = "c**> " reader.ps2 = "c/*> " reader.ps3 = "c|*> " - reader.ps4 = "c\*> " + reader.ps4 = r"c\*> " while reader.readline(): pass -if __name__=='__main__': + +if __name__ == '__main__': test() From 655cc50cb3290bb501b0c9067c251660f8cac7b6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Mar 2019 12:56:50 +0100 Subject: [PATCH 29/31] Remove pyrepl/_minimal_curses.py PyPy appears to ship its own, and it is not required for CPython. --- pyrepl/_minimal_curses.py | 75 --------------------------------------- 1 file changed, 75 deletions(-) delete mode 100644 pyrepl/_minimal_curses.py diff --git a/pyrepl/_minimal_curses.py b/pyrepl/_minimal_curses.py deleted file mode 100644 index f61b04e..0000000 --- a/pyrepl/_minimal_curses.py +++ /dev/null @@ -1,75 +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 -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 From 9a0ff78be48421a257b522a9751f8da6e8ff96e4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Mar 2019 13:00:56 +0100 Subject: [PATCH 30/31] keep pyrepl/cmdrepl.py from master --- pyrepl/cmdrepl.py | 53 +++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/pyrepl/cmdrepl.py b/pyrepl/cmdrepl.py index ce3eb23..8b500b0 100644 --- a/pyrepl/cmdrepl.py +++ b/pyrepl/cmdrepl.py @@ -35,26 +35,28 @@ from __future__ import print_function -from pyrepl import completing_reader as cr, reader, completer +from pyrepl import completer from pyrepl.completing_reader import CompletingReader as CR import cmd + class CmdReader(CR): def collect_keymap(self): return super(CmdReader, self).collect_keymap() + ( ("\\M-\\n", "invalid-key"), ("\\n", "accept")) - - CR_init = CR.__init__ + def __init__(self, completions): - self.CR_init(self) + super(CmdReader, self).__init__() self.completions = completions def get_completions(self, stem): if len(stem) != self.pos: return [] - return sorted(set(s for s in self.completions - if s.startswith(stem))) + return sorted(set(s + for s in self.completions + if s.startswith(stem))) + def replize(klass, history_across_invocations=1): @@ -71,26 +73,25 @@ def replize(klass, history_across_invocations=1): for s in completer.get_class_members(klass) if s.startswith("do_")] - if not issubclass(klass, cmd.Cmd): - raise Exception + assert issubclass(klass, cmd.Cmd) # if klass.cmdloop.im_class is not cmd.Cmd: # print "this may not work" - class CmdRepl(klass): - k_init = klass.__init__ - - if history_across_invocations: - _CmdRepl__history = [] - def __init__(self, *args, **kw): - self.k_init(*args, **kw) - self.__reader = CmdReader(completions) - self.__reader.history = CmdRepl._CmdRepl__history - self.__reader.historyi = len(CmdRepl._CmdRepl__history) - else: - def __init__(self, *args, **kw): - self.k_init(*args, **kw) - self.__reader = CmdReader(completions) - + class MultiHist(object): + __history = [] + + def __init__(self, *args, **kw): + super(MultiHist, self).__init__(*args, **kw) + self.__reader = CmdReader(completions) + self.__reader.history = self.__history + self.__reader.historyi = len(self.__history) + + class SimpleHist(object): + def __init__(self, *args, **kw): + super(SimpleHist, self).__init__(*args, **kw) + self.__reader = CmdReader(completions) + + class CmdLoopMixin(object): def cmdloop(self, intro=None): self.preloop() if intro is not None: @@ -113,6 +114,8 @@ def cmdloop(self, intro=None): stop = self.postcmd(stop, line) self.postloop() - CmdRepl.__name__ = "replize(%s.%s)"%(klass.__module__, klass.__name__) - return CmdRepl + hist = MultiHist if history_across_invocations else SimpleHist + class CmdRepl(hist, CmdLoopMixin, klass): + __name__ = "replize(%s.%s)" % (klass.__module__, klass.__name__) + return CmdRepl From 63ce133bb998b3b0fe29e6fab426e91e076b3dd9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Mar 2019 13:05:00 +0100 Subject: [PATCH 31/31] remove pyrepl/unicodedata_.py Not required due to 640d111. Should also have been removed in PyPy py3.6 branch with 58411:be161e2bc46a. --- pyrepl/unicodedata_.py | 59 ------------------------------------------ 1 file changed, 59 deletions(-) delete mode 100644 pyrepl/unicodedata_.py diff --git a/pyrepl/unicodedata_.py b/pyrepl/unicodedata_.py deleted file mode 100644 index 04e6c97..0000000 --- a/pyrepl/unicodedata_.py +++ /dev/null @@ -1,59 +0,0 @@ -try: - from unicodedata import * -except ImportError: - - def category(ch): - """ - ASCII only implementation - """ - if type(ch) is not unicode: - raise TypeError - if len(ch) != 1: - raise TypeError - return _categories.get(ord(ch), 'Co') # "Other, private use" - - _categories = { - 0: 'Cc', 1: 'Cc', 2: 'Cc', 3: 'Cc', 4: 'Cc', 5: 'Cc', - 6: 'Cc', 7: 'Cc', 8: 'Cc', 9: 'Cc', 10: 'Cc', 11: 'Cc', - 12: 'Cc', 13: 'Cc', 14: 'Cc', 15: 'Cc', 16: 'Cc', 17: 'Cc', - 18: 'Cc', 19: 'Cc', 20: 'Cc', 21: 'Cc', 22: 'Cc', 23: 'Cc', - 24: 'Cc', 25: 'Cc', 26: 'Cc', 27: 'Cc', 28: 'Cc', 29: 'Cc', - 30: 'Cc', 31: 'Cc', 32: 'Zs', 33: 'Po', 34: 'Po', 35: 'Po', - 36: 'Sc', 37: 'Po', 38: 'Po', 39: 'Po', 40: 'Ps', 41: 'Pe', - 42: 'Po', 43: 'Sm', 44: 'Po', 45: 'Pd', 46: 'Po', 47: 'Po', - 48: 'Nd', 49: 'Nd', 50: 'Nd', 51: 'Nd', 52: 'Nd', 53: 'Nd', - 54: 'Nd', 55: 'Nd', 56: 'Nd', 57: 'Nd', 58: 'Po', 59: 'Po', - 60: 'Sm', 61: 'Sm', 62: 'Sm', 63: 'Po', 64: 'Po', 65: 'Lu', - 66: 'Lu', 67: 'Lu', 68: 'Lu', 69: 'Lu', 70: 'Lu', 71: 'Lu', - 72: 'Lu', 73: 'Lu', 74: 'Lu', 75: 'Lu', 76: 'Lu', 77: 'Lu', - 78: 'Lu', 79: 'Lu', 80: 'Lu', 81: 'Lu', 82: 'Lu', 83: 'Lu', - 84: 'Lu', 85: 'Lu', 86: 'Lu', 87: 'Lu', 88: 'Lu', 89: 'Lu', - 90: 'Lu', 91: 'Ps', 92: 'Po', 93: 'Pe', 94: 'Sk', 95: 'Pc', - 96: 'Sk', 97: 'Ll', 98: 'Ll', 99: 'Ll', 100: 'Ll', 101: 'Ll', - 102: 'Ll', 103: 'Ll', 104: 'Ll', 105: 'Ll', 106: 'Ll', 107: 'Ll', - 108: 'Ll', 109: 'Ll', 110: 'Ll', 111: 'Ll', 112: 'Ll', 113: 'Ll', - 114: 'Ll', 115: 'Ll', 116: 'Ll', 117: 'Ll', 118: 'Ll', 119: 'Ll', - 120: 'Ll', 121: 'Ll', 122: 'Ll', 123: 'Ps', 124: 'Sm', 125: 'Pe', - 126: 'Sm', 127: 'Cc', 128: 'Cc', 129: 'Cc', 130: 'Cc', 131: 'Cc', - 132: 'Cc', 133: 'Cc', 134: 'Cc', 135: 'Cc', 136: 'Cc', 137: 'Cc', - 138: 'Cc', 139: 'Cc', 140: 'Cc', 141: 'Cc', 142: 'Cc', 143: 'Cc', - 144: 'Cc', 145: 'Cc', 146: 'Cc', 147: 'Cc', 148: 'Cc', 149: 'Cc', - 150: 'Cc', 151: 'Cc', 152: 'Cc', 153: 'Cc', 154: 'Cc', 155: 'Cc', - 156: 'Cc', 157: 'Cc', 158: 'Cc', 159: 'Cc', 160: 'Zs', 161: 'Po', - 162: 'Sc', 163: 'Sc', 164: 'Sc', 165: 'Sc', 166: 'So', 167: 'So', - 168: 'Sk', 169: 'So', 170: 'Ll', 171: 'Pi', 172: 'Sm', 173: 'Cf', - 174: 'So', 175: 'Sk', 176: 'So', 177: 'Sm', 178: 'No', 179: 'No', - 180: 'Sk', 181: 'Ll', 182: 'So', 183: 'Po', 184: 'Sk', 185: 'No', - 186: 'Ll', 187: 'Pf', 188: 'No', 189: 'No', 190: 'No', 191: 'Po', - 192: 'Lu', 193: 'Lu', 194: 'Lu', 195: 'Lu', 196: 'Lu', 197: 'Lu', - 198: 'Lu', 199: 'Lu', 200: 'Lu', 201: 'Lu', 202: 'Lu', 203: 'Lu', - 204: 'Lu', 205: 'Lu', 206: 'Lu', 207: 'Lu', 208: 'Lu', 209: 'Lu', - 210: 'Lu', 211: 'Lu', 212: 'Lu', 213: 'Lu', 214: 'Lu', 215: 'Sm', - 216: 'Lu', 217: 'Lu', 218: 'Lu', 219: 'Lu', 220: 'Lu', 221: 'Lu', - 222: 'Lu', 223: 'Ll', 224: 'Ll', 225: 'Ll', 226: 'Ll', 227: 'Ll', - 228: 'Ll', 229: 'Ll', 230: 'Ll', 231: 'Ll', 232: 'Ll', 233: 'Ll', - 234: 'Ll', 235: 'Ll', 236: 'Ll', 237: 'Ll', 238: 'Ll', 239: 'Ll', - 240: 'Ll', 241: 'Ll', 242: 'Ll', 243: 'Ll', 244: 'Ll', 245: 'Ll', - 246: 'Ll', 247: 'Sm', 248: 'Ll', 249: 'Ll', 250: 'Ll', 251: 'Ll', - 252: 'Ll', 253: 'Ll', 254: 'Ll' - }