diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76e8e84..36c2af4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,92 +21,53 @@ jobs: strategy: fail-fast: false matrix: - include: - # Linux - - tox_env: "py39-coverage" - python: "3.9" - os: ubuntu-20.04 - - tox_env: "py38-coverage" - python: "3.8" - os: ubuntu-20.04 - - tox_env: "py37-coverage" - python: "3.7" - os: ubuntu-20.04 - - tox_env: "py36-coverage" - python: "3.6" - os: ubuntu-20.04 - - tox_env: "py35-coverage" - python: "3.5" - os: ubuntu-20.04 - - - tox_env: "py27-coverage" - python: "2.7" - os: ubuntu-20.04 - - - tox_env: "pypy3-coverage" - python: "pypy-3.7" - os: ubuntu-20.04 - - tox_env: "pypy-coverage" - python: "pypy-2.7" - os: ubuntu-20.04 + os: [ubuntu-latest] + python: ["3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - # Caching. + - name: Set tox_env + run: | + pyv=$(echo ${{matrix.python}} | sed 's/\.//') + tox_env=py${pyv}-coverage + echo "tox env name: ${tox_env}" + echo "tox_env=${tox_env}">> $GITHUB_ENV + - name: set PY_CACHE_KEY run: echo "PY_CACHE_KEY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV - name: Cache .tox - uses: actions/cache@v1 + uses: actions/cache@v3 with: - path: ${{ github.workspace }}/.tox/${{ matrix.tox_env }} - key: "tox|${{ matrix.os }}|${{ matrix.tox_env }}|${{ env.PY_CACHE_KEY }}|${{ hashFiles('tox.ini', 'setup.*') }}" + path: ${{ github.workspace }}/.tox/${{ env.tox_env }} + key: "tox|${{ matrix.os }}|${{ env.tox_env }}|${{ env.PY_CACHE_KEY }}|${{ hashFiles('tox.ini', 'setup.*') }}" - - name: (Initial) version information/pinning + - name: Install/update tools run: | - set -x - python -m site - python -m pip --version - python -m pip list - if [[ "${{ matrix.python }}" == "3.4" ]]; then - # Install latest available pip. - # 7.1.2 (installed) is too old to not install too new packages, - # including pip itself. 19.2 dropped support for Python 3.4. - python -m pip install -U pip==19.1.1 - fi - python -m pip install -U setuptools==42.0.2 - python -m pip install -U virtualenv==20.4.3 - - - name: Install tox - run: python -m pip install git+https://github.com/blueyed/tox@master - - - name: Version information - run: python -m pip list + pip install -U pip setuptools virtualenv tox - name: Setup tox environment id: setup-tox - run: python -m tox --notest -v --durations -e ${{ matrix.tox_env }} + run: tox --notest -v -e ${{ env.tox_env }} - name: Test env: - COLUMNS: "90" # better alignment (working around https://github.com/blueyed/pytest/issues/491). + COLUMNS: "90" # better alignment (working around https://github.com/blueyed/pytest/issues/491). PY_COLORS: "1" # UTF-8 mode for Windows (https://docs.python.org/3/using/windows.html#utf-8-mode). PYTHONUTF8: "1" TOX_TESTENV_PASSENV: "PYTHONUTF8" - run: python -m tox -v --durations -e ${{ matrix.tox_env }} + run: tox -v -e ${{ env.tox_env }} - name: Report coverage - if: always() && (steps.setup-tox.outcome == 'success' && contains(matrix.tox_env, '-coverage')) - uses: codecov/codecov-action@v1 + if: always() && (steps.setup-tox.outcome == 'success' && contains(env.tox_env, '-coverage')) + uses: codecov/codecov-action@v3 with: files: ./coverage.xml flags: ${{ runner.os }} - name: ${{ matrix.tox_env }} + name: ${{ env.tox_env }} fail_ci_if_error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0894441 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Publish package to PyPI + +on: + release: + types: + - published + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.9" + - name: Install requirements + run: | + pip install -U pip twine build + - name: Build + run: python -m build + - run: check-manifest + - run: twine check dist/* + - name: Publish to PyPI + env: + TWINE_USERNAME: "__token__" + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a790198 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +*.mo +*.pot +*.log + +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +.mypy_cache/ +.dmypy.json +dmypy.json + +.ruff_cache/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7ba9ffc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -dist: xenial -language: python - -env: - global: - - PYTEST_ADDOPTS="-vv" - -jobs: - include: - - python: '3.4' - env: - - TOXENV=py34-coverage - - PYTEST="pytest @ git+https://github.com/blueyed/pytest@my-4.6-maintenance" - -install: - - pip install tox - -script: - - tox --force-dep="$PYTEST" - -after_script: - - | - if [[ "${TOXENV%-coverage}" != "$TOXENV" ]]; then - bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml -n $TOXENV - fi - -# Only master and releases. PRs are used otherwise. -branches: - only: - - master - - /^\d+\.\d+(\.\d+)?(-\S*)?$/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..43e1ec4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", +] +ignore = ["F401"] diff --git a/pyrepl/_minimal_curses.py b/pyrepl/_minimal_curses.py index 79af03f..65621d3 100644 --- a/pyrepl/_minimal_curses.py +++ b/pyrepl/_minimal_curses.py @@ -17,7 +17,7 @@ class error(Exception): def _find_clib(): - trylibs = ['ncursesw', 'ncurses', 'curses'] + trylibs = ["ncursesw", "ncurses", "curses"] for lib in trylibs: path = ctypes.util.find_library(lib) @@ -25,11 +25,11 @@ def _find_clib(): 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.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] @@ -45,27 +45,25 @@ def _find_clib(): try: from __pypy__ import builtinify - builtinify # silence broken pyflakes except ImportError: - builtinify = lambda f: f + def builtinify(f): + return f @builtinify def setupterm(termstr, fd): - if termstr is not None: - if not isinstance(termstr, bytes): - termstr = termstr.encode() + if termstr is not None and not isinstance(termstr, bytes): + termstr = termstr.encode() err = ctypes.c_int(0) result = clib.setupterm(termstr, fd, ctypes.byref(err)) if result == ERR: - raise error("setupterm(%r, %d) failed (err=%d)" % ( - termstr, fd, err.value)) + raise error("setupterm(%r, %d) failed (err=%d)" % (termstr, fd, err.value)) @builtinify def tigetstr(cap): if not isinstance(cap, bytes): - cap = cap.encode('ascii') + cap = cap.encode("ascii") result = clib.tigetstr(cap) if ctypes.cast(result, ctypes.c_void_p).value == ERR: return None diff --git a/pyrepl/cmdrepl.py b/pyrepl/cmdrepl.py index 8b500b0..87acaab 100644 --- a/pyrepl/cmdrepl.py +++ b/pyrepl/cmdrepl.py @@ -33,33 +33,31 @@ which is in fact done by the `pythoni' script that comes with pyrepl.""" -from __future__ import print_function + +import cmd 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() + ( + return super().collect_keymap() + ( ("\\M-\\n", "invalid-key"), - ("\\n", "accept")) + ("\\n", "accept"), + ) def __init__(self, completions): - super(CmdReader, self).__init__() + super().__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): - """Return a subclass of the cmd.Cmd-derived klass that uses pyrepl instead of readline. @@ -69,29 +67,29 @@ def replize(klass, history_across_invocations=1): controls whether instances of the returned class share histories.""" - completions = [s[3:] - for s in completer.get_class_members(klass) - if s.startswith("do_")] + completions = [ + s[3:] for s in completer.get_class_members(klass) if s.startswith("do_") + ] assert issubclass(klass, cmd.Cmd) -# if klass.cmdloop.im_class is not cmd.Cmd: -# print "this may not work" + # if klass.cmdloop.im_class is not cmd.Cmd: + # print "this may not work" - class MultiHist(object): + class MultiHist: __history = [] def __init__(self, *args, **kw): - super(MultiHist, self).__init__(*args, **kw) + super().__init__(*args, **kw) self.__reader = CmdReader(completions) self.__reader.history = self.__history self.__reader.historyi = len(self.__history) - class SimpleHist(object): + class SimpleHist: def __init__(self, *args, **kw): - super(SimpleHist, self).__init__(*args, **kw) + super().__init__(*args, **kw) self.__reader = CmdReader(completions) - class CmdLoopMixin(object): + class CmdLoopMixin: def cmdloop(self, intro=None): self.preloop() if intro is not None: @@ -117,5 +115,6 @@ def cmdloop(self, intro=None): hist = MultiHist if history_across_invocations else SimpleHist class CmdRepl(hist, CmdLoopMixin, klass): - __name__ = "replize(%s.%s)" % (klass.__module__, klass.__name__) + __name__ = f"replize({klass.__module__}.{klass.__name__})" + return CmdRepl diff --git a/pyrepl/commands.py b/pyrepl/commands.py index 62322f8..ba7063b 100644 --- a/pyrepl/commands.py +++ b/pyrepl/commands.py @@ -19,7 +19,9 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import sys, os +import os + +from pyrepl import input # noqa: F401 # Catgories of actions: # killing @@ -30,7 +32,8 @@ # finishing # [completion] -class Command(object): + +class Command: finish = 0 kills_digit_arg = 1 @@ -42,6 +45,7 @@ def __init__(self, reader, event_name, event): def do(self): pass + class KillCommand(Command): def kill_range(self, start, end): if start == end: @@ -60,29 +64,38 @@ def kill_range(self, start, end): r.pos = start r.dirty = 1 + class YankCommand(Command): pass + class MotionCommand(Command): pass + class EditCommand(Command): pass + class FinishCommand(Command): finish = 1 pass + def is_kill(command): return command and issubclass(command, KillCommand) + def is_yank(command): return command and issubclass(command, YankCommand) + # etc + class digit_arg(Command): kills_digit_arg = 0 + def do(self): r = self.reader c = self.event[-1] @@ -97,64 +110,74 @@ def do(self): r.arg = d else: if r.arg < 0: - r.arg = 10*r.arg - d + r.arg = 10 * r.arg - d else: - r.arg = 10*r.arg + d + r.arg = 10 * r.arg + d r.dirty = 1 + class clear_screen(Command): def do(self): r = self.reader r.console.clear() r.dirty = 1 + class refresh(Command): def do(self): self.reader.dirty = 1 + class repaint(Command): def do(self): self.reader.dirty = 1 self.reader.console.repaint_prep() + class kill_line(KillCommand): def do(self): r = self.reader b = r.buffer eol = r.eol() - for c in b[r.pos:eol]: + for c in b[r.pos : eol]: if not c.isspace(): self.kill_range(r.pos, eol) return else: - self.kill_range(r.pos, eol+1) + self.kill_range(r.pos, eol + 1) + class unix_line_discard(KillCommand): def do(self): r = self.reader self.kill_range(r.bol(), r.pos) + # XXX unix_word_rubout and backward_kill_word should actually # do different things... + class unix_word_rubout(KillCommand): def do(self): r = self.reader - for i in range(r.get_arg()): + for _i in range(r.get_arg()): self.kill_range(r.bow(), r.pos) + class kill_word(KillCommand): def do(self): r = self.reader - for i in range(r.get_arg()): + for _i in range(r.get_arg()): self.kill_range(r.pos, r.eow()) + class backward_kill_word(KillCommand): def do(self): r = self.reader - for i in range(r.get_arg()): + for _i in range(r.get_arg()): self.kill_range(r.bow(), r.pos) + class yank(YankCommand): def do(self): r = self.reader @@ -163,6 +186,7 @@ def do(self): return r.insert(r.kill_ring[-1]) + class yank_pop(YankCommand): def do(self): r = self.reader @@ -176,19 +200,23 @@ def do(self): repl = len(r.kill_ring[-1]) r.kill_ring.insert(0, r.kill_ring.pop()) t = r.kill_ring[-1] - b[r.pos - repl:r.pos] = t + b[r.pos - repl : r.pos] = t r.pos = r.pos - repl + len(t) r.dirty = 1 + class interrupt(FinishCommand): def do(self): import signal + self.reader.console.finish() os.kill(os.getpid(), signal.SIGINT) + class suspend(Command): def do(self): import signal + r = self.reader p = r.pos r.console.finish() @@ -201,10 +229,11 @@ def do(self): r.dirty = 1 r.console.screen = [] + class up(MotionCommand): def do(self): r = self.reader - for i in range(r.get_arg()): + for _i in range(r.get_arg()): bol1 = r.bol() if bol1 == 0: if r.historyi > 0: @@ -213,7 +242,7 @@ def do(self): r.pos = 0 r.error("start of buffer") return - bol2 = r.bol(bol1-1) + bol2 = r.bol(bol1 - 1) line_pos = r.pos - bol1 if line_pos > bol1 - bol2 - 1: r.sticky_y = line_pos @@ -221,11 +250,12 @@ def do(self): else: r.pos = bol2 + line_pos + class down(MotionCommand): def do(self): r = self.reader b = r.buffer - for i in range(r.get_arg()): + for _i in range(r.get_arg()): bol1 = r.bol() eol1 = r.eol() if eol1 == len(b): @@ -236,72 +266,82 @@ def do(self): r.pos = len(b) r.error("end of buffer") return - eol2 = r.eol(eol1+1) + eol2 = r.eol(eol1 + 1) if r.pos - bol1 > eol2 - eol1 - 1: r.pos = eol2 else: r.pos = eol1 + (r.pos - bol1) + 1 + class left(MotionCommand): def do(self): r = self.reader - for i in range(r.get_arg()): + for _i in range(r.get_arg()): p = r.pos - 1 if p >= 0: r.pos = p else: self.reader.error("start of buffer") + class right(MotionCommand): def do(self): r = self.reader b = r.buffer - for i in range(r.get_arg()): + for _i in range(r.get_arg()): p = r.pos + 1 if p <= len(b): r.pos = p else: self.reader.error("end of buffer") + class beginning_of_line(MotionCommand): def do(self): self.reader.pos = self.reader.bol() + class end_of_line(MotionCommand): def do(self): - r = self.reader self.reader.pos = self.reader.eol() + class home(MotionCommand): def do(self): self.reader.pos = 0 - + + class end(MotionCommand): def do(self): self.reader.pos = len(self.reader.buffer) - + + class forward_word(MotionCommand): def do(self): r = self.reader - for i in range(r.get_arg()): + for _i in range(r.get_arg()): r.pos = r.eow() - + + class backward_word(MotionCommand): def do(self): r = self.reader - for i in range(r.get_arg()): + for _i in range(r.get_arg()): r.pos = r.bow() + class self_insert(EditCommand): def do(self): r = self.reader r.insert(self.event * r.get_arg()) + class insert_nl(EditCommand): def do(self): r = self.reader r.insert("\n" * r.get_arg()) + class transpose_characters(EditCommand): def do(self): r = self.reader @@ -319,11 +359,12 @@ def do(self): r.pos = t r.dirty = 1 + class backspace(EditCommand): def do(self): r = self.reader b = r.buffer - for i in range(r.get_arg()): + for _i in range(r.get_arg()): if r.pos > 0: r.pos -= 1 del b[r.pos] @@ -331,41 +372,50 @@ def do(self): else: self.reader.error("can't backspace at start") + class delete(EditCommand): def do(self): r = self.reader b = r.buffer - if ( r.pos == 0 and len(b) == 0 # this is something of a hack - and self.event[-1] == "\004"): + if ( + r.pos == 0 + and len(b) == 0 # this is something of a hack + and self.event[-1] == "\004" + ): r.update_screen() r.console.finish() raise EOFError - for i in range(r.get_arg()): + for _i in range(r.get_arg()): if r.pos != len(b): del b[r.pos] r.dirty = 1 else: self.reader.error("end of buffer") + class accept(FinishCommand): def do(self): pass + class help(Command): def do(self): self.reader.msg = self.reader.help_text self.reader.dirty = 1 + class invalid_key(Command): def do(self): pending = self.reader.console.getpending() - s = ''.join(self.event) + pending.data - self.reader.error("`%r' not bound"%s) + s = "".join(self.event) + pending.data + self.reader.error("`%r' not bound" % s) + class invalid_command(Command): def do(self): s = self.event_name - self.reader.error("command `%s' not known"%s) + self.reader.error("command `%s' not known" % s) + class qIHelp(Command): def do(self): @@ -373,19 +423,21 @@ def do(self): r = self.reader pending = r.console.getpending().data - disp = disp_str((self.event + pending))[0] + disp = disp_str(self.event + pending)[0] r.insert(disp * r.get_arg()) r.pop_input_trans() -from pyrepl import input -class QITrans(object): +class QITrans: def push(self, evt): self.evt = evt + def get(self): - return ('qIHelp', self.evt.data) + return ("qIHelp", self.evt.data) + class quoted_insert(Command): kills_digit_arg = 0 + def do(self): self.reader.push_input_trans(QITrans()) diff --git a/pyrepl/completer.py b/pyrepl/completer.py index 45f40c1..9a65d49 100644 --- a/pyrepl/completer.py +++ b/pyrepl/completer.py @@ -17,22 +17,18 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -try: - import __builtin__ as builtins - builtins # silence broken pyflakes -except ImportError: - import builtins +import builtins -class Completer(object): +class Completer: def __init__(self, ns): self.ns = ns def complete(self, text): if "." in text: return self.attr_matches(text) - else: - return self.global_matches(text) + + return self.global_matches(text) def global_matches(self, text): """Compute matches when text is a simple name. @@ -42,11 +38,15 @@ def global_matches(self, text): """ import keyword + matches = [] - for list in [keyword.kwlist, - builtins.__dict__.keys(), - self.ns.keys()]: - for word in list: + + for list_ in [ + keyword.kwlist, + list(builtins.__dict__.keys()), + list(self.ns.keys()), + ]: + for word in list_: if word.startswith(text) and word != "__builtins__": matches.append(word) return matches @@ -65,26 +65,27 @@ def attr_matches(self, text): """ import re + m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text) if not m: return [] expr, attr = m.group(1, 3) object = eval(expr, self.ns) words = dir(object) - if hasattr(object, '__class__'): - words.append('__class__') + if hasattr(object, "__class__"): + words.append("__class__") words = words + get_class_members(object.__class__) matches = [] n = len(attr) for word in words: if word[:n] == attr and word != "__builtins__": - matches.append("%s.%s" % (expr, word)) + matches.append(f"{expr}.{word}") return matches def get_class_members(klass): ret = dir(klass) - if hasattr(klass, '__bases__'): + 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 6039688..aba3808 100644 --- a/pyrepl/completing_reader.py +++ b/pyrepl/completing_reader.py @@ -19,6 +19,7 @@ # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import re + from pyrepl import commands, reader from pyrepl.reader import Reader @@ -40,8 +41,9 @@ def prefix(wordlist, j=0): STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]") + def stripcolor(s): - return STRIPCOLOR_REGEX.sub('', s) + return STRIPCOLOR_REGEX.sub("", s) def real_len(s): @@ -54,7 +56,7 @@ def left_align(s, maxlen): # too bad, we remove the color return stripped[:maxlen] padding = maxlen - len(stripped) - return s + ' '*padding + return s + " " * padding def build_menu(cons, wordlist, start, use_brackets, sort_in_column): @@ -64,9 +66,9 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): else: item = "%s " padding = 2 - maxlen = min(max(map(real_len, wordlist)), cons.width - padding) + maxlen = min(max(list(map(real_len, wordlist))), cons.width - padding) cols = int(cons.width / (maxlen + padding)) - rows = int((len(wordlist) - 1)/cols + 1) + rows = int((len(wordlist) - 1) / cols + 1) if sort_in_column: # sort_in_column=False (default) sort_in_column=True @@ -76,20 +78,20 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): # # "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 + missing = cols * rows - len(wordlist) + wordlist = wordlist + [""] * missing indexes = [(i % cols) * rows + i // cols for i in range(len(wordlist))] wordlist = [wordlist[i] for i in indexes] menu = [] i = start for r in range(rows): row = [] - for col in range(cols): + for _col in range(cols): row.append(item % left_align(wordlist[i], maxlen)) i += 1 if i >= len(wordlist): break - menu.append(''.join(row)) + menu.append("".join(row)) if i >= len(wordlist): i = 0 break @@ -98,6 +100,7 @@ def build_menu(cons, wordlist, start, use_brackets, sort_in_column): break return menu, i + # this gets somewhat user interface-y, and as a result the logic gets # very convoluted. # @@ -163,7 +166,7 @@ def do(self): if completions_unchangable and len(completions[0]) == len(stem): r.msg = "[ sole completion ]" r.dirty = 1 - r.insert(completions[0][len(stem):]) + r.insert(completions[0][len(stem) :]) else: p = prefix(completions, len(stem)) if p: @@ -172,8 +175,12 @@ def do(self): if not r.cmpltn_menu_vis: r.cmpltn_menu_vis = 1 r.cmpltn_menu, r.cmpltn_menu_end = build_menu( - r.console, completions, r.cmpltn_menu_end, - r.use_brackets, r.sort_in_column) + r.console, + completions, + r.cmpltn_menu_end, + r.use_brackets, + r.sort_in_column, + ) r.dirty = 1 elif stem + p in completions: r.msg = "[ complete but not unique ]" @@ -192,12 +199,11 @@ def do(self): if len(stem) < 1: r.cmpltn_reset() else: - completions = [w for w in r.cmpltn_menu_choices - if w.startswith(stem)] + completions = [w for w in r.cmpltn_menu_choices if w.startswith(stem)] if completions: r.cmpltn_menu, r.cmpltn_menu_end = build_menu( - r.console, completions, 0, - r.use_brackets, r.sort_in_column) + r.console, completions, 0, r.use_brackets, r.sort_in_column + ) else: r.cmpltn_reset() @@ -209,40 +215,40 @@ class CompletingReader(Reader): * cmpltn_menu, cmpltn_menu_vis, cmpltn_menu_end, cmpltn_choices: * """ + # see the comment for the complete command assume_immutable_completions = True use_brackets = True # display completions inside [] sort_in_column = False def collect_keymap(self): - return super(CompletingReader, self).collect_keymap() + ( - (r'\t', 'complete'),) + return super().collect_keymap() + ((r"\t", "complete"),) def __init__(self, console): - super(CompletingReader, self).__init__(console) + super().__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): 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) + super().after_command(cmd) if not isinstance(cmd, (complete, self_insert)): self.cmpltn_reset() def calc_screen(self): - screen = super(CompletingReader, self).calc_screen() + screen = super().calc_screen() if self.cmpltn_menu_vis: ly = self.lxy[1] screen[ly:ly] = self.cmpltn_menu - self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu) + self.screeninfo[ly:ly] = [(0, [])] * len(self.cmpltn_menu) self.cxy = self.cxy[0], self.cxy[1] + len(self.cmpltn_menu) return screen def finish(self): - super(CompletingReader, self).finish() + super().finish() self.cmpltn_reset() def cmpltn_reset(self): @@ -258,7 +264,7 @@ def get_stem(self): p = self.pos - 1 while p >= 0 and st.get(b[p], SW) == SW: p -= 1 - return ''.join(b[p+1:self.pos]) + return "".join(b[p + 1 : self.pos]) def get_completions(self, stem): return [] @@ -267,9 +273,9 @@ def get_completions(self, stem): 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 self.history for s in l.split() if s and s.startswith(stem) + ] reader = TestReader() reader.ps1 = "c**> " @@ -280,5 +286,5 @@ def get_completions(self, stem): pass -if __name__ == '__main__': +if __name__ == "__main__": test() diff --git a/pyrepl/console.py b/pyrepl/console.py index cfbcbe9..41617f2 100644 --- a/pyrepl/console.py +++ b/pyrepl/console.py @@ -18,24 +18,26 @@ # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -class Event(object): +class Event: """An Event. `evt' is 'key' or somesuch.""" - __slots__ = 'evt', 'data', 'raw' - def __init__(self, evt, data, raw=''): + __slots__ = "evt", "data", "raw" + + def __init__(self, evt, data, raw=""): self.evt = evt self.data = data self.raw = raw def __repr__(self): - return 'Event(%r, %r)' % (self.evt, self.data) + return "Event({self.evt}, {self.data})" def __eq__(self, other): - return (self.evt == other.evt and - self.data == other.data and - self.raw == other.raw) + return ( + self.evt == other.evt and self.data == other.data and self.raw == other.raw + ) + -class Console(object): +class Console: """Attributes: screen, diff --git a/pyrepl/curses.py b/pyrepl/curses.py index 0331ce0..6ae8aaa 100644 --- a/pyrepl/curses.py +++ b/pyrepl/curses.py @@ -1,4 +1,3 @@ - # Copyright 2000-2010 Michael Hudson-Doyle # Armin Rigo # @@ -20,4 +19,4 @@ # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -from ._minimal_curses import setupterm, tigetstr, tparm, error +from ._minimal_curses import error, setupterm, tigetstr, tparm diff --git a/pyrepl/fancy_termios.py b/pyrepl/fancy_termios.py index 462f7c6..5b85cb0 100644 --- a/pyrepl/fancy_termios.py +++ b/pyrepl/fancy_termios.py @@ -19,34 +19,56 @@ import termios + class TermState: def __init__(self, tuples): - self.iflag, self.oflag, self.cflag, self.lflag, \ - self.ispeed, self.ospeed, self.cc = tuples + ( + self.iflag, + self.oflag, + self.cflag, + self.lflag, + self.ispeed, + self.ospeed, + self.cc, + ) = tuples + def as_list(self): - return [self.iflag, self.oflag, self.cflag, self.lflag, - self.ispeed, self.ospeed, self.cc] + return [ + self.iflag, + self.oflag, + self.cflag, + self.lflag, + self.ispeed, + self.ospeed, + self.cc, + ] def copy(self): return self.__class__(self.as_list()) + def tcgetattr(fd): return TermState(termios.tcgetattr(fd)) + def tcsetattr(fd, when, attrs): termios.tcsetattr(fd, when, attrs.as_list()) + class Term(TermState): TS__init__ = TermState.__init__ + def __init__(self, fd=0): self.TS__init__(termios.tcgetattr(fd)) self.fd = fd self.stack = [] + def save(self): - self.stack.append( self.as_list() ) + self.stack.append(self.as_list()) + def set(self, when=termios.TCSANOW): termios.tcsetattr(self.fd, when, self.as_list()) + def restore(self): self.TS__init__(self.stack.pop()) self.set() - diff --git a/pyrepl/historical_reader.py b/pyrepl/historical_reader.py index 01522ee..97bae94 100644 --- a/pyrepl/historical_reader.py +++ b/pyrepl/historical_reader.py @@ -17,28 +17,38 @@ # 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 commands, reader from pyrepl.reader import Reader as R isearch_keymap = tuple( - [('\\%03o'%c, 'isearch-end') for c in range(256) if chr(c) != '\\'] + \ - [(c, 'isearch-add-character') - for c in map(chr, range(32, 127)) if c != '\\'] + \ - [('\\%03o'%c, 'isearch-add-character') - for c in range(256) if chr(c).isalpha() and chr(c) != '\\'] + \ - [('\\\\', 'self-insert'), - (r'\C-r', 'isearch-backwards'), - (r'\C-s', 'isearch-forwards'), - (r'\C-c', 'isearch-cancel'), - (r'\C-g', 'isearch-cancel'), - (r'\', 'isearch-backspace')]) - -if 'c' in globals(): - del c - -ISEARCH_DIRECTION_NONE = '' -ISEARCH_DIRECTION_BACKWARDS = 'r' -ISEARCH_DIRECTION_FORWARDS = 'f' + [("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) != "\\"] + + [ + (c, "isearch-add-character") + for c in map(chr, list(range(32, 127))) + if c != "\\" + ] + + [ + ("\\%03o" % c, "isearch-add-character") + for c in range(256) + if chr(c).isalpha() and chr(c) != "\\" + ] + + [ + ("\\\\", "self-insert"), + (r"\C-r", "isearch-backwards"), + (r"\C-s", "isearch-forwards"), + (r"\C-c", "isearch-cancel"), + (r"\C-g", "isearch-cancel"), + (r"\", "isearch-backspace"), + ] +) + +if "c" in globals(): + del c # noqa: F821 + +ISEARCH_DIRECTION_NONE = "" +ISEARCH_DIRECTION_BACKWARDS = "r" +ISEARCH_DIRECTION_FORWARDS = "f" + class next_history(commands.Command): def do(self): @@ -48,6 +58,7 @@ def do(self): return r.select_item(r.historyi + 1) + class previous_history(commands.Command): def do(self): r = self.reader @@ -56,27 +67,31 @@ def do(self): return r.select_item(r.historyi - 1) + class restore_history(commands.Command): def do(self): r = self.reader - if r.historyi != len(r.history): - if r.get_unicode() != r.history[r.historyi]: - r.buffer = list(r.history[r.historyi]) - r.pos = len(r.buffer) - r.dirty = 1 + if r.historyi != len(r.history) and r.get_unicode() != r.history[r.historyi]: + r.buffer = list(r.history[r.historyi]) + r.pos = len(r.buffer) + r.dirty = 1 + class first_history(commands.Command): def do(self): self.reader.select_item(0) + class last_history(commands.Command): def do(self): self.reader.select_item(len(self.reader.history)) + class operate_and_get_next(commands.FinishCommand): def do(self): self.reader.next_history = self.reader.historyi + 1 + class yank_arg(commands.Command): def do(self): r = self.reader @@ -95,34 +110,33 @@ def do(self): return w = words[a] b = r.buffer - if r.yank_arg_i > 0: - o = len(r.yank_arg_yanked) - else: - o = 0 - b[r.pos - o:r.pos] = list(w) + o = len(r.yank_arg_yanked) if r.yank_arg_i > 0 else 0 + b[r.pos - o : r.pos] = list(w) r.yank_arg_yanked = w r.pos += len(w) - o r.dirty = 1 + class forward_history_isearch(commands.Command): def do(self): r = self.reader r.isearch_direction = ISEARCH_DIRECTION_FORWARDS r.isearch_start = r.historyi, r.pos - r.isearch_term = '' + r.isearch_term = "" r.dirty = 1 r.push_input_trans(r.isearch_trans) - + class reverse_history_isearch(commands.Command): def do(self): r = self.reader r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS r.dirty = 1 - r.isearch_term = '' + r.isearch_term = "" r.push_input_trans(r.isearch_trans) r.isearch_start = r.historyi, r.pos + class isearch_cancel(commands.Command): def do(self): r = self.reader @@ -132,6 +146,7 @@ def do(self): r.pos = r.isearch_start[1] r.dirty = 1 + class isearch_add_character(commands.Command): def do(self): r = self.reader @@ -139,9 +154,10 @@ def do(self): r.isearch_term += self.event[-1] r.dirty = 1 p = r.pos + len(r.isearch_term) - 1 - if b[p:p+1] != [r.isearch_term[-1]]: + if b[p : p + 1] != [r.isearch_term[-1]]: r.isearch_next() + class isearch_backspace(commands.Command): def do(self): r = self.reader @@ -151,18 +167,21 @@ def do(self): else: r.error("nothing to rubout") + class isearch_forwards(commands.Command): def do(self): r = self.reader r.isearch_direction = ISEARCH_DIRECTION_FORWARDS r.isearch_next() + class isearch_backwards(commands.Command): def do(self): r = self.reader r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS r.isearch_next() + class isearch_end(commands.Command): def do(self): r = self.reader @@ -171,6 +190,7 @@ def do(self): r.pop_input_trans() r.dirty = 1 + class HistoricalReader(R): """Adds history support (with incremental history searching) to the Reader class. @@ -188,38 +208,51 @@ class HistoricalReader(R): """ def collect_keymap(self): - return super(HistoricalReader, self).collect_keymap() + ( - (r'\C-n', 'next-history'), - (r'\C-p', 'previous-history'), - (r'\C-o', 'operate-and-get-next'), - (r'\C-r', 'reverse-history-isearch'), - (r'\C-s', 'forward-history-isearch'), - (r'\M-r', 'restore-history'), - (r'\M-.', 'yank-arg'), - (r'\', 'last-history'), - (r'\', 'first-history')) - + return super().collect_keymap() + ( + (r"\C-n", "next-history"), + (r"\C-p", "previous-history"), + (r"\C-o", "operate-and-get-next"), + (r"\C-r", "reverse-history-isearch"), + (r"\C-s", "forward-history-isearch"), + (r"\M-r", "restore-history"), + (r"\M-.", "yank-arg"), + (r"\", "last-history"), + (r"\", "first-history"), + ) def __init__(self, console): - super(HistoricalReader, self).__init__(console) + super().__init__(console) self.history = [] self.historyi = 0 self.transient_history = {} self.next_history = None self.isearch_direction = ISEARCH_DIRECTION_NONE - for c in [next_history, previous_history, restore_history, - first_history, last_history, yank_arg, - forward_history_isearch, reverse_history_isearch, - isearch_end, isearch_add_character, isearch_cancel, - isearch_add_character, isearch_backspace, - isearch_forwards, isearch_backwards, operate_and_get_next]: + for c in [ + next_history, + previous_history, + restore_history, + first_history, + last_history, + yank_arg, + forward_history_isearch, + reverse_history_isearch, + isearch_end, + isearch_add_character, + isearch_cancel, + isearch_add_character, + isearch_backspace, + isearch_forwards, + isearch_backwards, + operate_and_get_next, + ]: self.commands[c.__name__] = c - self.commands[c.__name__.replace('_', '-')] = 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) - + isearch_keymap, invalid_cls=isearch_end, character_cls=isearch_add_character + ) + def select_item(self, i): self.transient_history[self.historyi] = self.get_unicode() buf = self.transient_history.get(i) @@ -237,15 +270,14 @@ def get_item(self, i): return self.transient_history.get(i, self.get_unicode()) def prepare(self): - super(HistoricalReader, self).prepare() + super().prepare() try: self.transient_history = {} - if self.next_history is not None \ - and self.next_history < len(self.history): + if self.next_history is not None and self.next_history < len(self.history): self.historyi = self.next_history self.buffer[:] = list(self.history[self.next_history]) self.pos = len(self.buffer) - self.transient_history[len(self.history)] = '' + self.transient_history[len(self.history)] = "" else: self.historyi = len(self.history) self.next_history = None @@ -255,10 +287,10 @@ def prepare(self): def get_prompt(self, lineno, cursor_on_line): if cursor_on_line and self.isearch_direction != ISEARCH_DIRECTION_NONE: - d = 'rf'[self.isearch_direction == ISEARCH_DIRECTION_FORWARDS] - return "(%s-search `%s') "%(d, self.isearch_term) + d = "rf"[self.isearch_direction == ISEARCH_DIRECTION_FORWARDS] + return f"({d}-search `{self.isearch_term}') " else: - return super(HistoricalReader, self).get_prompt(lineno, cursor_on_line) + return super().get_prompt(lineno, cursor_on_line) def isearch_next(self): st = self.isearch_term @@ -267,16 +299,12 @@ def isearch_next(self): s = self.get_unicode() forwards = self.isearch_direction == ISEARCH_DIRECTION_FORWARDS while 1: - if forwards: - p = s.find(st, p + 1) - else: - p = s.rfind(st, 0, p + len(st) - 1) + p = s.find(st, p + 1) if forwards else s.rfind(st, 0, p + len(st) - 1) if p != -1: self.select_item(i) self.pos = p return - elif ((forwards and i == len(self.history) - 1) - or (not forwards and i == 0)): + elif (forwards and i == len(self.history) - 1) or (not forwards and i == 0): self.error("not found") return else: @@ -290,16 +318,18 @@ def isearch_next(self): p = len(s) def finish(self): - super(HistoricalReader, self).finish() + super().finish() ret = self.get_unicode() - for i, t in self.transient_history.items(): + for i, t in list(self.transient_history.items()): if i < len(self.history) and i != self.historyi: self.history[i] = t if ret: self.history.append(ret) + def test(): from pyrepl.unix_console import UnixConsole + reader = HistoricalReader(UnixConsole()) reader.ps1 = "h**> " reader.ps2 = "h/*> " @@ -308,5 +338,6 @@ def test(): while reader.readline(): pass -if __name__=='__main__': + +if __name__ == "__main__": test() diff --git a/pyrepl/input.py b/pyrepl/input.py index e92adbd..16de45b 100644 --- a/pyrepl/input.py +++ b/pyrepl/input.py @@ -32,28 +32,30 @@ # executive, temporary decision: [tab] and [C-i] are distinct, but # [meta-key] is identified with [esc key]. We demand that any console # class does quite a lot towards emulating a unix terminal. -from __future__ import print_function + +import pprint import unicodedata from collections import deque -import pprint + from .trace import trace -class InputTranslator(object): +class InputTranslator: def push(self, evt): pass + def get(self): pass + def empty(self): pass class KeymapTranslator(InputTranslator): - - def __init__(self, keymap, verbose=0, - invalid_cls=None, character_cls=None): + def __init__(self, keymap, verbose=0, invalid_cls=None, character_cls=None): self.verbose = verbose from pyrepl.keymap import compile_keymap, parse_keys + self.keymap = keymap self.invalid_cls = invalid_cls self.character_cls = character_cls @@ -62,7 +64,7 @@ def __init__(self, keymap, verbose=0, keyseq = tuple(parse_keys(keyspec)) d[keyseq] = command if verbose: - trace('[input] keymap: {}', pprint.pformat(d)) + trace("[input] keymap: {}", pprint.pformat(d)) self.k = self.ck = compile_keymap(d, ()) self.results = deque() self.stack = [] @@ -78,14 +80,12 @@ def push(self, evt): else: if d is None: trace("[input] invalid") - if self.stack or len(key) > 1 or unicodedata.category(key) == 'C': - self.results.append( - (self.invalid_cls, self.stack + [key])) + if self.stack or len(key) > 1 or unicodedata.category(key) == "C": + self.results.append((self.invalid_cls, self.stack + [key])) else: # small optimization: self.k[key] = self.character_cls - self.results.append( - (self.character_cls, [key])) + self.results.append((self.character_cls, [key])) else: trace("[input] matched {}", d) self.results.append((d, self.stack + [key])) diff --git a/pyrepl/keymap.py b/pyrepl/keymap.py index 2fcfce3..85f701e 100644 --- a/pyrepl/keymap.py +++ b/pyrepl/keymap.py @@ -54,116 +54,138 @@ """ _escapes = { - '\\':'\\', - "'":"'", - '"':'"', - 'a':'\a', - 'b':r'\h', - 'e':'\033', - 'f':'\f', - 'n':'\n', - 'r':'\r', - 't':'\t', - 'v':'\v' - } + "\\": "\\", + "'": "'", + '"': '"', + "a": "\a", + "b": r"\h", + "e": "\033", + "f": "\f", + "n": "\n", + "r": "\r", + "t": "\t", + "v": "\v", +} _keynames = { - 'backspace': 'backspace', - 'delete': 'delete', - 'down': 'down', - 'end': 'end', - 'enter': '\r', - 'escape': '\033', - 'f1' : 'f1', 'f2' : 'f2', 'f3' : 'f3', 'f4' : 'f4', - 'f5' : 'f5', 'f6' : 'f6', 'f7' : 'f7', 'f8' : 'f8', - 'f9' : 'f9', 'f10': 'f10', 'f11': 'f11', 'f12': 'f12', - 'f13': 'f13', 'f14': 'f14', 'f15': 'f15', 'f16': 'f16', - 'f17': 'f17', 'f18': 'f18', 'f19': 'f19', 'f20': 'f20', - 'home': 'home', - 'insert': 'insert', - 'left': 'left', - 'page down': 'page down', - 'page up': 'page up', - 'return': '\r', - 'right': 'right', - 'space': ' ', - 'tab': '\t', - 'up': 'up', - 'ctrl left': 'ctrl left', - 'ctrl right': 'ctrl right', - } + "backspace": "backspace", + "delete": "delete", + "down": "down", + "end": "end", + "enter": "\r", + "escape": "\033", + "f1": "f1", + "f2": "f2", + "f3": "f3", + "f4": "f4", + "f5": "f5", + "f6": "f6", + "f7": "f7", + "f8": "f8", + "f9": "f9", + "f10": "f10", + "f11": "f11", + "f12": "f12", + "f13": "f13", + "f14": "f14", + "f15": "f15", + "f16": "f16", + "f17": "f17", + "f18": "f18", + "f19": "f19", + "f20": "f20", + "home": "home", + "insert": "insert", + "left": "left", + "page down": "page down", + "page up": "page up", + "return": "\r", + "right": "right", + "space": " ", + "tab": "\t", + "up": "up", + "ctrl left": "ctrl left", + "ctrl right": "ctrl right", +} + class KeySpecError(Exception): pass + def _parse_key1(key, s): ctrl = 0 meta = 0 - ret = '' + ret = "" while not ret and s < len(key): - if key[s] == '\\': - c = key[s+1].lower() + if key[s] == "\\": + c = key[s + 1].lower() if c in _escapes: ret = _escapes[c] s += 2 elif c == "c": - if key[s + 2] != '-': + if key[s + 2] != "-": raise KeySpecError( - "\\C must be followed by `-' (char %d of %s)"%( - s + 2, repr(key))) + "\\C must be followed by `-' (char %d of %s)" + % (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] != '-': + if key[s + 2] != "-": raise KeySpecError( - "\\M must be followed by `-' (char %d of %s)"%( - s + 2, repr(key))) + "\\M must be followed by `-' (char %d of %s)" + % (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(): - n = key[s+1:s+4] + n = key[s + 1 : s + 4] ret = chr(int(n, 8)) s += 4 - elif c == 'x': - n = key[s+2:s+4] + elif c == "x": + n = key[s + 2 : s + 4] ret = chr(int(n, 16)) s += 4 - elif c == '<': - t = key.find('>', s) + elif c == "<": + t = key.find(">", s) if t == -1: raise KeySpecError( - "unterminated \\< starting at char %d of %s"%( - s + 1, repr(key))) - ret = key[s+2:t].lower() + "unterminated \\< starting at char %d of %s" + % (s + 1, repr(key)) + ) + ret = key[s + 2 : t].lower() if ret not in _keynames: raise KeySpecError( - "unrecognised keyname `%s' at char %d of %s"%( - ret, s + 2, repr(key))) + "unrecognised keyname `%s' at char %d of %s" + % (ret, s + 2, repr(key)) + ) ret = _keynames[ret] s = t + 1 else: raise KeySpecError( - "unknown backslash escape %s at char %d of %s"%( - repr(c), s + 2, repr(key))) + "unknown backslash escape %s at char %d of %s" + % (repr(c), s + 2, repr(key)) + ) else: ret = key[s] s += 1 if ctrl: if len(ret) > 1: raise KeySpecError("\\C- must be followed by a character") - ret = chr(ord(ret) & 0x1f) # curses.ascii.ctrl() - if meta: - ret = ['\033', ret] - else: - ret = [ret] + ret = chr(ord(ret) & 0x1F) # curses.ascii.ctrl() + ret = ["\x1b", ret] if meta else [ret] return ret, s + def parse_keys(key): s = 0 r = [] @@ -172,19 +194,18 @@ def parse_keys(key): r.extend(k) return r -def compile_keymap(keymap, empty=b''): + +def compile_keymap(keymap, empty=b""): r = {} - for key, value in keymap.items(): - if isinstance(key, bytes): - first = key[:1] - else: - first = key[0] + for key, value in list(keymap.items()): + first = key[:1] if isinstance(key, bytes) else key[0] r.setdefault(first, {})[key[1:]] = value - for key, value in r.items(): + for key, value in list(r.items()): if empty in value: if len(value) != 1: raise KeySpecError( - "key definitions for %s clash"%(value.values(),)) + f"key definitions for {list(value.values())} clash" + ) else: r[key] = value[empty] else: diff --git a/pyrepl/keymaps.py b/pyrepl/keymaps.py index 76ba896..98bdf00 100644 --- a/pyrepl/keymaps.py +++ b/pyrepl/keymaps.py @@ -18,123 +18,119 @@ # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. reader_emacs_keymap = tuple( - [(r'\C-a', 'beginning-of-line'), - (r'\C-b', 'left'), - (r'\C-c', 'interrupt'), - (r'\C-d', 'delete'), - (r'\C-e', 'end-of-line'), - (r'\C-f', 'right'), - (r'\C-g', 'cancel'), - (r'\C-h', 'backspace'), - (r'\C-j', 'self-insert'), - (r'\', 'accept'), - (r'\C-k', 'kill-line'), - (r'\C-l', 'clear-screen'), -# (r'\C-m', 'accept'), - (r'\C-q', 'quoted-insert'), - (r'\C-t', 'transpose-characters'), - (r'\C-u', 'unix-line-discard'), - (r'\C-v', 'quoted-insert'), - (r'\C-w', 'unix-word-rubout'), - (r'\C-x\C-u', 'upcase-region'), - (r'\C-y', 'yank'), - (r'\C-z', 'suspend'), - - (r'\M-b', 'backward-word'), - (r'\M-c', 'capitalize-word'), - (r'\M-d', 'kill-word'), - (r'\M-f', 'forward-word'), - (r'\M-l', 'downcase-word'), - (r'\M-t', 'transpose-words'), - (r'\M-u', 'upcase-word'), - (r'\M-y', 'yank-pop'), - (r'\M--', 'digit-arg'), - (r'\M-0', 'digit-arg'), - (r'\M-1', 'digit-arg'), - (r'\M-2', 'digit-arg'), - (r'\M-3', 'digit-arg'), - (r'\M-4', 'digit-arg'), - (r'\M-5', 'digit-arg'), - (r'\M-6', 'digit-arg'), - (r'\M-7', 'digit-arg'), - (r'\M-8', 'digit-arg'), - (r'\M-9', 'digit-arg'), - (r'\M-\n', 'self-insert'), - (r'\', 'self-insert')] + \ - [(c, 'self-insert') - 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'), - (r'\', 'down'), - (r'\', 'left'), - (r'\', 'right'), - (r'\', 'quoted-insert'), - (r'\', 'delete'), - (r'\', 'backspace'), - (r'\M-\', 'backward-kill-word'), - (r'\', 'end'), - (r'\', 'home'), - (r'\', 'help'), - (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"\C-a", "beginning-of-line"), + (r"\C-b", "left"), + (r"\C-c", "interrupt"), + (r"\C-d", "delete"), + (r"\C-e", "end-of-line"), + (r"\C-f", "right"), + (r"\C-g", "cancel"), + (r"\C-h", "backspace"), + (r"\C-j", "self-insert"), + (r"\", "accept"), + (r"\C-k", "kill-line"), + (r"\C-l", "clear-screen"), + # (r'\C-m', 'accept'), + (r"\C-q", "quoted-insert"), + (r"\C-t", "transpose-characters"), + (r"\C-u", "unix-line-discard"), + (r"\C-v", "quoted-insert"), + (r"\C-w", "unix-word-rubout"), + (r"\C-x\C-u", "upcase-region"), + (r"\C-y", "yank"), + (r"\C-z", "suspend"), + (r"\M-b", "backward-word"), + (r"\M-c", "capitalize-word"), + (r"\M-d", "kill-word"), + (r"\M-f", "forward-word"), + (r"\M-l", "downcase-word"), + (r"\M-t", "transpose-words"), + (r"\M-u", "upcase-word"), + (r"\M-y", "yank-pop"), + (r"\M--", "digit-arg"), + (r"\M-0", "digit-arg"), + (r"\M-1", "digit-arg"), + (r"\M-2", "digit-arg"), + (r"\M-3", "digit-arg"), + (r"\M-4", "digit-arg"), + (r"\M-5", "digit-arg"), + (r"\M-6", "digit-arg"), + (r"\M-7", "digit-arg"), + (r"\M-8", "digit-arg"), + (r"\M-9", "digit-arg"), + (r"\M-\n", "self-insert"), + (r"\", "self-insert"), + ] + + [(c, "self-insert") for c in map(chr, list(range(32, 127))) if c != "\\"] + + [(c, "self-insert") for c in map(chr, list(range(128, 256))) if c.isalpha()] + + [ + (r"\", "up"), + (r"\", "down"), + (r"\", "left"), + (r"\", "right"), + (r"\", "quoted-insert"), + (r"\", "delete"), + (r"\", "backspace"), + (r"\M-\", "backward-kill-word"), + (r"\", "end"), + (r"\", "home"), + (r"\", "help"), + (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 + ] +) hist_emacs_keymap = reader_emacs_keymap + ( - (r'\C-n', 'next-history'), - (r'\C-p', 'previous-history'), - (r'\C-o', 'operate-and-get-next'), - (r'\C-r', 'reverse-history-isearch'), - (r'\C-s', 'forward-history-isearch'), - (r'\M-r', 'restore-history'), - (r'\M-.', 'yank-arg'), - (r'\', 'last-history'), - (r'\', 'first-history')) + (r"\C-n", "next-history"), + (r"\C-p", "previous-history"), + (r"\C-o", "operate-and-get-next"), + (r"\C-r", "reverse-history-isearch"), + (r"\C-s", "forward-history-isearch"), + (r"\M-r", "restore-history"), + (r"\M-.", "yank-arg"), + (r"\", "last-history"), + (r"\", "first-history"), +) -comp_emacs_keymap = hist_emacs_keymap + ( - (r'\t', 'complete'),) +comp_emacs_keymap = hist_emacs_keymap + ((r"\t", "complete"),) python_emacs_keymap = comp_emacs_keymap + ( - (r'\n', 'maybe-accept'), - (r'\M-\n', 'self-insert')) - + (r"\n", "maybe-accept"), + (r"\M-\n", "self-insert"), +) + reader_vi_insert_keymap = tuple( - [(c, 'self-insert') - 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'), - (r'\', 'backspace'), - ('')]) + [(c, "self-insert") for c in map(chr, list(range(32, 127))) if c != "\\"] + + [(c, "self-insert") for c in map(chr, list(range(128, 256))) if c.isalpha()] + + [(r"\C-d", "delete"), (r"\", "backspace"), ("")] +) reader_vi_command_keymap = tuple( [ - ('E', 'enter-emacs-mode'), - ('R', 'enter-replace-mode'), - ('dw', 'delete-word'), - ('dd', 'delete-line'), - - ('h', 'left'), - ('i', 'enter-insert-mode'), - ('j', 'down'), - ('k', 'up'), - ('l', 'right'), - ('r', 'replace-char'), - ('w', 'forward-word'), - ('x', 'delete'), - ('.', 'repeat-edit'), # argh! - (r'\', 'enter-insert-mode'), - ] + - [(c, 'digit-arg') for c in '01234567689'] + - []) - + ("E", "enter-emacs-mode"), + ("R", "enter-replace-mode"), + ("dw", "delete-word"), + ("dd", "delete-line"), + ("h", "left"), + ("i", "enter-insert-mode"), + ("j", "down"), + ("k", "up"), + ("l", "right"), + ("r", "replace-char"), + ("w", "forward-word"), + ("x", "delete"), + (".", "repeat-edit"), # argh! + (r"\", "enter-insert-mode"), + ] + + [(c, "digit-arg") for c in "01234567689"] + + [] +) -reader_keymaps = { - 'emacs' : reader_emacs_keymap, - 'vi-insert' : reader_vi_insert_keymap, - 'vi-command' : reader_vi_command_keymap - } - -del c # from the listcomps +reader_keymaps = { + "emacs": reader_emacs_keymap, + "vi-insert": reader_vi_insert_keymap, + "vi-command": reader_vi_command_keymap, +} diff --git a/pyrepl/module_lister.py b/pyrepl/module_lister.py index f3d7b0f..5191a20 100644 --- a/pyrepl/module_lister.py +++ b/pyrepl/module_lister.py @@ -17,45 +17,50 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import os, sys +import os +import sys # for the completion support. # this is all quite nastily written. _packages = {} -def _make_module_list_dir(dir, suffs, prefix=''): + +def _make_module_list_dir(dir, suffs, prefix=""): l = [] for fname in os.listdir(dir): file = os.path.join(dir, fname) if os.path.isfile(file): for suff in suffs: if fname.endswith(suff): - l.append( prefix + fname[:-len(suff)] ) + l.append(prefix + fname[: -len(suff)]) break - elif os.path.isdir(file) \ - and os.path.exists(os.path.join(file, "__init__.py")): - l.append( prefix + fname ) + elif os.path.isdir(file) and os.path.exists(os.path.join(file, "__init__.py")): + l.append(prefix + fname) _packages[prefix + fname] = _make_module_list_dir( - file, suffs, prefix + fname + '.' ) + file, suffs, prefix + fname + "." + ) return sorted(set(l)) + def _make_module_list(): import imp - suffs = [x[0] for x in imp.get_suffixes() if x[0] != '.pyc'] + + suffs = [x[0] for x in imp.get_suffixes() if x[0] != ".pyc"] suffs.sort(reverse=True) - _packages[''] = list(sys.builtin_module_names) + _packages[""] = list(sys.builtin_module_names) for dir in sys.path: - if dir == '': - dir = '.' + if dir == "": + dir = "." if os.path.isdir(dir): - _packages[''] += _make_module_list_dir(dir, suffs) - _packages[''].sort() + _packages[""] += _make_module_list_dir(dir, suffs) + _packages[""].sort() + def find_modules(stem): - l = stem.split('.') - pack = '.'.join(l[:-1]) + l = stem.split(".") + pack = ".".join(l[:-1]) try: mods = _packages[pack] except KeyError: - raise ImportError("can't find \"%s\" package" % pack) + raise ImportError('can\'t find "%s" package' % pack) return [mod for mod in mods if mod.startswith(stem)] diff --git a/pyrepl/pygame_console.py b/pyrepl/pygame_console.py index cb90b8b..846c340 100644 --- a/pyrepl/pygame_console.py +++ b/pyrepl/pygame_console.py @@ -25,11 +25,13 @@ # during command execution and zap the executor process. Making this # work on non-Unix is expected to be even more entertaining. +# ruff: noqa: F405, F403 + +import pygame from pygame.locals import * -from pyrepl.console import Console, Event + from pyrepl import pygame_keymap -import pygame -import types +from pyrepl.console import Console, Event lmargin = 5 rmargin = 5 @@ -39,46 +41,59 @@ try: bool except NameError: + def bool(x): return not not x -modcolors = {K_LCTRL:1, - K_RCTRL:1, - K_LMETA:1, - K_RMETA:1, - K_LALT:1, - K_RALT:1, - K_LSHIFT:1, - K_RSHIFT:1} + +modcolors = { + K_LCTRL: 1, + K_RCTRL: 1, + K_LMETA: 1, + K_RMETA: 1, + K_LALT: 1, + K_RALT: 1, + K_LSHIFT: 1, + K_RSHIFT: 1, +} + class colors: - fg = 250,240,230 + fg = 250, 240, 230 bg = 5, 5, 5 cursor = 230, 0, 230 margin = 5, 5, 15 + class FakeStdout: def __init__(self, con): self.con = con + def write(self, text): self.con.write(text) + def flush(self): pass + class FakeStdin: def __init__(self, con): self.con = con + def read(self, n=None): # argh! raise NotImplementedError + def readline(self, n=None): - from reader import Reader + from .reader import Reader + try: # this isn't quite right: it will clobber any prompt that's # been printed. Not sure how to get around this... return Reader(self.con).readline() except EOFError: - return '' + return "" + class PyGameConsole(Console): """Attributes: @@ -89,13 +104,12 @@ class PyGameConsole(Console): height, width, """ - + def __init__(self): self.pygame_screen = pygame.display.set_mode((800, 600)) pygame.font.init() pygame.key.set_repeat(500, 30) - self.font = pygame.font.Font( - "/usr/X11R6/lib/X11/fonts/TTF/luximr.ttf", 15) + self.font = pygame.font.Font("/usr/X11R6/lib/X11/fonts/TTF/luximr.ttf", 15) self.fw, self.fh = self.fontsize = self.font.size("X") self.cursor = pygame.Surface(self.fontsize) self.cursor.fill(colors.cursor) @@ -105,7 +119,7 @@ def __init__(self): pygame.display.update() pygame.event.set_allowed(None) pygame.event.set_allowed(KEYDOWN) - + def install_keymap(self, keymap): """Install a given keymap. @@ -119,8 +133,10 @@ def char_rect(self, x, y): return self.char_pos(x, y), self.fontsize def char_pos(self, x, y): - return (lmargin + x*self.fw, - tmargin + y*self.fh + self.cur_top + self.scroll) + return ( + lmargin + x * self.fw, + tmargin + y * self.fh + self.cur_top + self.scroll, + ) def paint_margin(self): s = self.pygame_screen @@ -130,11 +146,12 @@ 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, xxx_todo_changeme): + (cx, cy) = xxx_todo_changeme self.screen = screen - self.pygame_screen.fill(colors.bg, - [0, tmargin + self.cur_top + self.scroll, - 800, 600]) + self.pygame_screen.fill( + colors.bg, [0, tmargin + self.cur_top + self.scroll, 800, 600] + ) self.paint_margin() line_top = self.cur_top @@ -142,7 +159,7 @@ def refresh(self, screen, (cx, cy)): self.cxy = (cx, cy) cp = self.char_pos(cx, cy) if cp[1] < tmargin: - self.scroll = - (cy*self.fh + self.cur_top) + self.scroll = -(cy * self.fh + self.cur_top) self.repaint() elif cp[1] + self.fh > 600 - bmargin: self.scroll += (600 - bmargin) - (cp[1] + self.fh) @@ -153,13 +170,14 @@ def refresh(self, screen, (cx, cy)): if 0 <= line_top + self.scroll <= (600 - bmargin - tmargin - self.fh): if line: ren = self.font.render(line, 1, colors.fg) - self.pygame_screen.blit(ren, (lmargin, - tmargin + line_top + self.scroll)) + self.pygame_screen.blit( + ren, (lmargin, tmargin + line_top + self.scroll) + ) line_top += self.fh pygame.display.update() def prepare(self): - self.cmd_buf = '' + self.cmd_buf = "" self.k = self.keymap self.height, self.width = self.getheightwidth() self.curs_vis = 1 @@ -178,7 +196,7 @@ def blit_a_char(self, linen, charn): def move_cursor(self, x, y): cp = self.char_pos(x, y) if cp[1] < tmargin or cp[1] + self.fh > 600 - bmargin: - self.event_queue.append(Event('refresh', '', '')) + self.event_queue.append(Event("refresh", "", "")) else: if self.curs_vis: cx, cy = self.cxy @@ -202,21 +220,23 @@ def set_cursor_vis(self, vis): def getheightwidth(self): """Return (height, width) where height and width are the height and width of the terminal window in characters.""" - return ((600 - tmargin - bmargin)/self.fh, - (800 - lmargin - rmargin)/self.fw) + return ( + (600 - tmargin - bmargin) / self.fh, + (800 - lmargin - rmargin) / self.fw, + ) def tr_event(self, pyg_event): - shift = bool(pyg_event.mod & KMOD_SHIFT) + bool(pyg_event.mod & KMOD_SHIFT) ctrl = bool(pyg_event.mod & KMOD_CTRL) - meta = bool(pyg_event.mod & (KMOD_ALT|KMOD_META)) + meta = bool(pyg_event.mod & (KMOD_ALT | KMOD_META)) try: - return self.k[(pyg_event.unicode, meta, ctrl)], pyg_event.unicode + return self.k[(pyg_event.str, meta, ctrl)], pyg_event.str except KeyError: try: - return self.k[(pyg_event.key, meta, ctrl)], pyg_event.unicode + return self.k[(pyg_event.key, meta, ctrl)], pyg_event.str except KeyError: - return "invalid-key", pyg_event.unicode + return "invalid-key", pyg_event.str def get_event(self, block=1): """Return an Event instance. Returns None if |block| is false @@ -236,13 +256,13 @@ def get_event(self, block=1): continue k, c = self.tr_event(pyg_event) - self.cmd_buf += c.encode('ascii', 'replace') + self.cmd_buf += c.encode("ascii", "replace") self.k = k - if not isinstance(k, types.DictType): + if not isinstance(k, dict): e = Event(k, self.cmd_buf, []) self.k = self.keymap - self.cmd_buf = '' + self.cmd_buf = "" return e def beep(self): @@ -253,7 +273,7 @@ def beep(self): def clear(self): """Wipe the screen""" self.pygame_screen.fill(colors.bg) - #self.screen = [] + # self.screen = [] self.pos = [0, 0] self.grobs = [] self.cur_top = 0 @@ -269,9 +289,10 @@ def finish(self): for line in self.screen: self.write_line(line, 1) if self.curs_vis: - self.pygame_screen.blit(self.cursor, - (lmargin + self.pos[1], - tmargin + self.pos[0] + self.scroll)) + self.pygame_screen.blit( + self.cursor, + (lmargin + self.pos[1], tmargin + self.pos[0] + self.scroll), + ) pygame.display.update() def flushoutput(self): @@ -282,9 +303,9 @@ 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): """Return the characters that have been typed but not yet processed.""" @@ -299,29 +320,28 @@ def getpending(self): def wait(self): """Wait for an event.""" - raise Exception, "erp!" + raise Exception("erp!") def repaint(self): # perhaps we should consolidate grobs? self.pygame_screen.fill(colors.bg) self.paint_margin() - for (y, x), surf, text in self.grobs: - if surf and 0 < y + self.scroll: - self.pygame_screen.blit(surf, (lmargin + x, - tmargin + y + self.scroll)) + for (y, x), surf, _text in self.grobs: + if surf and y + self.scroll > 0: + self.pygame_screen.blit(surf, (lmargin + x, tmargin + y + self.scroll)) pygame.display.update() def write_line(self, line, ret): - charsleft = (self.width*self.fw - self.pos[1])/self.fw + charsleft = (self.width * self.fw - self.pos[1]) / self.fw while len(line) > charsleft: self.write_line(line[:charsleft], 1) line = line[charsleft:] if line: ren = self.font.render(line, 1, colors.fg, colors.bg) self.grobs.append((self.pos[:], ren, line)) - self.pygame_screen.blit(ren, - (lmargin + self.pos[1], - tmargin + self.pos[0] + self.scroll)) + self.pygame_screen.blit( + ren, (lmargin + self.pos[1], tmargin + self.pos[0] + self.scroll) + ) else: self.grobs.append((self.pos[:], None, line)) if ret: @@ -331,22 +351,28 @@ def write_line(self, line, ret): self.repaint() self.pos[1] = 0 else: - self.pos[1] += self.fw*len(line) + self.pos[1] += self.fw * len(line) def write(self, text): lines = text.split("\n") if self.curs_vis: - self.pygame_screen.fill(colors.bg, - (lmargin + self.pos[1], - tmargin + self.pos[0] + self.scroll, - self.fw, self.fh)) + self.pygame_screen.fill( + colors.bg, + ( + lmargin + self.pos[1], + tmargin + self.pos[0] + self.scroll, + self.fw, + self.fh, + ), + ) for line in lines[:-1]: self.write_line(line, 1) self.write_line(lines[-1], 0) if self.curs_vis: - self.pygame_screen.blit(self.cursor, - (lmargin + self.pos[1], - tmargin + self.pos[0] + self.scroll)) + self.pygame_screen.blit( + self.cursor, + (lmargin + self.pos[1], tmargin + self.pos[0] + self.scroll), + ) pygame.display.update() def flush(self): diff --git a/pyrepl/pygame_keymap.py b/pyrepl/pygame_keymap.py index 5531f1c..8d1d049 100644 --- a/pyrepl/pygame_keymap.py +++ b/pyrepl/pygame_keymap.py @@ -36,114 +36,139 @@ # XXX it's actually possible to test this module, so it should have a # XXX test suite. +# ruff: noqa: F405, F403 + from pygame.locals import * _escapes = { - '\\': K_BACKSLASH, - "'" : K_QUOTE, - '"' : K_QUOTEDBL, -# 'a' : '\a', - 'b' : K_BACKSLASH, - 'e' : K_ESCAPE, -# 'f' : '\f', - 'n' : K_RETURN, - 'r' : K_RETURN, - 't' : K_TAB, -# 'v' : '\v' - } + "\\": K_BACKSLASH, + "'": K_QUOTE, + '"': K_QUOTEDBL, + # 'a' : '\a', + "b": K_BACKSLASH, + "e": K_ESCAPE, + # 'f' : '\f', + "n": K_RETURN, + "r": K_RETURN, + "t": K_TAB, + # 'v' : '\v' +} _keynames = { - 'backspace' : K_BACKSPACE, - 'delete' : K_DELETE, - 'down' : K_DOWN, - 'end' : K_END, - 'enter' : K_KP_ENTER, - 'escape' : K_ESCAPE, - 'f1' : K_F1, 'f2' : K_F2, 'f3' : K_F3, 'f4' : K_F4, - 'f5' : K_F5, 'f6' : K_F6, 'f7' : K_F7, 'f8' : K_F8, - 'f9' : K_F9, 'f10': K_F10,'f11': K_F11,'f12': K_F12, - 'f13': K_F13,'f14': K_F14,'f15': K_F15, - 'home' : K_HOME, - 'insert' : K_INSERT, - 'left' : K_LEFT, - 'pgdown' : K_PAGEDOWN, 'page down' : K_PAGEDOWN, - 'pgup' : K_PAGEUP, 'page up' : K_PAGEUP, - 'return' : K_RETURN, - 'right' : K_RIGHT, - 'space' : K_SPACE, - 'tab' : K_TAB, - 'up' : K_UP, - } + "backspace": K_BACKSPACE, + "delete": K_DELETE, + "down": K_DOWN, + "end": K_END, + "enter": K_KP_ENTER, + "escape": K_ESCAPE, + "f1": K_F1, + "f2": K_F2, + "f3": K_F3, + "f4": K_F4, + "f5": K_F5, + "f6": K_F6, + "f7": K_F7, + "f8": K_F8, + "f9": K_F9, + "f10": K_F10, + "f11": K_F11, + "f12": K_F12, + "f13": K_F13, + "f14": K_F14, + "f15": K_F15, + "home": K_HOME, + "insert": K_INSERT, + "left": K_LEFT, + "pgdown": K_PAGEDOWN, + "page down": K_PAGEDOWN, + "pgup": K_PAGEUP, + "page up": K_PAGEUP, + "return": K_RETURN, + "right": K_RIGHT, + "space": K_SPACE, + "tab": K_TAB, + "up": K_UP, +} + class KeySpecError(Exception): pass + def _parse_key1(key, s): ctrl = 0 meta = 0 - ret = '' + ret = "" while not ret and s < len(key): - if key[s] == '\\': - c = key[s+1].lower() - if _escapes.has_key(c): + if key[s] == "\\": + c = key[s + 1].lower() + if c in _escapes: ret = _escapes[c] s += 2 elif c == "c": - if key[s + 2] != '-': - raise KeySpecError, \ - "\\C must be followed by `-' (char %d of %s)"%( - s + 2, repr(key)) + if key[s + 2] != "-": + raise KeySpecError( + "\\C must be followed by `-' (char %d of %s)" + % (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, \ - "\\M must be followed by `-' (char %d of %s)"%( - s + 2, repr(key)) + if key[s + 2] != "-": + raise KeySpecError( + "\\M must be followed by `-' (char %d of %s)" + % (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(): - n = key[s+1:s+4] + n = key[s + 1 : s + 4] ret = chr(int(n, 8)) s += 4 - elif c == 'x': - n = key[s+2:s+4] + elif c == "x": + n = key[s + 2 : s + 4] ret = chr(int(n, 16)) s += 4 - elif c == '<': - t = key.find('>', s) + elif c == "<": + t = key.find(">", s) if t == -1: - raise KeySpecError, \ - "unterminated \\< starting at char %d of %s"%( - s + 1, repr(key)) + raise KeySpecError( + "unterminated \\< starting at char %d of %s" + % (s + 1, repr(key)) + ) try: - ret = _keynames[key[s+2:t].lower()] + ret = _keynames[key[s + 2 : t].lower()] s = t + 1 except KeyError: - raise KeySpecError, \ - "unrecognised keyname `%s' at char %d of %s"%( - key[s+2:t], s + 2, repr(key)) + raise KeySpecError( + "unrecognised keyname `%s' at char %d of %s" + % (key[s + 2 : t], s + 2, repr(key)) + ) if ret is None: return None, s else: - raise KeySpecError, \ - "unknown backslash escape %s at char %d of %s"%( - `c`, s + 2, repr(key)) + raise KeySpecError( + "unknown backslash escape %s at char %d of %s" + % (repr(c), s + 2, repr(key)) + ) else: if ctrl: - ret = chr(ord(key[s]) & 0x1f) # curses.ascii.ctrl() - ret = unicode(ret) + ret = chr(ord(key[s]) & 0x1F) # curses.ascii.ctrl() + ret = str(ret) else: - ret = unicode(key[s]) + ret = str(key[s]) s += 1 return (ret, meta, ctrl), s + def parse_keys(key): s = 0 r = [] @@ -154,37 +179,40 @@ def parse_keys(key): r.append(k) return tuple(r) + def _compile_keymap(keymap): r = {} - for key, value in keymap.items(): + for key, value in list(keymap.items()): 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(),) + for key, value in list(r.items()): + if () in value: + if len(value) != 1: + raise KeySpecError( + f"key definitions for {list(value.values())} clash" + ) else: r[key] = value[()] else: r[key] = _compile_keymap(value) return r + def compile_keymap(keymap): r = {} for key, value in keymap: k = parse_keys(key) - if value is None and r.has_key(k): + if value is None and k in r: del r[k] if k is not None: r[k] = value return _compile_keymap(r) + def keyname(key): - longest_match = '' - longest_match_name = '' - for name, keyseq in keyset.items(): - if keyseq and key.startswith(keyseq) and \ - len(keyseq) > len(longest_match): + longest_match = "" + longest_match_name = "" + for name, keyseq in list(keyset.items()): + if keyseq and key.startswith(keyseq) and len(keyseq) > len(longest_match): longest_match = keyseq longest_match_name = name if len(longest_match) > 0: @@ -192,59 +220,63 @@ def keyname(key): else: return None, 0 -_unescapes = {'\r':'\\r', '\n':'\\n', '\177':'^?'} -#for k,v in _escapes.items(): +_unescapes = {"\r": "\\r", "\n": "\\n", "\177": "^?"} + +# for k,v in _escapes.items(): # _unescapes[v] = k + def unparse_key(keyseq): if not keyseq: - return '' + return "" name, s = keyname(keyseq) if name: - if name <> 'escape' or s == len(keyseq): - return '\\<' + name + '>' + unparse_key(keyseq[s:]) + if name != "escape" or s == len(keyseq): + return "\\<" + name + ">" + unparse_key(keyseq[s:]) else: - return '\\M-' + unparse_key(keyseq[1:]) + return "\\M-" + unparse_key(keyseq[1:]) else: c = keyseq[0] r = keyseq[1:] - if c == '\\': - p = '\\\\' - elif _unescapes.has_key(c): + if c == "\\": + p = "\\\\" + elif c in _unescapes: p = _unescapes[c] - elif ord(c) < ord(' '): - p = '\\C-%s'%(chr(ord(c)+96),) - elif ord(' ') <= ord(c) <= ord('~'): + elif ord(c) < ord(" "): + p = f"\\C-{chr(ord(c) + 96)}" + elif ord(" ") <= ord(c) <= ord("~"): p = c else: - p = '\\%03o'%(ord(c),) + p = f"\\{ord(c):03o}" return p + unparse_key(r) + def _unparse_keyf(keyseq): if not 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:]) - return ['M-'+rest[0]] + rest[1:] + return ["M-" + rest[0]] + rest[1:] else: c = keyseq[0] r = keyseq[1:] - if c == '\\': - p = '\\' - elif _unescapes.has_key(c): + if c == "\\": + p = "\\" + elif c in _unescapes: p = _unescapes[c] - elif ord(c) < ord(' '): - p = 'C-%s'%(chr(ord(c)+96),) - elif ord(' ') <= ord(c) <= ord('~'): + elif ord(c) < ord(" "): + p = f"C-{chr(ord(c) + 96)}" + elif ord(" ") <= ord(c) <= ord("~"): p = c else: - p = '\\%03o'%(ord(c),) + p = f"\\{ord(c):03o}" return [p] + _unparse_keyf(r) + def unparse_keyf(keyseq): return " ".join(_unparse_keyf(keyseq)) diff --git a/pyrepl/python_reader.py b/pyrepl/python_reader.py index 5e2d4e7..8072313 100644 --- a/pyrepl/python_reader.py +++ b/pyrepl/python_reader.py @@ -20,20 +20,20 @@ # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # one impressive collections of imports: -from __future__ import print_function -from __future__ import unicode_literals + + +import atexit +import code +import imp +import os +import re +import sys +import traceback +import warnings + +from pyrepl import commands, completer, completing_reader, module_lister, reader from pyrepl.completing_reader import CompletingReader from pyrepl.historical_reader import HistoricalReader -from pyrepl import completing_reader, reader -from pyrepl import commands, completer -from pyrepl import module_lister -import imp, sys, os, re, code, traceback -import atexit, warnings - -try: - unicode -except: - unicode = str try: imp.find_module("twisted") @@ -44,23 +44,16 @@ CommandCompiler = code.CommandCompiler + def eat_it(*args): """this function eats warnings, if you were wondering""" pass -if sys.version_info >= (3,0): - def _reraise(cls, val, tb): - __tracebackhide__ = True - assert hasattr(val, '__traceback__') - raise val -else: - exec (""" + def _reraise(cls, val, tb): __tracebackhide__ = True - raise cls, val, tb -""") - - + assert hasattr(val, "__traceback__") + raise val class maybe_accept(commands.Command): @@ -78,29 +71,33 @@ def do(self): else: self.finish = 1 + from_line_prog = re.compile( - "^from\s+(?P[A-Za-z_.0-9]*)\s+import\s+(?P[A-Za-z_.0-9]*)") -import_line_prog = re.compile( - "^(?:import|from)\s+(?P[A-Za-z_.0-9]*)\s*$") + "^from\s+(?P[A-Za-z_.0-9]*)\s+import\s+(?P[A-Za-z_.0-9]*)" +) +import_line_prog = re.compile("^(?:import|from)\s+(?P[A-Za-z_.0-9]*)\s*$") + def saver(reader=reader): try: with open(os.path.expanduser("~/.pythoni.hist"), "wb") as fp: - fp.write(b'\n'.join(item.encode('unicode_escape') - for item in reader.history)) - except IOError as e: + fp.write( + b"\n".join(item.encode("unicode_escape") for item in reader.history) + ) + except OSError as e: print(e) pass + class PythonicReader(CompletingReader, HistoricalReader): def collect_keymap(self): - return super(PythonicReader, self).collect_keymap() + ( - (r'\n', 'maybe-accept'), - (r'\M-\n', 'insert-nl')) - - def __init__(self, console, locals, - compiler=None): - super(PythonicReader, self).__init__(console) + return super().collect_keymap() + ( + (r"\n", "maybe-accept"), + (r"\M-\n", "insert-nl"), + ) + + def __init__(self, console, locals, compiler=None): + super().__init__(console) self.completer = completer.Completer(locals) st = self.syntax_table for c in "._0123456789": @@ -110,23 +107,22 @@ def __init__(self, console, locals, self.compiler = CommandCompiler() else: self.compiler = compiler + try: - file = open(os.path.expanduser("~/.pythoni.hist"), 'rb') - except IOError: + with open(os.path.expanduser("~/.pythoni.hist"), "rb") as fh: + lines = fh.readlines() + self.history = [ + line.rstrip(b"\n").decode("unicode_escape") for line in lines + ] + except FileNotFoundError: self.history = [] - else: - try: - lines = file.readlines() - self.history = [ x.rstrip(b'\n').decode('unicode_escape') for x in lines] - except: - self.history = [] - self.historyi = len(self.history) - file.close() + self.historyi = len(self.history) + atexit.register(lambda: saver(self)) for c in [maybe_accept]: self.commands[c.__name__] = c - self.commands[c.__name__.replace('_', '-')] = c - + self.commands[c.__name__.replace("_", "-")] = c + def get_completions(self, stem): b = self.get_unicode() m = import_line_prog.match(b) @@ -147,21 +143,22 @@ def get_completions(self, stem): l = module_lister._packages[mod] except KeyError: try: - mod = __import__(mod, self.locals, self.locals, ['']) + mod = __import__(mod, self.locals, self.locals, [""]) return [x for x in dir(mod) if x.startswith(name)] except ImportError: pass else: - return [x[len(mod) + 1:] - for x in l if x.startswith(mod + '.' + name)] + return [x[len(mod) + 1 :] for x in l if x.startswith(mod + "." + name)] try: l = sorted(set(self.completer.complete(stem))) return l except (NameError, AttributeError): return [] + class ReaderConsole(code.InteractiveInterpreter): II_init = code.InteractiveInterpreter.__init__ + def __init__(self, console, locals=None): if locals is None: locals = {} @@ -169,7 +166,7 @@ def __init__(self, console, locals=None): self.compiler = CommandCompiler() self.compile = self.compiler.compiler self.reader = PythonicReader(console, locals, self.compiler) - locals['Reader'] = self.reader + locals["Reader"] = self.reader def run_user_init_file(self): for key in "PYREPLSTARTUP", "PYTHONSTARTUP": @@ -179,7 +176,7 @@ def run_user_init_file(self): else: return try: - with open(initfile, "r") as f: + with open(initfile) as f: exec(compile(f.read(), initfile, "exec"), self.locals, self.locals) except: etype, value, tb = sys.exc_info() @@ -188,7 +185,7 @@ def run_user_init_file(self): def execute(self, text): try: # ooh, look at the hack: - code = self.compile(text, '', 'single') + code = self.compile(text, "", "single") except (OverflowError, SyntaxError, ValueError): self.showsyntaxerror("") else: @@ -198,13 +195,13 @@ def execute(self, text): def interact(self): while 1: - try: # catches EOFError's and KeyboardInterrupts during execution - try: # catches KeyboardInterrupts during editing - try: # warning saver + try: # catches EOFError's and KeyboardInterrupts during execution + try: # catches KeyboardInterrupts during editing + try: # warning saver # can't have warnings spewed onto terminal sv = warnings.showwarning warnings.showwarning = eat_it - l = unicode(self.reader.readline(), 'utf-8') + l = str(self.reader.readline(), "utf-8") finally: warnings.showwarning = sv except KeyboardInterrupt: @@ -221,7 +218,7 @@ def prepare(self): self.sv_sw = warnings.showwarning warnings.showwarning = eat_it self.reader.prepare() - self.reader.refresh() # we want :after methods... + self.reader.refresh() # we want :after methods... def restore(self): self.reader.restore() @@ -254,10 +251,11 @@ def tkfilehandler(self, file, mask): # createfilehandler)? threads, I guess def really_tkinteract(self): import _tkinter + _tkinter.createfilehandler( - self.reader.console.input_fd, _tkinter.READABLE, - self.tkfilehandler) - + self.reader.console.input_fd, _tkinter.READABLE, self.tkfilehandler + ) + self.exc_info = None while 1: # dooneevent will return 0 without blocking if there are @@ -270,13 +268,13 @@ def really_tkinteract(self): type, value, tb = self.exc_info self.exc_info = None _reraise(type, value, tb) - + def tkinteract(self): """Run a Tk-aware Python interactive session. This function simulates the Python top-level in a way that allows Tk's mainloop to run.""" - + # attempting to understand the control flow of this function # without help may cause internal injuries. so, some # explanation. @@ -295,14 +293,14 @@ def tkinteract(self): # KeyboardInterrupts cause a restart. All other exceptions # are likely bugs in pyrepl (well, 'cept for SystemExit, of # course). - + while 1: try: try: self.prepare() try: while 1: - if sys.modules.has_key("_tkinter"): + if "_tkinter" in sys.modules: self.really_tkinteract() # really_tkinteract is not expected to # return except via an exception, but: @@ -318,13 +316,16 @@ def tkinteract(self): break def twistedinteract(self): + import signal + from twisted.internet import reactor from twisted.internet.abstract import FileDescriptor - import signal + outerself = self + class Me(FileDescriptor): def fileno(self): - """ We want to select on FD 0 """ + """We want to select on FD 0""" return 0 def doRead(self): @@ -335,67 +336,79 @@ def doRead(self): reactor.stop() reactor.addReader(Me()) - reactor.callWhenRunning(signal.signal, - signal.SIGINT, - signal.default_int_handler) + reactor.callWhenRunning( + signal.signal, signal.SIGINT, signal.default_int_handler + ) self.prepare() try: reactor.run() finally: self.restore() - def cocoainteract(self, inputfilehandle=None, outputfilehandle=None): # only call this when there's a run loop already going! # note that unlike the other *interact methods, this returns immediately from cocoasupport import CocoaInteracter - self.cocoainteracter = CocoaInteracter.alloc().init(self, inputfilehandle, outputfilehandle) - - -def main(use_pygame_console=0, interactmethod=default_interactmethod, print_banner=True, clear_main=True): + + self.cocoainteracter = CocoaInteracter.alloc().init( + self, inputfilehandle, outputfilehandle + ) + + +def main( + use_pygame_console=0, + interactmethod=default_interactmethod, + print_banner=True, + clear_main=True, +): si, se, so = sys.stdin, sys.stderr, sys.stdout try: - if 0 and use_pygame_console: # pygame currently borked - from pyrepl.pygame_console import PyGameConsole, FakeStdin, FakeStdout + if False: # pygame currently borked + from pyrepl.pygame_console import FakeStdin, FakeStdout, PyGameConsole + con = PyGameConsole() sys.stderr = sys.stdout = FakeStdout(con) sys.stdin = FakeStdin(con) else: from pyrepl.unix_console import UnixConsole + try: import locale except ImportError: encoding = None else: - if hasattr(locale, 'nl_langinfo') \ - and hasattr(locale, 'CODESET'): + if hasattr(locale, "nl_langinfo") and hasattr(locale, "CODESET"): encoding = locale.nl_langinfo(locale.CODESET) - elif os.environ.get('TERM_PROGRAM') == 'Apple_Terminal': + elif os.environ.get("TERM_PROGRAM") == "Apple_Terminal": # /me whistles innocently... - code = int(os.popen( - "defaults read com.apple.Terminal StringEncoding" - ).read()) + code = int( + os.popen( + "defaults read com.apple.Terminal StringEncoding" + ).read() + ) if code == 4: - encoding = 'utf-8' + encoding = "utf-8" # More could go here -- and what's here isn't # bulletproof. What would be? AppleScript? # Doesn't seem to be possible. else: encoding = None else: - encoding = None # so you get ASCII... + encoding = None # so you get ASCII... con = UnixConsole(os.dup(0), os.dup(1), None, encoding) if print_banner: print("Python", sys.version, "on", sys.platform) - print('Type "help", "copyright", "credits" or "license" '\ - 'for more information.') + print( + 'Type "help", "copyright", "credits" or "license" ' + "for more information." + ) sys.path.insert(0, os.getcwd()) - if clear_main and __name__ != '__main__': - mainmod = imp.new_module('__main__') - sys.modules['__main__'] = mainmod + if clear_main and __name__ != "__main__": + mainmod = imp.new_module("__main__") + sys.modules["__main__"] = mainmod else: - mainmod = sys.modules['__main__'] + mainmod = sys.modules["__main__"] rc = ReaderConsole(con, mainmod.__dict__) rc.reader._module_list_ready = False @@ -404,5 +417,6 @@ def main(use_pygame_console=0, interactmethod=default_interactmethod, print_bann finally: sys.stdin, sys.stderr, sys.stdout = si, se, so -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/pyrepl/reader.py b/pyrepl/reader.py index e1db998..3812278 100644 --- a/pyrepl/reader.py +++ b/pyrepl/reader.py @@ -19,32 +19,33 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -from __future__ import unicode_literals + import unicodedata -from pyrepl import commands -from pyrepl import input + +from pyrepl import commands, input + try: - unicode + str except NameError: - unicode = str - unichr = chr - basestring = bytes, str + str = str + chr = chr + str = bytes, str def _make_unctrl_map(): uc_map = {} - for c in map(unichr, range(256)): - if unicodedata.category(c)[0] != 'C': + for c in map(chr, list(range(256))): + if unicodedata.category(c)[0] != "C": uc_map[c] = 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('^?') + c = chr(i) + uc_map[c] = "^" + chr(ord("A") + i - 1) + uc_map[b"\t"] = " " # display TABs as 4 characters + uc_map[b"\177"] = str("^?") for i in range(256): - c = unichr(i) + c = chr(i) if c not in uc_map: - uc_map[c] = unicode('\\%03o') % i + uc_map[c] = str("\\%03o") % i return uc_map @@ -52,14 +53,14 @@ def _my_unctrl(c, u=_make_unctrl_map()): if c in u: return u[c] else: - if unicodedata.category(c).startswith('C'): - return br'\u%04x' % ord(c) + if unicodedata.category(c).startswith("C"): + return rb"\u%04x" % ord(c) else: return c -def disp_str(buffer, join=''.join, uc=_my_unctrl): - """ disp_str(buffer:string) -> (string, [int]) +def disp_str(buffer, join="".join, uc=_my_unctrl): + """disp_str(buffer:string) -> (string, [int]) Return the string that should be the printed represenation of |buffer| and a list detailing where the characters of |buffer| @@ -79,98 +80,95 @@ def disp_str(buffer, join=''.join, uc=_my_unctrl): b.extend([0] * (len(x) - 1)) return join(s), b + del _my_unctrl del _make_unctrl_map # syntax classes: -[SYNTAX_WHITESPACE, - SYNTAX_WORD, - SYNTAX_SYMBOL] = range(3) +[SYNTAX_WHITESPACE, SYNTAX_WORD, SYNTAX_SYMBOL] = list(range(3)) def make_default_syntax_table(): # XXX perhaps should use some unicodedata here? st = {} - for c in map(unichr, range(256)): + for c in map(chr, list(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(chr, list(range(256))) if a.isalpha()]: st[c] = SYNTAX_WORD - st[unicode('\n')] = st[unicode(' ')] = SYNTAX_WHITESPACE + st[str("\n")] = st[str(" ")] = SYNTAX_WHITESPACE return st + default_keymap = tuple( - [(r'\C-a', 'beginning-of-line'), - (r'\C-b', 'left'), - (r'\C-c', 'interrupt'), - (r'\C-d', 'delete'), - (r'\C-e', 'end-of-line'), - (r'\C-f', 'right'), - (r'\C-g', 'cancel'), - (r'\C-h', 'backspace'), - (r'\C-j', 'accept'), - (r'\', 'accept'), - (r'\C-k', 'kill-line'), - (r'\C-l', 'clear-screen'), - (r'\C-m', 'accept'), - (r'\C-q', 'quoted-insert'), - (r'\C-t', 'transpose-characters'), - (r'\C-u', 'unix-line-discard'), - (r'\C-v', 'quoted-insert'), - (r'\C-w', 'unix-word-rubout'), - (r'\C-x\C-u', 'upcase-region'), - (r'\C-y', 'yank'), - (r'\C-z', 'suspend'), - - (r'\M-b', 'backward-word'), - (r'\M-c', 'capitalize-word'), - (r'\M-d', 'kill-word'), - (r'\M-f', 'forward-word'), - (r'\M-l', 'downcase-word'), - (r'\M-t', 'transpose-words'), - (r'\M-u', 'upcase-word'), - (r'\M-y', 'yank-pop'), - (r'\M--', 'digit-arg'), - (r'\M-0', 'digit-arg'), - (r'\M-1', 'digit-arg'), - (r'\M-2', 'digit-arg'), - (r'\M-3', 'digit-arg'), - (r'\M-4', 'digit-arg'), - (r'\M-5', 'digit-arg'), - (r'\M-6', 'digit-arg'), - (r'\M-7', 'digit-arg'), - (r'\M-8', 'digit-arg'), - (r'\M-9', 'digit-arg'), - #(r'\M-\n', 'insert-nl'), - ('\\\\', 'self-insert')] + - [(c, 'self-insert') - 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'), - (r'\', 'down'), - (r'\', 'left'), - (r'\', 'right'), - (r'\', 'quoted-insert'), - (r'\', 'delete'), - (r'\', 'backspace'), - (r'\M-\', 'backward-kill-word'), - (r'\', 'end-of-line'), # was 'end' - (r'\', 'beginning-of-line'), # was 'home' - (r'\', 'help'), - (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 - del c # from the listcomps - - -class Reader(object): + [ + (r"\C-a", "beginning-of-line"), + (r"\C-b", "left"), + (r"\C-c", "interrupt"), + (r"\C-d", "delete"), + (r"\C-e", "end-of-line"), + (r"\C-f", "right"), + (r"\C-g", "cancel"), + (r"\C-h", "backspace"), + (r"\C-j", "accept"), + (r"\", "accept"), + (r"\C-k", "kill-line"), + (r"\C-l", "clear-screen"), + (r"\C-m", "accept"), + (r"\C-q", "quoted-insert"), + (r"\C-t", "transpose-characters"), + (r"\C-u", "unix-line-discard"), + (r"\C-v", "quoted-insert"), + (r"\C-w", "unix-word-rubout"), + (r"\C-x\C-u", "upcase-region"), + (r"\C-y", "yank"), + (r"\C-z", "suspend"), + (r"\M-b", "backward-word"), + (r"\M-c", "capitalize-word"), + (r"\M-d", "kill-word"), + (r"\M-f", "forward-word"), + (r"\M-l", "downcase-word"), + (r"\M-t", "transpose-words"), + (r"\M-u", "upcase-word"), + (r"\M-y", "yank-pop"), + (r"\M--", "digit-arg"), + (r"\M-0", "digit-arg"), + (r"\M-1", "digit-arg"), + (r"\M-2", "digit-arg"), + (r"\M-3", "digit-arg"), + (r"\M-4", "digit-arg"), + (r"\M-5", "digit-arg"), + (r"\M-6", "digit-arg"), + (r"\M-7", "digit-arg"), + (r"\M-8", "digit-arg"), + (r"\M-9", "digit-arg"), + # (r'\M-\n', 'insert-nl'), + ("\\\\", "self-insert"), + ] + + [(c, "self-insert") for c in map(chr, list(range(32, 127))) if c != "\\"] + + [(c, "self-insert") for c in map(chr, list(range(128, 256))) if c.isalpha()] + + [ + (r"\", "up"), + (r"\", "down"), + (r"\", "left"), + (r"\", "right"), + (r"\", "quoted-insert"), + (r"\", "delete"), + (r"\", "backspace"), + (r"\M-\", "backward-kill-word"), + (r"\", "end-of-line"), # was 'end' + (r"\", "beginning-of-line"), # was 'home' + (r"\", "help"), + (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"), + ] +) + +class Reader: """The Reader class implements the bare bones of a command reader, handling such details as editing and cursor motion. What it does not support are such things as completion or history support - @@ -244,20 +242,21 @@ def __init__(self, console): self.finished = 0 self.console = 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()): + self.msg = "" + for v in list(vars(commands).values()): + 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.commands[v.__name__.replace("_", "-")] = v self.syntax_table = make_default_syntax_table() self.input_trans_stack = [] self.keymap = self.collect_keymap() self.input_trans = input.KeymapTranslator( - self.keymap, - invalid_cls='invalid-key', - character_cls='self-insert') + self.keymap, invalid_cls="invalid-key", character_cls="self-insert" + ) def collect_keymap(self): return default_keymap @@ -273,7 +272,7 @@ def calc_screen(self): screeninfo = [] w = self.console.width - 1 p = self.pos - for ln, line in zip(range(len(lines)), lines): + for ln, line in zip(list(range(len(lines))), lines): ll = len(line) if 0 <= p <= ll: if self.msg and not self.msg_at_bottom: @@ -282,8 +281,8 @@ def calc_screen(self): screeninfo.append((0, [])) self.lxy = p, ln prompt = self.get_prompt(ln, ll >= p >= 0) - while '\n' in prompt: - pre_prompt, _, prompt = prompt.partition('\n') + while "\n" in prompt: + pre_prompt, _, prompt = prompt.partition("\n") screen.append(pre_prompt) screeninfo.append((0, [])) p -= ll + 1 @@ -294,13 +293,13 @@ def calc_screen(self): screen.append(prompt + l) screeninfo.append((lp, l2 + [1])) else: - screen.append(prompt + l[:w - lp] + "\\") - screeninfo.append((lp, l2[:w - lp])) + screen.append(prompt + l[: w - lp] + "\\") + screeninfo.append((lp, l2[: w - lp])) for i in range(-lp + w, -lp + wrapcount * w, w): - screen.append(l[i:i + w] + "\\") - screeninfo.append((0, l2[i:i + w])) - screen.append(l[wrapcount * w - lp:]) - screeninfo.append((0, l2[wrapcount * w - lp:] + [1])) + screen.append(l[i : i + w] + "\\") + screeninfo.append((0, l2[i : i + w])) + screen.append(l[wrapcount * w - lp :]) + screeninfo.append((0, l2[wrapcount * w - lp :] + [1])) self.screeninfo = screeninfo self.cxy = self.pos2xy(self.pos) if self.msg and self.msg_at_bottom: @@ -310,26 +309,26 @@ def calc_screen(self): return screen def process_prompt(self, prompt): - """ Process the prompt. + """Process the prompt. This means calculate the length of the prompt. The character \x01 and \x02 are used to bracket ANSI control sequences and need to be excluded from the length calculation. So also a copy of the prompt - is returned with these control characters removed. """ + is returned with these control characters removed.""" - out_prompt = '' + out_prompt = "" l = len(prompt) pos = 0 while True: - s = prompt.find('\x01', pos) + s = prompt.find("\x01", pos) if s == -1: break - e = prompt.find('\x02', s) + e = prompt.find("\x02", s) 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] + out_prompt += prompt[pos:s] + prompt[s + 1 : e] pos = e + 1 out_prompt += prompt[pos:] return out_prompt, l @@ -377,7 +376,7 @@ def bol(self, p=None): p = self.pos b = self.buffer p -= 1 - while p >= 0 and b[p] != '\n': + while p >= 0 and b[p] != "\n": p -= 1 return p + 1 @@ -389,7 +388,7 @@ def eol(self, p=None): if p is None: p = self.pos b = self.buffer - while p < len(b) and b[p] != '\n': + while p < len(b) and b[p] != "\n": p += 1 return p @@ -458,7 +457,7 @@ def pos2xy(self, pos): def insert(self, text): """Insert 'text' at the insertion point.""" - self.buffer[self.pos:self.pos] = list(text) + self.buffer[self.pos : self.pos] = list(text) self.pos += len(text) self.dirty = 1 @@ -522,11 +521,10 @@ def refresh(self): self.dirty = 0 # forgot this for a while (blush) def do_cmd(self, cmd): - #print cmd - if isinstance(cmd[0], basestring): - #XXX: unify to text - cmd = self.commands.get(cmd[0], - commands.invalid_command)(self, *cmd) + # print cmd + if isinstance(cmd[0], str): + # XXX: unify to text + cmd = self.commands.get(cmd[0], commands.invalid_command)(self, *cmd) elif isinstance(cmd[0], type): cmd = cmd[0](self, *cmd) else: @@ -555,7 +553,7 @@ def handle1(self, block=1): pending.""" if self.msg: - self.msg = '' + self.msg = "" self.dirty = 1 while 1: @@ -565,19 +563,16 @@ def handle1(self, block=1): translate = True - if event.evt == 'key': + if event.evt == "key": self.input_trans.push(event) - elif event.evt == 'scroll': + elif event.evt == "scroll": self.refresh() - elif event.evt == 'resize': + elif event.evt == "resize": self.refresh() else: translate = False - if translate: - cmd = self.input_trans.get() - else: - cmd = event.evt, event.data + cmd = self.input_trans.get() if translate else (event.evt, event.data) if cmd is None: if block: @@ -612,22 +607,22 @@ def readline(self, returns_unicode=False, startup_hook=None): def bind(self, spec, command): self.keymap = self.keymap + ((spec, command),) self.input_trans = input.KeymapTranslator( - self.keymap, - invalid_cls='invalid-key', - character_cls='self-insert') + self.keymap, invalid_cls="invalid-key", character_cls="self-insert" + ) def get_buffer(self, encoding=None): if encoding is None: encoding = self.console.encoding - return unicode('').join(self.buffer).encode(self.console.encoding) + return str("").join(self.buffer).encode(self.console.encoding) def get_unicode(self): """Return the current buffer as a unicode string.""" - return unicode('').join(self.buffer) + return "".join(self.buffer) def test(): from pyrepl.unix_console import UnixConsole + reader = Reader(UnixConsole()) reader.ps1 = "**> " reader.ps2 = "/*> " @@ -637,5 +632,5 @@ def test(): pass -if __name__ == '__main__': +if __name__ == "__main__": test() diff --git a/pyrepl/readline.py b/pyrepl/readline.py index 8ac466d..23ba864 100644 --- a/pyrepl/readline.py +++ b/pyrepl/readline.py @@ -26,58 +26,51 @@ extensions for multiline input. """ -import sys +import contextlib import os +import sys + from pyrepl import commands -from pyrepl.historical_reader import HistoricalReader from pyrepl.completing_reader import CompletingReader +from pyrepl.historical_reader import HistoricalReader 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 +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', + "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', + "multiline_input", ] # ____________________________________________________________ -class ReadlineConfig(object): +class ReadlineConfig: readline_completer = None - completer_delims = dict.fromkeys(' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?') + completer_delims = dict.fromkeys(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?") class ReadlineAlikeReader(HistoricalReader, CompletingReader): @@ -86,7 +79,7 @@ class ReadlineAlikeReader(HistoricalReader, CompletingReader): sort_in_column = True def error(self, msg="none"): - pass # don't show error messages by default + pass # don't show error messages by default def get_stem(self): b = self.buffer @@ -94,16 +87,16 @@ def get_stem(self): completer_delims = self.config.completer_delims while p >= 0 and b[p] not in completer_delims: p -= 1 - return ''.join(b[p+1:self.pos]) + return "".join(b[p + 1 : self.pos]) def get_completions(self, stem): result = [] function = self.config.readline_completer if function is not None: try: - stem = str(stem) # rlcompleter.py seems to not like unicode + stem = str(stem) # rlcompleter.py seems to not like unicode except UnicodeEncodeError: - pass # but feed unicode anyway if we have no choice + pass # but feed unicode anyway if we have no choice state = 0 while True: try: @@ -143,16 +136,15 @@ def get_trimmed_history(self, maxlength): more_lines = None def collect_keymap(self): - return super(ReadlineAlikeReader, self).collect_keymap() + ( - (r'\n', 'maybe-accept'),) + return super().collect_keymap() + ((r"\n", "maybe-accept"),) def __init__(self, console): - super(ReadlineAlikeReader, self).__init__(console) - self.commands['maybe_accept'] = maybe_accept - self.commands['maybe-accept'] = maybe_accept + super().__init__(console) + self.commands["maybe_accept"] = maybe_accept + self.commands["maybe-accept"] = maybe_accept def after_command(self, cmd): - super(ReadlineAlikeReader, self).after_command(cmd) + super().after_command(cmd) if self.more_lines is None: # Force single-line input if we are in raw_input() mode. # Although there is no direct way to add a \n in this mode, @@ -176,7 +168,7 @@ def do(self): # if there are already several lines and the cursor # is not on the last one, always insert a new \n. text = r.get_unicode() - if "\n" in r.buffer[r.pos:]: + if "\n" in r.buffer[r.pos :]: r.insert("\n") elif r.more_lines is not None and r.more_lines(text): r.insert("\n") @@ -184,7 +176,7 @@ def do(self): self.finish = 1 -class _ReadlineWrapper(object): +class _ReadlineWrapper: reader = None saved_history_length = -1 startup_hook = None @@ -209,7 +201,7 @@ def get_reader(self): self.reader.config = self.config return self.reader - def raw_input(self, prompt=''): + def raw_input(self, prompt="") -> str: try: reader = self.get_reader() except _error: @@ -221,18 +213,14 @@ def raw_input(self, prompt=''): # behavior: it seems to be the correct thing to do, and moreover it # mitigates this pytest issue: # https://github.com/pytest-dev/pytest/issues/5134 - if self.stdout and hasattr(self.stdout, 'flush'): + if self.stdout and hasattr(self.stdout, "flush"): self.stdout.flush() - if self.stderr and hasattr(self.stderr, 'flush'): + if self.stderr and hasattr(self.stderr, "flush"): self.stderr.flush() 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 str(ret, ENCODING) def multiline_input(self, more_lines, ps1, ps2, returns_unicode=False): """Read an input on possibly multiple lines, asking for more @@ -264,17 +252,10 @@ def set_completer_delims(self, string): def get_completer_delims(self): chars = list(self.config.completer_delims.keys()) chars.sort() - return ''.join(chars) + return "".join(chars) - 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') + def _histline(self, line: str): + return line.rstrip("\n") def get_history_length(self): return self.saved_history_length @@ -285,45 +266,36 @@ def set_history_length(self, length): def get_current_history_length(self): return len(self.get_reader().history) - def read_history_file(self, filename='~/.history'): + def read_history_file(self, filename="~/.history"): # multiline extension (really a hack) for the end of lines that # are actually continuations inside a single multiline_input() # 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') - buffer = [] - for line in f: - if line.endswith('\r\n'): - buffer.append(line) - else: - line = self._histline(line) - if buffer: - line = ''.join(buffer).replace('\r', '') + line - del buffer[:] - if line: - history.append(line) - f.close() - - def write_history_file(self, filename='~/.history'): + with open(os.path.expanduser(filename)) as f: + buffer = [] + for line in f: + if line.endswith("\r\n"): + buffer.append(line) + else: + line = self._histline(line) + if buffer: + line = "".join(buffer).replace("\r", "") + line + del buffer[:] + if line: + history.append(line) + + def write_history_file(self, filename="~/.history"): maxlength = self.saved_history_length history = self.get_reader().get_trimmed_history(maxlength) - entries = '' + 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 sys.version_info < (3,): - entry = entry.encode('utf-8') - entry = entry.replace('\n', '\r\n') # multiline history support - entries += entry + '\n' + entry = entry.replace("\n", "\r\n") # multiline history support + entries += entry + "\n" fname = os.path.expanduser(filename) - if PY3: - f = open(fname, 'w', encoding='utf-8') - else: - f = open(fname, 'w') - f.write(entries) - f.close() + with open(fname, "w", encoding="utf-8") as f: + f.write(entries) def clear_history(self): del self.get_reader().history[:] @@ -331,9 +303,9 @@ def clear_history(self): def get_history_item(self, index): history = self.get_reader().history if 1 <= index <= len(history): - return history[index-1] - else: - return None # blame readline.c for not raising + return history[index - 1] + + return None # blame readline.c for not raising def remove_history_item(self, index): history = self.get_reader().history @@ -358,9 +330,7 @@ def set_startup_hook(self, function=None): self.startup_hook = function def get_line_buffer(self): - if PY3: - return self.get_reader().get_unicode() - return self.get_reader().get_buffer() + return self.get_reader().get_unicode() def _get_idxs(self): start = cursor = self.get_reader().pos @@ -420,20 +390,24 @@ def insert_text(self, text): def _make_stub(_name, _ret): def stub(*args, **kwds): import warnings + warnings.warn("readline.%s() not implemented" % _name, stacklevel=2) - stub.func_name = _name + + stub.__name__ = _name globals()[_name] = stub + for _name, _ret in [ - ('read_init_file', None), - ('redisplay', None), - ('set_pre_input_hook', None), + ("read_init_file", None), + ("redisplay", None), + ("set_pre_input_hook", None), ]: assert _name not in globals(), _name _make_stub(_name, _ret) def _setup(): + # TODO: is the raw_input logic still required? global _old_raw_input if _old_raw_input is not None: return @@ -451,31 +425,33 @@ def _setup(): _wrapper.f_out = f_out _wrapper.setup_std_streams(sys.stdin, sys.stdout, sys.stderr) - if '__pypy__' in sys.builtin_module_names: # PyPy + if "__pypy__" in sys.builtin_module_names: # PyPy - def _old_raw_input(prompt=''): + def _old_raw_input(prompt=""): # sys.__raw_input__() is only called when stdin and stdout are # as expected and are ttys. If it is the case, then get_reader() # should not really fail in _wrapper.raw_input(). If it still # does, then we will just cancel the redirection and call again # the built-in raw_input(). - try: + with contextlib.suppress(AttributeError): 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: - import __builtin__ - _old_raw_input = __builtin__.raw_input - __builtin__.raw_input = _wrapper.raw_input + import builtins + + _old_raw_input = builtins.raw_input + builtins.raw_input = _wrapper.raw_input except ImportError: import builtins + _old_raw_input = builtins.input builtins.input = _wrapper.raw_input + _old_raw_input = None _setup() diff --git a/pyrepl/simple_interact.py b/pyrepl/simple_interact.py index 3b84a15..068c411 100644 --- a/pyrepl/simple_interact.py +++ b/pyrepl/simple_interact.py @@ -24,10 +24,11 @@ """ import sys -from pyrepl.readline import multiline_input, _error, _get_reader +from pyrepl.readline import _error, _get_reader, multiline_input -def check(): # returns False if there is a problem initializing the state + +def check(): # returns False if there is a problem initializing the state try: _get_reader() except _error: @@ -37,20 +38,18 @@ def check(): # returns False if there is a problem initializing the state def run_multiline_interactive_console(mainmodule=None, future_flags=0): import code + import __main__ + mainmodule = mainmodule or __main__ - console = code.InteractiveConsole(mainmodule.__dict__, filename='') + console = code.InteractiveConsole(mainmodule.__dict__, filename="") if future_flags: 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') - else: - src = unicodetext + src = unicodetext try: - code = console.compile(src, '', 'single') + code = console.compile(src, "", "single") except (OverflowError, SyntaxError, ValueError): return False else: @@ -58,11 +57,10 @@ def more_lines(unicodetext): while 1: try: - ps1 = getattr(sys, 'ps1', '>>> ') - ps2 = getattr(sys, 'ps2', '... ') + ps1 = getattr(sys, "ps1", ">>> ") + ps2 = getattr(sys, "ps2", "... ") try: - statement = multiline_input(more_lines, ps1, ps2, - returns_unicode=True) + statement = multiline_input(more_lines, ps1, ps2, returns_unicode=True) except EOFError: break more = console.push(statement) diff --git a/pyrepl/trace.py b/pyrepl/trace.py index 8b0dd35..5c95724 100644 --- a/pyrepl/trace.py +++ b/pyrepl/trace.py @@ -2,16 +2,13 @@ trace_filename = os.environ.get("PYREPL_TRACE") -if trace_filename is not None: - trace_file = open(trace_filename, 'a') -else: - trace_file = None +trace_file = open(trace_filename, "a") if trace_filename is not None else None + def trace(line, *k, **kw): if trace_file is None: return if k or kw: line = line.format(*k, **kw) - trace_file.write(line+'\n') + trace_file.write(line + "\n") trace_file.flush() - diff --git a/pyrepl/unix_console.py b/pyrepl/unix_console.py index 281c200..b87a6c5 100644 --- a/pyrepl/unix_console.py +++ b/pyrepl/unix_console.py @@ -19,35 +19,38 @@ # 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 contextlib import errno -import signal +import os import re -import time +import select +import signal +import struct import sys +import termios +import time from fcntl import ioctl + from . import curses -from .fancy_termios import tcgetattr, tcsetattr from .console import Console, Event -from .unix_eventqueue import EventQueue +from .fancy_termios import tcgetattr, tcsetattr from .trace import trace +from .unix_eventqueue import EventQueue class InvalidTerminal(RuntimeError): pass + try: - unicode + str except NameError: - unicode = str + str = str _error = (termios.error, curses.error, InvalidTerminal) # there are arguments for changing this to "refresh" -SIGWINCH_EVENT = 'repaint' +SIGWINCH_EVENT = "repaint" FIONREAD = getattr(termios, "FIONREAD", None) TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", None) @@ -57,19 +60,41 @@ 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 + ratedict = {} -for r in [0, 110, 115200, 1200, 134, 150, 1800, 19200, 200, 230400, - 2400, 300, 38400, 460800, 4800, 50, 57600, 600, 75, 9600]: +for r in [ + 0, + 110, + 115200, + 1200, + 134, + 150, + 1800, + 19200, + 200, + 230400, + 2400, + 300, + 38400, + 460800, + 4800, + 50, + 57600, + 600, + 75, + 9600, +]: maybe_add_baudrate(ratedict, r) del r, maybe_add_baudrate @@ -92,13 +117,15 @@ 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' +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') + "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): @@ -124,14 +151,14 @@ def __init__(self, f_in=0, f_out=1, term=None, encoding=None): self.term = term for name in required_curses_tistrings.split(): - setattr(self, '_' + name, _my_getstr(name)) + setattr(self, "_" + name, _my_getstr(name)) for name in optional_curses_tistrings.split(): - setattr(self, '_' + name, _my_getstr(name, optional=1)) + setattr(self, "_" + name, _my_getstr(name, optional=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 False: self.__move_x = self.__move_x_hpa elif self._cub and self._cuf: self.__move_x = self.__move_x_cub_cuf @@ -198,15 +225,15 @@ def refresh(self, screen, c_xy): offset = max(len(screen) - height, 0) screen.append("") - oldscr = self.screen[old_offset:old_offset + height] - newscr = screen[offset:offset + height] + oldscr = self.screen[old_offset : old_offset + height] + newscr = screen[offset : offset + height] # use hardware scrolling if we have it. if old_offset > offset and self._ri: self.__hide_cursor() self.__write_code(self._cup, 0, 0) self.__posxy = 0, old_offset - for i in range(old_offset - offset): + for _ in range(old_offset - offset): self.__write_code(self._ri) oldscr.pop(-1) oldscr.insert(0, "") @@ -214,16 +241,18 @@ def refresh(self, screen, c_xy): self.__hide_cursor() self.__write_code(self._cup, self.height - 1, 0) self.__posxy = 0, old_offset + self.height - 1 - for i in range(offset - old_offset): + for _ in range(offset - old_offset): self.__write_code(self._ind) oldscr.pop(0) oldscr.append("") self.__offset = offset - for y, oldline, newline, in zip(range(offset, offset + height), - oldscr, - newscr): + for ( + y, + oldline, + newline, + ) in zip(list(range(offset, offset + height)), oldscr, newscr): if oldline != newline: self.__write_changed_line(y, oldline, newline, px) @@ -252,24 +281,31 @@ 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': + # 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 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] + ): x = px self.__move(x, y) self.__write_code(self.ich1) self.__write(newline[x]) self.__posxy = x + 1, y - elif x < minlen and oldline[x + 1:] == newline[x + 1:]: + elif x < minlen and oldline[x + 1 :] == newline[x + 1 :]: self.__move(x, y) self.__write(newline[x]) self.__posxy = x + 1, y - elif (self.dch1 and self.ich1 and len(newline) == self.width - and x < len(newline) - 2 - and newline[x+1:-1] == oldline[x:-2]): + elif ( + self.dch1 + and self.ich1 + and len(newline) == self.width + and x < len(newline) - 2 + and newline[x + 1 : -1] == oldline[x:-2] + ): self.__hide_cursor() self.__move(self.width - 2, y) self.__posxy = self.width - 2, y @@ -286,8 +322,8 @@ def __write_changed_line(self, y, oldline, newline, px): self.__write(newline[x:]) self.__posxy = len(newline), y - #XXX: check for unicode mess - if '\x1b' in newline: + # 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 # to the left margin should work to get to a known position. @@ -297,7 +333,6 @@ def __write(self, text): self.__buffer.append((text, 0)) def __write_code(self, fmt, *args): - self.__buffer.append((curses.tparm(fmt, *args), 1)) def __maybe_write_code(self, fmt, *args): @@ -307,9 +342,9 @@ def __maybe_write_code(self, fmt, *args): def __move_y_cuu1_cud1(self, y): dy = y - self.__posxy[1] if dy > 0: - self.__write_code(dy*self._cud1) + self.__write_code(dy * self._cud1) elif dy < 0: - self.__write_code((-dy)*self._cuu1) + self.__write_code((-dy) * self._cuu1) def __move_y_cuu_cud(self, y): dy = y - self.__posxy[1] @@ -325,9 +360,9 @@ def __move_x_hpa(self, x): def __move_x_cub1_cuf1(self, x): dx = x - self.__posxy[0] if dx > 0: - self.__write_code(self._cuf1*dx) + self.__write_code(self._cuf1 * dx) elif dx < 0: - self.__write_code(self._cub1*(-dx)) + self.__write_code(self._cub1 * (-dx)) def __move_x_cub_cuf(self, x): dx = x - self.__posxy[0] @@ -346,7 +381,7 @@ def __move_tall(self, x, y): def move_cursor(self, x, y): if y < self.__offset or y >= self.__offset + self.height: - self.event_queue.insert(Event('scroll', None)) + self.event_queue.insert(Event("scroll", None)) else: self.__move(x, y) self.__posxy = x, y @@ -357,12 +392,12 @@ def prepare(self): self.__svtermstate = tcgetattr(self.input_fd) raw = self.__svtermstate.copy() raw.iflag |= termios.ICRNL - raw.iflag &= ~(termios.BRKINT | termios.INPCK | - termios.ISTRIP | termios.IXON) + raw.iflag &= ~(termios.BRKINT | termios.INPCK | termios.ISTRIP | termios.IXON) raw.cflag &= ~(termios.CSIZE | termios.PARENB) - raw.cflag |= (termios.CS8) - raw.lflag &= ~(termios.ICANON | termios.ECHO | - termios.IEXTEN | (termios.ISIG * 1)) + 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,18 +414,15 @@ def prepare(self): self.__maybe_write_code(self._smkx) - try: - self.old_sigwinch = signal.signal( - signal.SIGWINCH, self.__sigwinch) - except ValueError: - pass + with contextlib.suppress(ValueError): + self.old_sigwinch = signal.signal(signal.SIGWINCH, self.__sigwinch) def restore(self): self.__maybe_write_code(self._rmkx) self.flushoutput() tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate) - if hasattr(self, 'old_sigwinch'): + if hasattr(self, "old_sigwinch"): try: signal.signal(signal.SIGWINCH, self.old_sigwinch) del self.old_sigwinch @@ -400,12 +432,12 @@ def restore(self): def __sigwinch(self, signum, frame): self.height, self.width = self.getheightwidth() - self.event_queue.insert(Event('resize', None)) + self.event_queue.insert(Event("resize", None)) if self.old_sigwinch != signal.SIG_DFL: self.old_sigwinch(signum, frame) def push_char(self, char): - trace('push char {char!r}', char=char) + trace("push char {char!r}", char=char) self.event_queue.push(char) def get_event(self, block=1): @@ -414,7 +446,7 @@ def get_event(self, block=1): # All hail Unix! try: self.push_char(os.read(self.input_fd, 1)) - except (IOError, OSError) as err: + except OSError as err: if err.errno == errno.EINTR: if not self.event_queue.empty(): return self.event_queue.get() @@ -451,25 +483,29 @@ def repaint_prep(self): if not self.__gone_tall: self.__posxy = 0, self.__posxy[1] self.__write("\r") - ns = len(self.screen)*['\000'*self.width] + ns = len(self.screen) * ["\000" * self.width] self.screen = ns else: self.__posxy = 0, self.__offset self.__move(0, self.__offset) - ns = self.height*['\000'*self.width] + ns = self.height * ["\000" * self.width] self.screen = ns if TIOCGWINSZ: + def getheightwidth(self): try: 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] + "hhhh", ioctl(self.input_fd, TIOCGWINSZ, "\000" * 8) + )[0:2] if not height: return 25, 80 return height, width + else: + def getheightwidth(self): try: return int(os.environ["LINES"]), int(os.environ["COLUMNS"]) @@ -484,7 +520,7 @@ def flushoutput(self): if iscode: self.__tputs(text) else: - os.write(self.output_fd, text.encode(self.encoding, 'replace')) + os.write(self.output_fd, text.encode(self.encoding, "replace")) del self.__buffer[:] def __tputs(self, fmt, prog=delayprog): @@ -506,13 +542,13 @@ 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 "*" in m.group(2): delay *= self.height if self._pad: - nchars = (bps*delay)/1000 - os.write(self.output_fd, self._pad*nchars) + nchars = (bps * delay) / 1000 + os.write(self.output_fd, self._pad * nchars) else: - time.sleep(float(delay)/1000.0) + time.sleep(float(delay) / 1000.0) def finish(self): y = len(self.screen) - 1 @@ -527,25 +563,27 @@ def beep(self): self.flushoutput() if FIONREAD: + def getpending(self): - e = Event('key', '', '') + 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, "\0\0\0\0"))[0] + 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 + raw = str(data, self.encoding, "replace") + # XXX: something is wrong here e.data += raw e.raw += raw return e + else: + def getpending(self): - e = Event('key', '', '') + e = Event("key", "", "") while not self.event_queue.empty(): e2 = self.event_queue.get() @@ -554,8 +592,8 @@ def getpending(self): amount = 10000 data = os.read(self.input_fd, amount) - raw = unicode(data, self.encoding, 'replace') - #XXX: something is wrong here + raw = str(data, self.encoding, "replace") + # XXX: something is wrong here e.data += raw e.raw += raw return e diff --git a/pyrepl/unix_eventqueue.py b/pyrepl/unix_eventqueue.py index 332d952..39e81b8 100644 --- a/pyrepl/unix_eventqueue.py +++ b/pyrepl/unix_eventqueue.py @@ -21,19 +21,14 @@ # Bah, this would be easier to test if curses/terminfo didn't have so # much non-introspectable global state. +import os from collections import deque +from termios import VERASE, tcgetattr -from pyrepl import keymap +from pyrepl import curses, keymap from pyrepl.console import Event -from pyrepl import curses -from .trace import trace -from termios import tcgetattr, VERASE -import os -try: - unicode -except NameError: - unicode = str +from .trace import trace _keynames = { "delete": "kdch1", @@ -50,8 +45,8 @@ } -#function keys x in 1-20 -> fX: kfX -_keynames.update(('f%d' % i, 'kf%d' % i) for i in range(1, 21)) +# 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 @@ -65,18 +60,19 @@ # CTRL_ARROW_KEYCODE = { # for xterm, gnome-terminal, xfce terminal, etc. - b'\033[1;5D': 'ctrl left', - b'\033[1;5C': 'ctrl right', + b"\033[1;5D": "ctrl left", + b"\033[1;5C": "ctrl right", # for rxvt - b'\033Od': 'ctrl left', - b'\033Oc': 'ctrl right', + b"\033Od": "ctrl left", + b"\033Oc": "ctrl right", } + def general_keycodes(): keycodes = {} - for key, tiname in _keynames.items(): + for key, tiname in list(_keynames.items()): keycode = curses.tigetstr(tiname) - trace('key {key} tiname {tiname} keycode {keycode!r}', **locals()) + trace("key {key} tiname {tiname} keycode {keycode!r}", **locals()) if keycode: keycodes[keycode] = key keycodes.update(CTRL_ARROW_KEYCODE) @@ -87,13 +83,13 @@ def EventQueue(fd, encoding): keycodes = general_keycodes() if os.isatty(fd): backspace = tcgetattr(fd)[6][VERASE] - keycodes[backspace] = unicode('backspace') + keycodes[backspace] = "backspace" k = keymap.compile_keymap(keycodes) - trace('keymap {k!r}', k=k) + trace("keymap {k!r}", k=k) return EncodedQueue(k, encoding) -class EncodedQueue(object): +class EncodedQueue: def __init__(self, keymap, encoding): self.k = self.ck = keymap self.events = deque() @@ -115,7 +111,7 @@ def flush_buf(self): return old def insert(self, event): - trace('added event {event}', event=event) + trace("added event {event}", event=event) self.events.append(event) def push(self, char): @@ -124,23 +120,23 @@ def push(self, 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 + # sanity check, buffer is empty when a special key comes assert len(self.buf) == 1 k = self.k[char] - trace('found map {k!r}', k=k) + trace("found map {k!r}", k=k) if isinstance(k, dict): self.k = k else: - self.insert(Event('key', k, self.flush_buf())) + 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...') + trace("unrecognized escape sequence, propagating...") self.k = self.ck - self.insert(Event('key', '\033', bytearray(b'\033'))) + self.insert(Event("key", "\033", bytearray(b"\033"))) for c in self.flush_buf()[1:]: self.push(chr(c)) @@ -150,5 +146,5 @@ def push(self, char): except UnicodeError: 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/pythoni b/pythoni index 4c11eba..2d95ea1 100755 --- a/pythoni +++ b/pythoni @@ -19,18 +19,21 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import locale, pdb, sys +import locale +import pdb +import sys + # I forget exactly why this is necessary: try: - locale.setlocale(locale.LC_ALL, '') + locale.setlocale(locale.LC_ALL, "") except locale.Error: - pass # oh well + pass # oh well -from pyrepl.python_reader import main from pyrepl import cmdrepl +from pyrepl.python_reader import main # whizzy feature: graft pyrepl support onto pdb -#pdb.Pdb = cmdrepl.replize(pdb.Pdb, 1) +# pdb.Pdb = cmdrepl.replize(pdb.Pdb, 1) -main(use_pygame_console=('pg' in sys.argv)) +main(use_pygame_console=("pg" in sys.argv)) diff --git a/pythoni1 b/pythoni1 index ee5f289..4dde86e 100755 --- a/pythoni1 +++ b/pythoni1 @@ -10,10 +10,10 @@ import sys from pyrepl import readline from pyrepl.simple_interact import run_multiline_interactive_console -sys.modules['readline'] = readline +sys.modules["readline"] = readline -if os.getenv('PYTHONSTARTUP'): - exec(open(os.getenv('PYTHONSTARTUP')).read()) +if os.getenv("PYTHONSTARTUP"): + exec(open(os.getenv("PYTHONSTARTUP")).read()) -print('Python', sys.version) +print("Python", sys.version) run_multiline_interactive_console() diff --git a/testing/test_basic.py b/testing/test_basic.py deleted file mode 100644 index 66d53ca..0000000 --- a/testing/test_basic.py +++ /dev/null @@ -1,114 +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. -import pytest -from .infrastructure import read_spec - - -def test_basic(): - read_spec([(('self-insert', 'a'), ['a']), - ( 'accept', ['a'])]) - - -def test_repeat(): - read_spec([(('digit-arg', '3'), ['']), - (('self-insert', 'a'), ['aaa']), - ( 'accept', ['aaa'])]) - - -def test_kill_line(): - read_spec([(('self-insert', 'abc'), ['abc']), - ( 'left', None), - ( 'kill-line', ['ab']), - ( 'accept', ['ab'])]) - - -def test_unix_line_discard(): - read_spec([(('self-insert', 'abc'), ['abc']), - ( 'left', None), - ( 'unix-word-rubout', ['c']), - ( 'accept', ['c'])]) - - -def test_kill_word(): - read_spec([(('self-insert', 'ab cd'), ['ab cd']), - ( 'beginning-of-line', ['ab cd']), - ( 'kill-word', [' cd']), - ( 'accept', [' cd'])]) - - -def test_backward_kill_word(): - read_spec([(('self-insert', 'ab cd'), ['ab cd']), - ( 'backward-kill-word', ['ab ']), - ( 'accept', ['ab '])]) - - -def test_yank(): - read_spec([(('self-insert', 'ab cd'), ['ab cd']), - ( 'backward-kill-word', ['ab ']), - ( 'beginning-of-line', ['ab ']), - ( 'yank', ['cdab ']), - ( 'accept', ['cdab '])]) - - -def test_yank_pop(): - read_spec([(('self-insert', 'ab cd'), ['ab cd']), - ( 'backward-kill-word', ['ab ']), - ( 'left', ['ab ']), - ( 'backward-kill-word', [' ']), - ( 'yank', ['ab ']), - ( 'yank-pop', ['cd ']), - ( 'accept', ['cd '])]) - - -def test_interrupt(): - with pytest.raises(KeyboardInterrupt): - read_spec([('interrupt', [''])]) - - -# test_suspend -- hah -def test_up(): - read_spec([(('self-insert', 'ab\ncd'), ['ab', 'cd']), - ( 'up', ['ab', 'cd']), - (('self-insert', 'e'), ['abe', 'cd']), - ( 'accept', ['abe', 'cd'])]) - - -def test_down(): - read_spec([(('self-insert', 'ab\ncd'), ['ab', 'cd']), - ( 'up', ['ab', 'cd']), - (('self-insert', 'e'), ['abe', 'cd']), - ( 'down', ['abe', 'cd']), - (('self-insert', 'f'), ['abe', 'cdf']), - ( 'accept', ['abe', 'cdf'])]) - - -def test_left(): - read_spec([(('self-insert', 'ab'), ['ab']), - ( 'left', ['ab']), - (('self-insert', 'c'), ['acb']), - ( 'accept', ['acb'])]) - - -def test_right(): - read_spec([(('self-insert', 'ab'), ['ab']), - ( 'left', ['ab']), - (('self-insert', 'c'), ['acb']), - ( 'right', ['acb']), - (('self-insert', 'd'), ['acbd']), - ( 'accept', ['acbd'])]) diff --git a/testing/test_keymap.py b/testing/test_keymap.py deleted file mode 100644 index 12c4f0d..0000000 --- a/testing/test_keymap.py +++ /dev/null @@ -1,10 +0,0 @@ -from pyrepl.keymap import compile_keymap - - -def test_compile_keymap(): - k = compile_keymap({ - b'a': 'test', - b'bc': 'test2', - }) - - assert k == {b'a': 'test', b'b': {b'c': 'test2'}} diff --git a/testing/__init__.py b/tests/__init__.py similarity index 100% rename from testing/__init__.py rename to tests/__init__.py diff --git a/testing/infrastructure.py b/tests/infrastructure.py similarity index 87% rename from testing/infrastructure.py rename to tests/infrastructure.py index e90298b..7557538 100644 --- a/testing/infrastructure.py +++ b/tests/infrastructure.py @@ -17,12 +17,12 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -from __future__ import print_function -from pyrepl.reader import Reader + from pyrepl.console import Console, Event +from pyrepl.reader import Reader -class EqualsAnything(object): +class EqualsAnything: def __eq__(self, other): return True @@ -33,7 +33,7 @@ def __eq__(self, other): class TestConsole(Console): height = 24 width = 80 - encoding = 'utf-8' + encoding = "utf-8" def __init__(self, events, verbose=False): self.events = events @@ -42,8 +42,9 @@ def __init__(self, events, verbose=False): def refresh(self, screen, xy): if self.next_screen is not None: - assert screen == self.next_screen, "[ %s != %s after %r ]" % ( - screen, self.next_screen, self.last_event_name) + assert screen == self.next_screen, ( + f"[ {screen} != {self.next_screen}" "after {self.last_event_name} ]" + ) def get_event(self, block=1): ev, sc = self.events.pop(0) @@ -57,14 +58,14 @@ def get_event(self, block=1): def getpending(self): """Nothing pending, but do not return None here.""" - return Event('key', '', b'') + return Event("key", "", b"") class TestReader(Reader): __test__ = False def get_prompt(self, lineno, cursor_on_line): - return '' + return "" def refresh(self): Reader.refresh(self) diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..23df0a1 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,158 @@ +# 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. +import pytest + +from .infrastructure import read_spec + + +def test_basic(): + read_spec([(("self-insert", "a"), ["a"]), ("accept", ["a"])]) + + +def test_repeat(): + read_spec( + [ + (("digit-arg", "3"), [""]), + (("self-insert", "a"), ["aaa"]), + ("accept", ["aaa"]), + ] + ) + + +def test_kill_line(): + read_spec( + [ + (("self-insert", "abc"), ["abc"]), + ("left", None), + ("kill-line", ["ab"]), + ("accept", ["ab"]), + ] + ) + + +def test_unix_line_discard(): + read_spec( + [ + (("self-insert", "abc"), ["abc"]), + ("left", None), + ("unix-word-rubout", ["c"]), + ("accept", ["c"]), + ] + ) + + +def test_kill_word(): + read_spec( + [ + (("self-insert", "ab cd"), ["ab cd"]), + ("beginning-of-line", ["ab cd"]), + ("kill-word", [" cd"]), + ("accept", [" cd"]), + ] + ) + + +def test_backward_kill_word(): + read_spec( + [ + (("self-insert", "ab cd"), ["ab cd"]), + ("backward-kill-word", ["ab "]), + ("accept", ["ab "]), + ] + ) + + +def test_yank(): + read_spec( + [ + (("self-insert", "ab cd"), ["ab cd"]), + ("backward-kill-word", ["ab "]), + ("beginning-of-line", ["ab "]), + ("yank", ["cdab "]), + ("accept", ["cdab "]), + ] + ) + + +def test_yank_pop(): + read_spec( + [ + (("self-insert", "ab cd"), ["ab cd"]), + ("backward-kill-word", ["ab "]), + ("left", ["ab "]), + ("backward-kill-word", [" "]), + ("yank", ["ab "]), + ("yank-pop", ["cd "]), + ("accept", ["cd "]), + ] + ) + + +def test_interrupt(): + with pytest.raises(KeyboardInterrupt): + read_spec([("interrupt", [""])]) + + +# test_suspend -- hah +def test_up(): + read_spec( + [ + (("self-insert", "ab\ncd"), ["ab", "cd"]), + ("up", ["ab", "cd"]), + (("self-insert", "e"), ["abe", "cd"]), + ("accept", ["abe", "cd"]), + ] + ) + + +def test_down(): + read_spec( + [ + (("self-insert", "ab\ncd"), ["ab", "cd"]), + ("up", ["ab", "cd"]), + (("self-insert", "e"), ["abe", "cd"]), + ("down", ["abe", "cd"]), + (("self-insert", "f"), ["abe", "cdf"]), + ("accept", ["abe", "cdf"]), + ] + ) + + +def test_left(): + read_spec( + [ + (("self-insert", "ab"), ["ab"]), + ("left", ["ab"]), + (("self-insert", "c"), ["acb"]), + ("accept", ["acb"]), + ] + ) + + +def test_right(): + read_spec( + [ + (("self-insert", "ab"), ["ab"]), + ("left", ["ab"]), + (("self-insert", "c"), ["acb"]), + ("right", ["acb"]), + (("self-insert", "d"), ["acbd"]), + ("accept", ["acbd"]), + ] + ) diff --git a/testing/test_bugs.py b/tests/test_bugs.py similarity index 82% rename from testing/test_bugs.py rename to tests/test_bugs.py index bc7367c..2845ff5 100644 --- a/testing/test_bugs.py +++ b/tests/test_bugs.py @@ -17,31 +17,31 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +import pytest + from pyrepl.historical_reader import HistoricalReader + from .infrastructure import EA, TestReader, read_spec # this test case should contain as-verbatim-as-possible versions of # (applicable) bug reports -import pytest class HistoricalTestReader(HistoricalReader, TestReader): pass -@pytest.mark.xfail(reason='event missing', run=False) +@pytest.mark.xfail(reason="event missing", run=False) def test_transpose_at_start(): - read_spec([ - ('transpose', [EA, '']), - ('accept', [''])]) + read_spec([("transpose", [EA, ""]), ("accept", [""])]) def test_cmd_instantiation_crash(): spec = [ - ('reverse-history-isearch', ["(r-search `') "]), - (('key', 'left'), ['']), - ('accept', ['']) + ("reverse-history-isearch", ["(r-search `') "]), + (("key", "left"), [""]), + ("accept", [""]), ] read_spec(spec, HistoricalTestReader) @@ -50,6 +50,7 @@ def test_signal_failure(monkeypatch): import os import pty import signal + from pyrepl.unix_console import UnixConsole def failing_signal(a, b): @@ -63,9 +64,9 @@ def really_failing_signal(a, b): c = UnixConsole(sfd, sfd) c.prepare() c.restore() - monkeypatch.setattr(signal, 'signal', failing_signal) + monkeypatch.setattr(signal, "signal", failing_signal) c.prepare() - monkeypatch.setattr(signal, 'signal', really_failing_signal) + monkeypatch.setattr(signal, "signal", really_failing_signal) c.restore() finally: os.close(mfd) diff --git a/testing/test_curses.py b/tests/test_curses.py similarity index 87% rename from testing/test_curses.py rename to tests/test_curses.py index 5188f43..472cace 100644 --- a/testing/test_curses.py +++ b/tests/test_curses.py @@ -1,6 +1,7 @@ import pytest -from pyrepl.curses import setupterm + import pyrepl +from pyrepl.curses import setupterm def test_setupterm(monkeypatch): @@ -12,10 +13,10 @@ def test_setupterm(monkeypatch): ): setupterm("term_does_not_exist", 0) - monkeypatch.setenv('TERM', 'xterm') + monkeypatch.setenv("TERM", "xterm") assert setupterm(None, 0) is None - monkeypatch.delenv('TERM') + monkeypatch.delenv("TERM") with pytest.raises( pyrepl._minimal_curses.error, match=r"setupterm\(None, 0\) failed \(err=-1\)", diff --git a/testing/test_functional.py b/tests/test_functional.py similarity index 79% rename from testing/test_functional.py rename to tests/test_functional.py index f1f555b..bb01ea9 100644 --- a/testing/test_functional.py +++ b/tests/test_functional.py @@ -13,8 +13,7 @@ try: import pexpect except ImportError as exc: - pytest.skip("could not import pexpect: {}".format(exc), - allow_module_level=True) + pytest.skip(f"could not import pexpect: {exc}", allow_module_level=True) @pytest.fixture @@ -24,18 +23,13 @@ def start_child(): def start_child_func(env_update=None): assert not ret_childs, "child started already" - env = {k: v for k, v in os.environ.items() if k in ( - "TERM", - )} + env = {k: v for k, v in list(os.environ.items()) if k in ("TERM",)} if env_update: env.update(env_update) child = pexpect.spawn(sys.executable, timeout=5, env=env) - if sys.version_info >= (3, ): - child.logfile = sys.stdout.buffer - else: - child.logfile = sys.stdout + child.logfile = sys.stdout.buffer child.expect_exact(">>> ") - child.sendline('from pyrepl.python_reader import main') + child.sendline("from pyrepl.python_reader import main") ret_childs.append(child) return child @@ -62,10 +56,10 @@ def child(start_child): def test_basic(child): child.expect_exact("->> ") - child.sendline('a = 40 + 2') + child.sendline("a = 40 + 2") child.expect_exact("->> ") - child.sendline('a') - child.expect_exact('42') + child.sendline("a") + child.expect_exact("42") child.expect_exact("->> ") @@ -76,8 +70,9 @@ def test_sigwinch_default(child): def test_sigwinch_forwarded(start_child, tmpdir): with open(str(tmpdir.join("initfile")), "w") as initfile: - initfile.write(textwrap.dedent( - """ + initfile.write( + textwrap.dedent( + """ import signal called = [] @@ -89,7 +84,8 @@ def custom_handler(signum, frame): print("PYREPLSTARTUP called") """ - )) + ) + ) child = start_child(env_update={"PYREPLSTARTUP": initfile.name}) child.sendline("main()") diff --git a/tests/test_keymap.py b/tests/test_keymap.py new file mode 100644 index 0000000..ce8df85 --- /dev/null +++ b/tests/test_keymap.py @@ -0,0 +1,12 @@ +from pyrepl.keymap import compile_keymap + + +def test_compile_keymap(): + k = compile_keymap( + { + b"a": "test", + b"bc": "test2", + } + ) + + assert k == {b"a": "test", b"b": {b"c": "test2"}} diff --git a/testing/test_readline.py b/tests/test_readline.py similarity index 73% rename from testing/test_readline.py rename to tests/test_readline.py index a40aed0..16b8411 100644 --- a/testing/test_readline.py +++ b/tests/test_readline.py @@ -1,6 +1,5 @@ import os import pty -import sys import pytest from pyrepl.readline import _ReadlineWrapper @@ -12,46 +11,34 @@ def readline_wrapper(): return _ReadlineWrapper(slave, slave) -if sys.version_info < (3, ): - bytes_type = str - unicode_type = unicode # noqa: F821 -else: - bytes_type = bytes - unicode_type = str - - def test_readline(): master, slave = pty.openpty() readline_wrapper = _ReadlineWrapper(slave, slave) - os.write(master, b'input\n') + os.write(master, b"input\n") result = readline_wrapper.get_reader().readline() - assert result == b'input' - assert isinstance(result, bytes_type) + assert result == b"input" + assert isinstance(result, bytes) def test_readline_returns_unicode(): master, slave = pty.openpty() readline_wrapper = _ReadlineWrapper(slave, slave) - os.write(master, b'input\n') + os.write(master, b"input\n") result = readline_wrapper.get_reader().readline(returns_unicode=True) - assert result == 'input' - assert isinstance(result, unicode_type) + assert result == "input" + assert isinstance(result, str) def test_raw_input(): master, slave = pty.openpty() readline_wrapper = _ReadlineWrapper(slave, slave) - os.write(master, b'input\n') + 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) + result = readline_wrapper.raw_input("prompt:") + assert result == "input" + assert isinstance(result, str) def test_read_history_file(readline_wrapper, tmp_path): @@ -78,7 +65,7 @@ def test_write_history_file(readline_wrapper, tmp_path): readline_wrapper.write_history_file(str(histfile)) - with open(str(histfile), "r") as f: + with open(str(histfile)) as f: assert f.readlines() == ["foo\n", "bar\n"] @@ -92,7 +79,7 @@ def test_write_history_file_with_exception(readline_wrapper, tmp_path): class BadEntryException(Exception): pass - class BadEntry(object): + class BadEntry: @classmethod def replace(cls, *args): raise BadEntryException @@ -103,5 +90,5 @@ def replace(cls, *args): with pytest.raises(BadEntryException): readline_wrapper.write_history_file(str(histfile)) - with open(str(histfile), "r") as f: + with open(str(histfile)) as f: assert f.readlines() == ["foo\n", "bar\n"] diff --git a/testing/test_unix_reader.py b/tests/test_unix_reader.py similarity index 51% rename from testing/test_unix_reader.py rename to tests/test_unix_reader.py index 9fbcb2c..3c0976a 100644 --- a/testing/test_unix_reader.py +++ b/tests/test_unix_reader.py @@ -1,12 +1,11 @@ -from __future__ import unicode_literals from pyrepl.unix_eventqueue import EncodedQueue, Event def test_simple(): - q = EncodedQueue({}, 'utf-8') + q = EncodedQueue({}, "utf-8") - a = u'\u1234' - b = a.encode('utf-8') + a = "\u1234" + b = a.encode("utf-8") for c in b: q.push(c) @@ -27,21 +26,21 @@ def send(keys): if event is None: break events.append(event) - return events + return events keymap = { - b'\033': {b'U': 'up', b'D': 'down'}, - b'\xf7': 'backspace', + b"\033": {b"U": "up", b"D": "down"}, + b"\xf7": "backspace", } - q = EncodedQueue(keymap, 'utf-8') + 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'))] + 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') + events = send(b"\033\xf7") assert events == [ - Event('key', '\033', bytearray(b'\033')), - Event('key', 'backspace', bytearray(b'\xf7')) + Event("key", "\033", bytearray(b"\033")), + Event("key", "backspace", bytearray(b"\xf7")), ] diff --git a/testing/test_wishes.py b/tests/test_wishes.py similarity index 83% rename from testing/test_wishes.py rename to tests/test_wishes.py index d0c1e6f..d1a94a1 100644 --- a/testing/test_wishes.py +++ b/tests/test_wishes.py @@ -24,8 +24,11 @@ def test_quoted_insert_repeat(): - read_spec([ - (('digit-arg', '3'), ['']), - (('quoted-insert', None), ['']), - (('key', '\033'), ['^[^[^[']), - (('accept', None), None)]) + read_spec( + [ + (("digit-arg", "3"), [""]), + (("quoted-insert", None), [""]), + (("key", "\033"), ["^[^[^["]), + (("accept", None), None), + ] + ) diff --git a/tox.ini b/tox.ini index 67b30a5..f5bb3c8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,34,35,36,37,38,39,py,py3}, flake8 +envlist = py{38,39,310,311,py3}, flake8 [testenv] deps = @@ -19,18 +19,17 @@ setenv = [testenv:qa] deps = - flake8 - mccabe -commands = flake8 --max-complexity=10 setup.py pyrepl testing pythoni pythoni1 + ruff +commands = ruff check setup.py pyrepl tests pythoni pythoni1 [pytest] -testpaths = testing +testpaths = tests addopts = -ra filterwarnings = error [coverage:run] -include = pyrepl/*, testing/* +include = pyrepl/*, tests/* parallel = 1 branch = 1