├── .github
└── workflows
│ └── ci.yml
├── .travis.yml
├── CHANGES
├── CREDITS
├── LICENSE
├── MANIFEST.in
├── README
├── TODO
├── codecov.yml
├── pyrepl.rst
├── pyrepl
├── __init__.py
├── _minimal_curses.py
├── cmdrepl.py
├── commands.py
├── completer.py
├── completing_reader.py
├── console.py
├── curses.py
├── fancy_termios.py
├── historical_reader.py
├── input.py
├── keymap.py
├── keymaps.py
├── module_lister.py
├── pygame_console.py
├── pygame_keymap.py
├── python_reader.py
├── reader.py
├── readline.py
├── simple_interact.py
├── trace.py
├── unix_console.py
└── unix_eventqueue.py
├── pythoni
├── pythoni1
├── setup.py
├── testing
├── __init__.py
├── infrastructure.py
├── test_basic.py
├── test_bugs.py
├── test_curses.py
├── test_functional.py
├── test_keymap.py
├── test_readline.py
├── test_unix_reader.py
└── test_wishes.py
└── tox.ini
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - "master"
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | env:
11 | PIP_DISABLE_PIP_VERSION_CHECK: true
12 | PYTEST_ADDOPTS: "-vv"
13 | # Set TERM to some commonly used default
14 | # (not provided/setup by GitHub Actions by default).
15 | TERM: xterm-256color
16 |
17 | jobs:
18 | tests:
19 | runs-on: ${{ matrix.os }}
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | include:
25 | # Linux
26 | - tox_env: "py39-coverage"
27 | python: "3.9"
28 | os: ubuntu-20.04
29 | - tox_env: "py38-coverage"
30 | python: "3.8"
31 | os: ubuntu-20.04
32 | - tox_env: "py37-coverage"
33 | python: "3.7"
34 | os: ubuntu-20.04
35 | - tox_env: "py36-coverage"
36 | python: "3.6"
37 | os: ubuntu-20.04
38 | - tox_env: "py35-coverage"
39 | python: "3.5"
40 | os: ubuntu-20.04
41 |
42 | - tox_env: "py27-coverage"
43 | python: "2.7"
44 | os: ubuntu-20.04
45 |
46 | - tox_env: "pypy3-coverage"
47 | python: "pypy-3.7"
48 | os: ubuntu-20.04
49 | - tox_env: "pypy-coverage"
50 | python: "pypy-2.7"
51 | os: ubuntu-20.04
52 |
53 | steps:
54 | - uses: actions/checkout@v2
55 | with:
56 | fetch-depth: 0
57 | - name: Set up Python ${{ matrix.python }}
58 | uses: actions/setup-python@v2
59 | with:
60 | python-version: ${{ matrix.python }}
61 |
62 | # Caching.
63 | - name: set PY_CACHE_KEY
64 | run: echo "PY_CACHE_KEY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV
65 | - name: Cache .tox
66 | uses: actions/cache@v1
67 | with:
68 | path: ${{ github.workspace }}/.tox/${{ matrix.tox_env }}
69 | key: "tox|${{ matrix.os }}|${{ matrix.tox_env }}|${{ env.PY_CACHE_KEY }}|${{ hashFiles('tox.ini', 'setup.*') }}"
70 |
71 | - name: (Initial) version information/pinning
72 | run: |
73 | set -x
74 | python -m site
75 | python -m pip --version
76 | python -m pip list
77 | if [[ "${{ matrix.python }}" == "3.4" ]]; then
78 | # Install latest available pip.
79 | # 7.1.2 (installed) is too old to not install too new packages,
80 | # including pip itself. 19.2 dropped support for Python 3.4.
81 | python -m pip install -U pip==19.1.1
82 | fi
83 | python -m pip install -U setuptools==42.0.2
84 | python -m pip install -U virtualenv==20.4.3
85 |
86 | - name: Install tox
87 | run: python -m pip install git+https://github.com/blueyed/tox@master
88 |
89 | - name: Version information
90 | run: python -m pip list
91 |
92 | - name: Setup tox environment
93 | id: setup-tox
94 | run: python -m tox --notest -v --durations -e ${{ matrix.tox_env }}
95 |
96 | - name: Test
97 | env:
98 | COLUMNS: "90" # better alignment (working around https://github.com/blueyed/pytest/issues/491).
99 | PY_COLORS: "1"
100 | # UTF-8 mode for Windows (https://docs.python.org/3/using/windows.html#utf-8-mode).
101 | PYTHONUTF8: "1"
102 | TOX_TESTENV_PASSENV: "PYTHONUTF8"
103 | run: python -m tox -v --durations -e ${{ matrix.tox_env }}
104 |
105 | - name: Report coverage
106 | if: always() && (steps.setup-tox.outcome == 'success' && contains(matrix.tox_env, '-coverage'))
107 | uses: codecov/codecov-action@v1
108 | with:
109 | files: ./coverage.xml
110 | flags: ${{ runner.os }}
111 | name: ${{ matrix.tox_env }}
112 | fail_ci_if_error: true
113 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | language: python
3 |
4 | env:
5 | global:
6 | - PYTEST_ADDOPTS="-vv"
7 |
8 | jobs:
9 | include:
10 | - python: '3.4'
11 | env:
12 | - TOXENV=py34-coverage
13 | - PYTEST="pytest @ git+https://github.com/blueyed/pytest@my-4.6-maintenance"
14 |
15 | install:
16 | - pip install tox
17 |
18 | script:
19 | - tox --force-dep="$PYTEST"
20 |
21 | after_script:
22 | - |
23 | if [[ "${TOXENV%-coverage}" != "$TOXENV" ]]; then
24 | 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
25 | fi
26 |
27 | # Only master and releases. PRs are used otherwise.
28 | branches:
29 | only:
30 | - master
31 | - /^\d+\.\d+(\.\d+)?(-\S*)?$/
32 |
--------------------------------------------------------------------------------
/CHANGES:
--------------------------------------------------------------------------------
1 | This file contains more verbose descriptions of the changes between
2 | versions than those in README.
3 |
4 | New in Version 0.8.x:
5 |
6 | * propper command usage + fix one wrong call (thanks Trundle)
7 |
8 |
9 | New in 0.7.3:
10 | + ^T at the start of a line should not crash.
11 |
12 | New in 0.7.2:
13 | + Fix a couple of embarrassing typos in unix_console.py and
14 | python_reader.py
15 | + Ran pychecker over the rest. Found a few problems, and that pychecker
16 | really doesn't like some of my idioms...
17 | + "import no_such_package.[TAB]" should no longer crash.
18 |
19 | New in 0.7.1:
20 | + I learnt how to use distutils properly...
21 | + Another few days of pygame_console hacking; scrolling support is the
22 | biggy.
23 |
24 | New in 0.7.0:
25 | + Moved to a package architecture.
26 | + Wrote a (very simple!) distutils setup.py script.
27 | + Changed the keyspec format to be more sensible. See the docstring in
28 | pyrepl/keymap.py for more information.
29 | + Portability fixes (coping without FIONREAD, select.poll, sundry
30 | terminal capibilities, probably some other stuff).
31 | + Various tortuous changes to use 2.2 features where possible but
32 | retaining 2.1 support (I hope; haven't got a 2.1 here to test with).
33 | + Jumping up and down on control-C now shouldn't dump you out of
34 | pyrepl (via a large hammer kind of approach).
35 | + Cancelling a command with ^C left an empty string in the history.
36 | No longer. Fixing this proved to require making the history code
37 | more sensible, which was a good thing in itself.
38 | + reader.Reader has a new method, bind(), intended to be used by the
39 | user.
40 | + The $PYTHONSTARTUP file is executed in a namespace containing Reader.
41 | This means you can use the presence of the variable 'Reader' to tell
42 | whether you're executing in a pyrepl shell.
43 | + The $PYREPLSTARTUP env. var now overrides $PYTHONSTARTUP for pyrepl
44 | shells if set.
45 | + A prototype pygame console. To use this, call python_reader.main(1)
46 | instead of just main() or main(0), or run 'pythoni pg'
47 | - input is a bit messy right now
48 | - printing large gobs of text is really slow
49 | - you can't ^C running commands (you have to find the controlling
50 | terminal and type ^C there).
51 | - during long running commands we don't pump pygame's event loop, which
52 | is naughty. These last two are related and probably require threads
53 | (and much thought) to fix.
54 | - probably lots of other warts!
55 |
56 | New in 0.6.0:
57 | + Rewrote the low level code to be (hopefully) more portable and
58 | (certainly) clearer. Most of the code from 0.5.1 is still present,
59 | but it's been moved around and refactored (and the names have
60 | gotten more sensible).
61 | + The above process fixed a fair few bugs.
62 | + Implemented a few more emacs-mode bindings.
63 | + Nailed another couple of differences between my top-level and the
64 | the default one, including playing nice with Tk.
65 | + Implemented a saner way of handling window resizes (a sane way of
66 | handling signals! Call the Unix police!)
67 |
68 | New in 0.5.1:
69 | + Realizing that it wasn't going to run under 2.0 after all, I ripped
70 | out some of the 2.0 compatibility code.
71 |
72 | Version 0.5.0 is sufficiently historic that I can't remember what it
73 | was like :)
74 |
--------------------------------------------------------------------------------
/CREDITS:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pypy/pyrepl/ca192a80b76700118b9bfd261a3d098b92ccfc31/CREDITS
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Permission to use, copy, modify, and distribute this software and its
2 | documentation for any purpose is hereby granted without fee, provided
3 | that the above copyright notice appear in all copies and that both
4 | that copyright notice and this permission notice appear in supporting
5 | documentation.
6 |
7 | THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
8 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
9 | FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
10 | INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
11 | FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
12 | NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
13 | WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
15 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include TODO CREDITS CHANGES pythoni encopyright.py LICENSE
2 | include MANIFEST.in
3 | recursive-include testing *.py
4 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | This is pyrepl, a readline-a-like in Python.
2 |
3 | It requires python 2.7 (or newer) and features:
4 |
5 | * sane multi-line editing
6 | * history, with incremental search
7 | * completion, including displaying of available options
8 | * a fairly large subset of the readline emacs-mode keybindings
9 | (adding more is mostly just a matter of typing)
10 | * a liberal, Python-style, license
11 | * a new python top-level
12 | * no global variables, so you can run two or more independent readers
13 | without having their histories interfering.
14 | * no hogging of control -- it should be easy to integrate pyrepl into
15 | YOUR application's event loop.
16 | * generally speaking, a much more interactive experience than readline
17 | (it's a bit like a cross between readline and emacs's mini-buffer)
18 | * unicode support (given terminal support)
19 |
20 | There are probably still a few little bugs & misfeatures, but _I_ like
21 | it, and use it as my python top-level most of the time.
22 |
23 | To get a feel for it, just execute:
24 |
25 | $ python pythoni
26 |
27 | (One point that may confuse: because the arrow keys are used to move
28 | up and down in the command currently being edited, you need to use ^P
29 | and ^N to move through the history)
30 |
31 | If you like what you see, you can install it with the familiar
32 |
33 | $ python setup.py install
34 |
35 | which will also install the above "pythoni" script.
36 |
37 |
38 | Summary of 0.8.4:
39 |
40 | + python3 support
41 | + support for more readline hooks
42 | + backport various fixes from pypy
43 | + gracefully break on sys.stdout.close()
44 |
45 | Summary of 0.8.3:
46 |
47 | + First release from new home on bitbucket.
48 | + Various fixes to pyrepl.readline.
49 | + Allow pyrepl to run if unicodedata is unimportable.
50 |
51 |
52 | Summary of 0.8.2:
53 |
54 | + This is the same version which is distributed with PyPy 1.4, which uses it
55 | as its default interactive interpreter:
56 |
57 | - have the possibility of having a "CPython-like" prompt, with ">>>" as
58 | PS1 and "..." as PS2
59 |
60 | - add the pyrepl.readline module, which exposes a subset of CPython's
61 | readline implemented on top of pyrepl
62 |
63 | + Add support for colored completions: see e.g. fancycomplete:
64 | http://bitbucket.org/antocuni/fancycompleter
65 |
66 | Summary of 0.8.1:
67 | + Fixes
68 | - in the area of unbound keys and unknown commands
69 | - in quoted-insert
70 | - in unicode support
71 | + make Reader and subclasses new-style classes
72 | - make the inheritance hierachy look like this
73 | Reader
74 | / \
75 | HistoricalReader CompletingReader
76 | \ /
77 | PythonicReader
78 | Turns out I've been wanting new-style classes since before they existed!
79 | - needed to slightly change the way keymaps are built
80 |
81 | Summary of 0.8.0:
82 | + A whole bundle of things.
83 | - unicode support (although working out what encoding the terminal
84 | is using can be "tricky")
85 | - internal rearchitecting
86 | - probably a bunch of new bugs...
87 | + Development and web-presence moved to codespeak.net
88 |
89 | Summary of new stuff in 0.7.1:
90 | + A non-broken setup.py...
91 |
92 | Summary of new stuff in 0.7.0:
93 | + Moved to a package architecture.
94 | + Wrote a (very simple!) distutils setup.py script.
95 | + Changed the keyspec format to be more sensible. See the docstring
96 | in pyrepl/keymap.py for more information.
97 | + Portability fixes.
98 | + Various tortuous changes to use 2.2 features where possible but
99 | retaining 2.1 support (I hope; haven't got a 2.1 here to test with).
100 | + Jumping up and down on control-C now shouldn't dump you out of
101 | pyrepl (via a large hammer kind of approach).
102 | + Bug fixes, particularly in the history handling stuff.
103 | + reader.Reader has a new method, bind(), intended to be used by the
104 | user.
105 | + Changes to the init file handling.
106 | + Sundry code reorganization. Libraries built on top of pyrepl will
107 | probably require small modifications (but I'm not sure anyone has
108 | written any of these yet!).
109 | + A prototypical pygame console.
110 | -- see CHANGES for more details and older news
111 |
112 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | In no particular order:
2 | + vi-mode
3 | + GUI toolkit friendliness (done for tk)
4 | + point-and-mark?
5 | + user customization support (maybe even try to read $INPUTRC?)
6 | + in general, implement more readline functions (just a matter of
7 | typing, for the most part)
8 | + a more incremental way of updating the screen.
9 | + unicode support
10 | + undo support
11 | + port to windows
12 |
13 | + reduce verbosity
14 | + tabs
15 | + look up delete in terminfo? tcsetattr(self.fd).cc[termios.VERASE]
16 | + mention license?
17 |
18 | + find a font. maybe I can bundle andale mono?
19 |
20 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project: true
4 | patch: true
5 | changes: true
6 | comment: false
7 |
--------------------------------------------------------------------------------
/pyrepl.rst:
--------------------------------------------------------------------------------
1 | pyrepl
2 | ======
3 |
4 | For ages now, I've been working on and off on a replacement for
5 | readline for use from Python. readline is undoubtedly great, but a
6 | couple of things irritate me about it. One is the inability to do
7 | sane multi-line editing. Have you ever typed something like::
8 |
9 | >>> for i in range(10):
10 | ... for i in range(10):
11 | ... print i*j
12 |
13 | into a Python top-level? Grr, that "i" on the second line should have
14 | been a "j". Wouldn't it be nice if you could just press "up" on your
15 | keyboard and fix it? This was one of the aims I kept in mind when
16 | writing pyrepl (or pyrl as I used to call it, but that name's
17 | `taken `_).
18 |
19 | Another irritation of readline is the GPL. I'm not even nearly as
20 | anti-GPL as some, but I don't want to have to GPL my program just so I
21 | can use readline.
22 |
23 | 0.7 adds to the version that runs an a terminal an experimental
24 | version that runs in a pygame surface. A long term goal is
25 | Mathematica-like notebooks, but that's a loong way off...
26 |
27 | Anyway, after many months of intermittent toil I present:
28 |
29 |
30 | Dependencies: Python 2.7 with the termios and curses modules built (I
31 | don't really use curses, but I need the terminfo functions that live
32 | in the curses module), or pygame installed (if you want to live on the
33 | bleeding edge).
34 |
35 | There are probably portability gremlins in some of the ioctl using
36 | code. Fixes gratefully received!
37 |
38 | Features:
39 | * sane multi-line editing
40 | * history, with incremental search
41 | * completion, including displaying of available options
42 | * a fairly large subset of the readline emacs-mode key bindings (adding
43 | more is mostly just a matter of typing)
44 | * Deliberately liberal, Python-style license
45 | * a new python top-level that I really like; possibly my favourite
46 | feature I've yet added is the ability to type::
47 |
48 | ->> from __f
49 |
50 | and hit TAB to get::
51 |
52 | ->> from __future__
53 |
54 | then you type " import n" and hit tab again to get::
55 |
56 | ->> from __future__ import nested_scopes
57 |
58 | (this is very addictive!).
59 |
60 | * no global variables, so you can run two independent
61 | readers without having their histories interfering.
62 | * An experimental version that runs in a pygame surface.
63 |
64 | pyrepl currently consists of four major classes::
65 |
66 | Reader - HistoricalReader - CompletingReader - PythonReader
67 |
68 |
69 | There's also a **UnixConsole** class that handles the low-level
70 | details.
71 |
72 | Each of these lives in it's own file, and there are a bunch of support
73 | files (including a C module that just provides a bit of a speed up -
74 | building it is strictly optional).
75 |
76 | IMHO, the best way to get a feel for how it works is to type::
77 |
78 | $ python pythoni
79 |
80 | and just play around. If you're used to readline's emacs-mode, you
81 | should feel right at home. One point that might confuse: because the
82 | arrow keys are used to move up and down in the command currently being
83 | edited, you need to use ^P and ^N to move through the history.
84 |
85 |
--------------------------------------------------------------------------------
/pyrepl/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2008 Michael Hudson-Doyle
2 | # Armin Rigo
3 | #
4 | # All Rights Reserved
5 | #
6 | #
7 | # Permission to use, copy, modify, and distribute this software and
8 | # its documentation for any purpose is hereby granted without fee,
9 | # provided that the above copyright notice appear in all copies and
10 | # that both that copyright notice and this permission notice appear in
11 | # supporting documentation.
12 | #
13 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 |
--------------------------------------------------------------------------------
/pyrepl/_minimal_curses.py:
--------------------------------------------------------------------------------
1 | """Minimal '_curses' module, the low-level interface for curses module
2 | which is not meant to be used directly.
3 |
4 | Based on ctypes. It's too incomplete to be really called '_curses', so
5 | to use it, you have to import it and stick it in sys.modules['_curses']
6 | manually.
7 |
8 | Note that there is also a built-in module _minimal_curses which will
9 | hide this one if compiled in.
10 | """
11 |
12 | import ctypes.util
13 |
14 |
15 | class error(Exception):
16 | pass
17 |
18 |
19 | def _find_clib():
20 | trylibs = ['ncursesw', 'ncurses', 'curses']
21 |
22 | for lib in trylibs:
23 | path = ctypes.util.find_library(lib)
24 | if path:
25 | return path
26 | raise ImportError("curses library not found")
27 |
28 | _clibpath = _find_clib()
29 | clib = ctypes.cdll.LoadLibrary(_clibpath)
30 |
31 | clib.setupterm.argtypes = [ctypes.c_char_p, ctypes.c_int,
32 | ctypes.POINTER(ctypes.c_int)]
33 | clib.setupterm.restype = ctypes.c_int
34 |
35 | clib.tigetstr.argtypes = [ctypes.c_char_p]
36 | clib.tigetstr.restype = ctypes.POINTER(ctypes.c_char)
37 |
38 | clib.tparm.argtypes = [ctypes.c_char_p] + 9 * [ctypes.c_int]
39 | clib.tparm.restype = ctypes.c_char_p
40 |
41 | OK = 0
42 | ERR = -1
43 |
44 | # ____________________________________________________________
45 |
46 | try:
47 | from __pypy__ import builtinify
48 | builtinify # silence broken pyflakes
49 | except ImportError:
50 | builtinify = lambda f: f
51 |
52 |
53 | @builtinify
54 | def setupterm(termstr, fd):
55 | if termstr is not None:
56 | if not isinstance(termstr, bytes):
57 | termstr = termstr.encode()
58 | err = ctypes.c_int(0)
59 | result = clib.setupterm(termstr, fd, ctypes.byref(err))
60 | if result == ERR:
61 | raise error("setupterm(%r, %d) failed (err=%d)" % (
62 | termstr, fd, err.value))
63 |
64 |
65 | @builtinify
66 | def tigetstr(cap):
67 | if not isinstance(cap, bytes):
68 | cap = cap.encode('ascii')
69 | result = clib.tigetstr(cap)
70 | if ctypes.cast(result, ctypes.c_void_p).value == ERR:
71 | return None
72 | return ctypes.cast(result, ctypes.c_char_p).value
73 |
74 |
75 | @builtinify
76 | def tparm(str, i1=0, i2=0, i3=0, i4=0, i5=0, i6=0, i7=0, i8=0, i9=0):
77 | result = clib.tparm(str, i1, i2, i3, i4, i5, i6, i7, i8, i9)
78 | if result is None:
79 | raise error("tparm() returned NULL")
80 | return result
81 |
--------------------------------------------------------------------------------
/pyrepl/cmdrepl.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2007 Michael Hudson-Doyle
2 | # Maciek Fijalkowski
3 | #
4 | # All Rights Reserved
5 | #
6 | #
7 | # Permission to use, copy, modify, and distribute this software and
8 | # its documentation for any purpose is hereby granted without fee,
9 | # provided that the above copyright notice appear in all copies and
10 | # that both that copyright notice and this permission notice appear in
11 | # supporting documentation.
12 | #
13 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 |
21 | """Wedge pyrepl behaviour into cmd.Cmd-derived classes.
22 |
23 | replize, when given a subclass of cmd.Cmd, returns a class that
24 | behaves almost identically to the supplied class, except that it uses
25 | pyrepl instead if raw_input.
26 |
27 | It was designed to let you do this:
28 |
29 | >>> import pdb
30 | >>> from pyrepl import replize
31 | >>> pdb.Pdb = replize(pdb.Pdb)
32 |
33 | which is in fact done by the `pythoni' script that comes with
34 | pyrepl."""
35 |
36 | from __future__ import print_function
37 |
38 | from pyrepl import completer
39 | from pyrepl.completing_reader import CompletingReader as CR
40 | import cmd
41 |
42 |
43 | class CmdReader(CR):
44 | def collect_keymap(self):
45 | return super(CmdReader, self).collect_keymap() + (
46 | ("\\M-\\n", "invalid-key"),
47 | ("\\n", "accept"))
48 |
49 | def __init__(self, completions):
50 | super(CmdReader, self).__init__()
51 | self.completions = completions
52 |
53 | def get_completions(self, stem):
54 | if len(stem) != self.pos:
55 | return []
56 | return sorted(set(s
57 | for s in self.completions
58 | if s.startswith(stem)))
59 |
60 |
61 | def replize(klass, history_across_invocations=1):
62 |
63 | """Return a subclass of the cmd.Cmd-derived klass that uses
64 | pyrepl instead of readline.
65 |
66 | Raises a ValueError if klass does not derive from cmd.Cmd.
67 |
68 | The optional history_across_invocations parameter (default 1)
69 | controls whether instances of the returned class share
70 | histories."""
71 |
72 | completions = [s[3:]
73 | for s in completer.get_class_members(klass)
74 | if s.startswith("do_")]
75 |
76 | assert issubclass(klass, cmd.Cmd)
77 | # if klass.cmdloop.im_class is not cmd.Cmd:
78 | # print "this may not work"
79 |
80 | class MultiHist(object):
81 | __history = []
82 |
83 | def __init__(self, *args, **kw):
84 | super(MultiHist, self).__init__(*args, **kw)
85 | self.__reader = CmdReader(completions)
86 | self.__reader.history = self.__history
87 | self.__reader.historyi = len(self.__history)
88 |
89 | class SimpleHist(object):
90 | def __init__(self, *args, **kw):
91 | super(SimpleHist, self).__init__(*args, **kw)
92 | self.__reader = CmdReader(completions)
93 |
94 | class CmdLoopMixin(object):
95 | def cmdloop(self, intro=None):
96 | self.preloop()
97 | if intro is not None:
98 | self.intro = intro
99 | if self.intro:
100 | print(self.intro)
101 | stop = None
102 | while not stop:
103 | if self.cmdqueue:
104 | line = self.cmdqueue[0]
105 | del self.cmdqueue[0]
106 | else:
107 | try:
108 | self.__reader.ps1 = self.prompt
109 | line = self.__reader.readline()
110 | except EOFError:
111 | line = "EOF"
112 | line = self.precmd(line)
113 | stop = self.onecmd(line)
114 | stop = self.postcmd(stop, line)
115 | self.postloop()
116 |
117 | hist = MultiHist if history_across_invocations else SimpleHist
118 |
119 | class CmdRepl(hist, CmdLoopMixin, klass):
120 | __name__ = "replize(%s.%s)" % (klass.__module__, klass.__name__)
121 | return CmdRepl
122 |
--------------------------------------------------------------------------------
/pyrepl/commands.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2010 Michael Hudson-Doyle
2 | # Antonio Cuni
3 | # Armin Rigo
4 | #
5 | # All Rights Reserved
6 | #
7 | #
8 | # Permission to use, copy, modify, and distribute this software and
9 | # its documentation for any purpose is hereby granted without fee,
10 | # provided that the above copyright notice appear in all copies and
11 | # that both that copyright notice and this permission notice appear in
12 | # supporting documentation.
13 | #
14 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
15 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
16 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
17 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
18 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
19 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
20 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 |
22 | import sys, os
23 |
24 | # Catgories of actions:
25 | # killing
26 | # yanking
27 | # motion
28 | # editing
29 | # history
30 | # finishing
31 | # [completion]
32 |
33 | class Command(object):
34 | finish = 0
35 | kills_digit_arg = 1
36 |
37 | def __init__(self, reader, event_name, event):
38 | self.reader = reader
39 | self.event = event
40 | self.event_name = event_name
41 |
42 | def do(self):
43 | pass
44 |
45 | class KillCommand(Command):
46 | def kill_range(self, start, end):
47 | if start == end:
48 | return
49 | r = self.reader
50 | b = r.buffer
51 | text = b[start:end]
52 | del b[start:end]
53 | if is_kill(r.last_command):
54 | if start < r.pos:
55 | r.kill_ring[-1] = text + r.kill_ring[-1]
56 | else:
57 | r.kill_ring[-1] = r.kill_ring[-1] + text
58 | else:
59 | r.kill_ring.append(text)
60 | r.pos = start
61 | r.dirty = 1
62 |
63 | class YankCommand(Command):
64 | pass
65 |
66 | class MotionCommand(Command):
67 | pass
68 |
69 | class EditCommand(Command):
70 | pass
71 |
72 | class FinishCommand(Command):
73 | finish = 1
74 | pass
75 |
76 | def is_kill(command):
77 | return command and issubclass(command, KillCommand)
78 |
79 | def is_yank(command):
80 | return command and issubclass(command, YankCommand)
81 |
82 | # etc
83 |
84 | class digit_arg(Command):
85 | kills_digit_arg = 0
86 | def do(self):
87 | r = self.reader
88 | c = self.event[-1]
89 | if c == "-":
90 | if r.arg is not None:
91 | r.arg = -r.arg
92 | else:
93 | r.arg = -1
94 | else:
95 | d = int(c)
96 | if r.arg is None:
97 | r.arg = d
98 | else:
99 | if r.arg < 0:
100 | r.arg = 10*r.arg - d
101 | else:
102 | r.arg = 10*r.arg + d
103 | r.dirty = 1
104 |
105 | class clear_screen(Command):
106 | def do(self):
107 | r = self.reader
108 | r.console.clear()
109 | r.dirty = 1
110 |
111 | class refresh(Command):
112 | def do(self):
113 | self.reader.dirty = 1
114 |
115 | class repaint(Command):
116 | def do(self):
117 | self.reader.dirty = 1
118 | self.reader.console.repaint_prep()
119 |
120 | class kill_line(KillCommand):
121 | def do(self):
122 | r = self.reader
123 | b = r.buffer
124 | eol = r.eol()
125 | for c in b[r.pos:eol]:
126 | if not c.isspace():
127 | self.kill_range(r.pos, eol)
128 | return
129 | else:
130 | self.kill_range(r.pos, eol+1)
131 |
132 | class unix_line_discard(KillCommand):
133 | def do(self):
134 | r = self.reader
135 | self.kill_range(r.bol(), r.pos)
136 |
137 | # XXX unix_word_rubout and backward_kill_word should actually
138 | # do different things...
139 |
140 | class unix_word_rubout(KillCommand):
141 | def do(self):
142 | r = self.reader
143 | for i in range(r.get_arg()):
144 | self.kill_range(r.bow(), r.pos)
145 |
146 | class kill_word(KillCommand):
147 | def do(self):
148 | r = self.reader
149 | for i in range(r.get_arg()):
150 | self.kill_range(r.pos, r.eow())
151 |
152 | class backward_kill_word(KillCommand):
153 | def do(self):
154 | r = self.reader
155 | for i in range(r.get_arg()):
156 | self.kill_range(r.bow(), r.pos)
157 |
158 | class yank(YankCommand):
159 | def do(self):
160 | r = self.reader
161 | if not r.kill_ring:
162 | r.error("nothing to yank")
163 | return
164 | r.insert(r.kill_ring[-1])
165 |
166 | class yank_pop(YankCommand):
167 | def do(self):
168 | r = self.reader
169 | b = r.buffer
170 | if not r.kill_ring:
171 | r.error("nothing to yank")
172 | return
173 | if not is_yank(r.last_command):
174 | r.error("previous command was not a yank")
175 | return
176 | repl = len(r.kill_ring[-1])
177 | r.kill_ring.insert(0, r.kill_ring.pop())
178 | t = r.kill_ring[-1]
179 | b[r.pos - repl:r.pos] = t
180 | r.pos = r.pos - repl + len(t)
181 | r.dirty = 1
182 |
183 | class interrupt(FinishCommand):
184 | def do(self):
185 | import signal
186 | self.reader.console.finish()
187 | os.kill(os.getpid(), signal.SIGINT)
188 |
189 | class suspend(Command):
190 | def do(self):
191 | import signal
192 | r = self.reader
193 | p = r.pos
194 | r.console.finish()
195 | os.kill(0, signal.SIGSTOP)
196 | ## this should probably be done
197 | ## in a handler for SIGCONT?
198 | r.console.prepare()
199 | r.pos = p
200 | r.posxy = 0, 0
201 | r.dirty = 1
202 | r.console.screen = []
203 |
204 | class up(MotionCommand):
205 | def do(self):
206 | r = self.reader
207 | for i in range(r.get_arg()):
208 | bol1 = r.bol()
209 | if bol1 == 0:
210 | if r.historyi > 0:
211 | r.select_item(r.historyi - 1)
212 | return
213 | r.pos = 0
214 | r.error("start of buffer")
215 | return
216 | bol2 = r.bol(bol1-1)
217 | line_pos = r.pos - bol1
218 | if line_pos > bol1 - bol2 - 1:
219 | r.sticky_y = line_pos
220 | r.pos = bol1 - 1
221 | else:
222 | r.pos = bol2 + line_pos
223 |
224 | class down(MotionCommand):
225 | def do(self):
226 | r = self.reader
227 | b = r.buffer
228 | for i in range(r.get_arg()):
229 | bol1 = r.bol()
230 | eol1 = r.eol()
231 | if eol1 == len(b):
232 | if r.historyi < len(r.history):
233 | r.select_item(r.historyi + 1)
234 | r.pos = r.eol(0)
235 | return
236 | r.pos = len(b)
237 | r.error("end of buffer")
238 | return
239 | eol2 = r.eol(eol1+1)
240 | if r.pos - bol1 > eol2 - eol1 - 1:
241 | r.pos = eol2
242 | else:
243 | r.pos = eol1 + (r.pos - bol1) + 1
244 |
245 | class left(MotionCommand):
246 | def do(self):
247 | r = self.reader
248 | for i in range(r.get_arg()):
249 | p = r.pos - 1
250 | if p >= 0:
251 | r.pos = p
252 | else:
253 | self.reader.error("start of buffer")
254 |
255 | class right(MotionCommand):
256 | def do(self):
257 | r = self.reader
258 | b = r.buffer
259 | for i in range(r.get_arg()):
260 | p = r.pos + 1
261 | if p <= len(b):
262 | r.pos = p
263 | else:
264 | self.reader.error("end of buffer")
265 |
266 | class beginning_of_line(MotionCommand):
267 | def do(self):
268 | self.reader.pos = self.reader.bol()
269 |
270 | class end_of_line(MotionCommand):
271 | def do(self):
272 | r = self.reader
273 | self.reader.pos = self.reader.eol()
274 |
275 | class home(MotionCommand):
276 | def do(self):
277 | self.reader.pos = 0
278 |
279 | class end(MotionCommand):
280 | def do(self):
281 | self.reader.pos = len(self.reader.buffer)
282 |
283 | class forward_word(MotionCommand):
284 | def do(self):
285 | r = self.reader
286 | for i in range(r.get_arg()):
287 | r.pos = r.eow()
288 |
289 | class backward_word(MotionCommand):
290 | def do(self):
291 | r = self.reader
292 | for i in range(r.get_arg()):
293 | r.pos = r.bow()
294 |
295 | class self_insert(EditCommand):
296 | def do(self):
297 | r = self.reader
298 | r.insert(self.event * r.get_arg())
299 |
300 | class insert_nl(EditCommand):
301 | def do(self):
302 | r = self.reader
303 | r.insert("\n" * r.get_arg())
304 |
305 | class transpose_characters(EditCommand):
306 | def do(self):
307 | r = self.reader
308 | b = r.buffer
309 | s = r.pos - 1
310 | if s < 0:
311 | r.error("cannot transpose at start of buffer")
312 | else:
313 | if s == len(b):
314 | s -= 1
315 | t = min(s + r.get_arg(), len(b) - 1)
316 | c = b[s]
317 | del b[s]
318 | b.insert(t, c)
319 | r.pos = t
320 | r.dirty = 1
321 |
322 | class backspace(EditCommand):
323 | def do(self):
324 | r = self.reader
325 | b = r.buffer
326 | for i in range(r.get_arg()):
327 | if r.pos > 0:
328 | r.pos -= 1
329 | del b[r.pos]
330 | r.dirty = 1
331 | else:
332 | self.reader.error("can't backspace at start")
333 |
334 | class delete(EditCommand):
335 | def do(self):
336 | r = self.reader
337 | b = r.buffer
338 | if ( r.pos == 0 and len(b) == 0 # this is something of a hack
339 | and self.event[-1] == "\004"):
340 | r.update_screen()
341 | r.console.finish()
342 | raise EOFError
343 | for i in range(r.get_arg()):
344 | if r.pos != len(b):
345 | del b[r.pos]
346 | r.dirty = 1
347 | else:
348 | self.reader.error("end of buffer")
349 |
350 | class accept(FinishCommand):
351 | def do(self):
352 | pass
353 |
354 | class help(Command):
355 | def do(self):
356 | self.reader.msg = self.reader.help_text
357 | self.reader.dirty = 1
358 |
359 | class invalid_key(Command):
360 | def do(self):
361 | pending = self.reader.console.getpending()
362 | s = ''.join(self.event) + pending.data
363 | self.reader.error("`%r' not bound"%s)
364 |
365 | class invalid_command(Command):
366 | def do(self):
367 | s = self.event_name
368 | self.reader.error("command `%s' not known"%s)
369 |
370 | class qIHelp(Command):
371 | def do(self):
372 | from .reader import disp_str
373 |
374 | r = self.reader
375 | pending = r.console.getpending().data
376 | disp = disp_str((self.event + pending))[0]
377 | r.insert(disp * r.get_arg())
378 | r.pop_input_trans()
379 |
380 | from pyrepl import input
381 |
382 | class QITrans(object):
383 | def push(self, evt):
384 | self.evt = evt
385 | def get(self):
386 | return ('qIHelp', self.evt.data)
387 |
388 | class quoted_insert(Command):
389 | kills_digit_arg = 0
390 | def do(self):
391 | self.reader.push_input_trans(QITrans())
392 |
--------------------------------------------------------------------------------
/pyrepl/completer.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | try:
21 | import __builtin__ as builtins
22 | builtins # silence broken pyflakes
23 | except ImportError:
24 | import builtins
25 |
26 |
27 | class Completer(object):
28 | def __init__(self, ns):
29 | self.ns = ns
30 |
31 | def complete(self, text):
32 | if "." in text:
33 | return self.attr_matches(text)
34 | else:
35 | return self.global_matches(text)
36 |
37 | def global_matches(self, text):
38 | """Compute matches when text is a simple name.
39 |
40 | Return a list of all keywords, built-in functions and names
41 | currently defines in __main__ that match.
42 |
43 | """
44 | import keyword
45 | matches = []
46 | for list in [keyword.kwlist,
47 | builtins.__dict__.keys(),
48 | self.ns.keys()]:
49 | for word in list:
50 | if word.startswith(text) and word != "__builtins__":
51 | matches.append(word)
52 | return matches
53 |
54 | def attr_matches(self, text):
55 | """Compute matches when text contains a dot.
56 |
57 | Assuming the text is of the form NAME.NAME....[NAME], and is
58 | evaluatable in the globals of __main__, it will be evaluated
59 | and its attributes (as revealed by dir()) are used as possible
60 | completions. (For class instances, class members are are also
61 | considered.)
62 |
63 | WARNING: this can still invoke arbitrary C code, if an object
64 | with a __getattr__ hook is evaluated.
65 |
66 | """
67 | import re
68 | m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text)
69 | if not m:
70 | return []
71 | expr, attr = m.group(1, 3)
72 | object = eval(expr, self.ns)
73 | words = dir(object)
74 | if hasattr(object, '__class__'):
75 | words.append('__class__')
76 | words = words + get_class_members(object.__class__)
77 | matches = []
78 | n = len(attr)
79 | for word in words:
80 | if word[:n] == attr and word != "__builtins__":
81 | matches.append("%s.%s" % (expr, word))
82 | return matches
83 |
84 |
85 | def get_class_members(klass):
86 | ret = dir(klass)
87 | if hasattr(klass, '__bases__'):
88 | for base in klass.__bases__:
89 | ret = ret + get_class_members(base)
90 | return ret
91 |
--------------------------------------------------------------------------------
/pyrepl/completing_reader.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2010 Michael Hudson-Doyle
2 | # Antonio Cuni
3 | #
4 | # All Rights Reserved
5 | #
6 | #
7 | # Permission to use, copy, modify, and distribute this software and
8 | # its documentation for any purpose is hereby granted without fee,
9 | # provided that the above copyright notice appear in all copies and
10 | # that both that copyright notice and this permission notice appear in
11 | # supporting documentation.
12 | #
13 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 |
21 | import re
22 | from pyrepl import commands, reader
23 | from pyrepl.reader import Reader
24 |
25 |
26 | def prefix(wordlist, j=0):
27 | d = {}
28 | i = j
29 | try:
30 | while 1:
31 | for word in wordlist:
32 | d[word[i]] = 1
33 | if len(d) > 1:
34 | return wordlist[0][j:i]
35 | i += 1
36 | d = {}
37 | except IndexError:
38 | return wordlist[0][j:i]
39 |
40 |
41 | STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]")
42 |
43 | def stripcolor(s):
44 | return STRIPCOLOR_REGEX.sub('', s)
45 |
46 |
47 | def real_len(s):
48 | return len(stripcolor(s))
49 |
50 |
51 | def left_align(s, maxlen):
52 | stripped = stripcolor(s)
53 | if len(stripped) > maxlen:
54 | # too bad, we remove the color
55 | return stripped[:maxlen]
56 | padding = maxlen - len(stripped)
57 | return s + ' '*padding
58 |
59 |
60 | def build_menu(cons, wordlist, start, use_brackets, sort_in_column):
61 | if use_brackets:
62 | item = "[ %s ]"
63 | padding = 4
64 | else:
65 | item = "%s "
66 | padding = 2
67 | maxlen = min(max(map(real_len, wordlist)), cons.width - padding)
68 | cols = int(cons.width / (maxlen + padding))
69 | rows = int((len(wordlist) - 1)/cols + 1)
70 |
71 | if sort_in_column:
72 | # sort_in_column=False (default) sort_in_column=True
73 | # A B C A D G
74 | # D E F B E
75 | # G C F
76 | #
77 | # "fill" the table with empty words, so we always have the same amout
78 | # of rows for each column
79 | missing = cols*rows - len(wordlist)
80 | wordlist = wordlist + ['']*missing
81 | indexes = [(i % cols) * rows + i // cols for i in range(len(wordlist))]
82 | wordlist = [wordlist[i] for i in indexes]
83 | menu = []
84 | i = start
85 | for r in range(rows):
86 | row = []
87 | for col in range(cols):
88 | row.append(item % left_align(wordlist[i], maxlen))
89 | i += 1
90 | if i >= len(wordlist):
91 | break
92 | menu.append(''.join(row))
93 | if i >= len(wordlist):
94 | i = 0
95 | break
96 | if r + 5 > cons.height:
97 | menu.append(" %d more... " % (len(wordlist) - i))
98 | break
99 | return menu, i
100 |
101 | # this gets somewhat user interface-y, and as a result the logic gets
102 | # very convoluted.
103 | #
104 | # To summarise the summary of the summary:- people are a problem.
105 | # -- The Hitch-Hikers Guide to the Galaxy, Episode 12
106 |
107 | #### Desired behaviour of the completions commands.
108 | # the considerations are:
109 | # (1) how many completions are possible
110 | # (2) whether the last command was a completion
111 | # (3) if we can assume that the completer is going to return the same set of
112 | # completions: this is controlled by the ``assume_immutable_completions``
113 | # variable on the reader, which is True by default to match the historical
114 | # behaviour of pyrepl, but e.g. False in the ReadlineAlikeReader to match
115 | # more closely readline's semantics (this is needed e.g. by
116 | # fancycompleter)
117 | #
118 | # if there's no possible completion, beep at the user and point this out.
119 | # this is easy.
120 | #
121 | # if there's only one possible completion, stick it in. if the last thing
122 | # user did was a completion, point out that he isn't getting anywhere, but
123 | # only if the ``assume_immutable_completions`` is True.
124 | #
125 | # now it gets complicated.
126 | #
127 | # for the first press of a completion key:
128 | # if there's a common prefix, stick it in.
129 |
130 | # irrespective of whether anything got stuck in, if the word is now
131 | # complete, show the "complete but not unique" message
132 |
133 | # if there's no common prefix and if the word is not now complete,
134 | # beep.
135 |
136 | # common prefix -> yes no
137 | # word complete \/
138 | # yes "cbnu" "cbnu"
139 | # no - beep
140 |
141 | # for the second bang on the completion key
142 | # there will necessarily be no common prefix
143 | # show a menu of the choices.
144 |
145 | # for subsequent bangs, rotate the menu around (if there are sufficient
146 | # choices).
147 |
148 |
149 | class complete(commands.Command):
150 | def do(self):
151 | r = self.reader
152 | last_is_completer = r.last_command_is(self.__class__)
153 | immutable_completions = r.assume_immutable_completions
154 | completions_unchangable = last_is_completer and immutable_completions
155 | stem = r.get_stem()
156 | if not completions_unchangable:
157 | r.cmpltn_menu_choices = r.get_completions(stem)
158 |
159 | completions = r.cmpltn_menu_choices
160 | if not completions:
161 | r.error("no matches")
162 | elif len(completions) == 1:
163 | if completions_unchangable and len(completions[0]) == len(stem):
164 | r.msg = "[ sole completion ]"
165 | r.dirty = 1
166 | r.insert(completions[0][len(stem):])
167 | else:
168 | p = prefix(completions, len(stem))
169 | if p:
170 | r.insert(p)
171 | if last_is_completer:
172 | if not r.cmpltn_menu_vis:
173 | r.cmpltn_menu_vis = 1
174 | r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
175 | r.console, completions, r.cmpltn_menu_end,
176 | r.use_brackets, r.sort_in_column)
177 | r.dirty = 1
178 | elif stem + p in completions:
179 | r.msg = "[ complete but not unique ]"
180 | r.dirty = 1
181 | else:
182 | r.msg = "[ not unique ]"
183 | r.dirty = 1
184 |
185 |
186 | class self_insert(commands.self_insert):
187 | def do(self):
188 | commands.self_insert.do(self)
189 | r = self.reader
190 | if r.cmpltn_menu_vis:
191 | stem = r.get_stem()
192 | if len(stem) < 1:
193 | r.cmpltn_reset()
194 | else:
195 | completions = [w for w in r.cmpltn_menu_choices
196 | if w.startswith(stem)]
197 | if completions:
198 | r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
199 | r.console, completions, 0,
200 | r.use_brackets, r.sort_in_column)
201 | else:
202 | r.cmpltn_reset()
203 |
204 |
205 | class CompletingReader(Reader):
206 | """Adds completion support
207 |
208 | Adds instance variables:
209 | * cmpltn_menu, cmpltn_menu_vis, cmpltn_menu_end, cmpltn_choices:
210 | *
211 | """
212 | # see the comment for the complete command
213 | assume_immutable_completions = True
214 | use_brackets = True # display completions inside []
215 | sort_in_column = False
216 |
217 | def collect_keymap(self):
218 | return super(CompletingReader, self).collect_keymap() + (
219 | (r'\t', 'complete'),)
220 |
221 | def __init__(self, console):
222 | super(CompletingReader, self).__init__(console)
223 | self.cmpltn_menu = ["[ menu 1 ]", "[ menu 2 ]"]
224 | self.cmpltn_menu_vis = 0
225 | self.cmpltn_menu_end = 0
226 | for c in (complete, self_insert):
227 | self.commands[c.__name__] = c
228 | self.commands[c.__name__.replace('_', '-')] = c
229 |
230 | def after_command(self, cmd):
231 | super(CompletingReader, self).after_command(cmd)
232 | if not isinstance(cmd, (complete, self_insert)):
233 | self.cmpltn_reset()
234 |
235 | def calc_screen(self):
236 | screen = super(CompletingReader, self).calc_screen()
237 | if self.cmpltn_menu_vis:
238 | ly = self.lxy[1]
239 | screen[ly:ly] = self.cmpltn_menu
240 | self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu)
241 | self.cxy = self.cxy[0], self.cxy[1] + len(self.cmpltn_menu)
242 | return screen
243 |
244 | def finish(self):
245 | super(CompletingReader, self).finish()
246 | self.cmpltn_reset()
247 |
248 | def cmpltn_reset(self):
249 | self.cmpltn_menu = []
250 | self.cmpltn_menu_vis = 0
251 | self.cmpltn_menu_end = 0
252 | self.cmpltn_menu_choices = []
253 |
254 | def get_stem(self):
255 | st = self.syntax_table
256 | SW = reader.SYNTAX_WORD
257 | b = self.buffer
258 | p = self.pos - 1
259 | while p >= 0 and st.get(b[p], SW) == SW:
260 | p -= 1
261 | return ''.join(b[p+1:self.pos])
262 |
263 | def get_completions(self, stem):
264 | return []
265 |
266 |
267 | def test():
268 | class TestReader(CompletingReader):
269 | def get_completions(self, stem):
270 | return [s for l in self.history
271 | for s in l.split()
272 | if s and s.startswith(stem)]
273 |
274 | reader = TestReader()
275 | reader.ps1 = "c**> "
276 | reader.ps2 = "c/*> "
277 | reader.ps3 = "c|*> "
278 | reader.ps4 = r"c\*> "
279 | while reader.readline():
280 | pass
281 |
282 |
283 | if __name__ == '__main__':
284 | test()
285 |
--------------------------------------------------------------------------------
/pyrepl/console.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 |
21 | class Event(object):
22 | """An Event. `evt' is 'key' or somesuch."""
23 | __slots__ = 'evt', 'data', 'raw'
24 |
25 | def __init__(self, evt, data, raw=''):
26 | self.evt = evt
27 | self.data = data
28 | self.raw = raw
29 |
30 | def __repr__(self):
31 | return 'Event(%r, %r)' % (self.evt, self.data)
32 |
33 | def __eq__(self, other):
34 | return (self.evt == other.evt and
35 | self.data == other.data and
36 | self.raw == other.raw)
37 |
38 | class Console(object):
39 | """Attributes:
40 |
41 | screen,
42 | height,
43 | width,
44 | """
45 |
46 | def refresh(self, screen, xy):
47 | pass
48 |
49 | def prepare(self):
50 | pass
51 |
52 | def restore(self):
53 | pass
54 |
55 | def move_cursor(self, x, y):
56 | pass
57 |
58 | def set_cursor_vis(self, vis):
59 | pass
60 |
61 | def getheightwidth(self):
62 | """Return (height, width) where height and width are the height
63 | and width of the terminal window in characters."""
64 | pass
65 |
66 | def get_event(self, block=1):
67 | """Return an Event instance. Returns None if |block| is false
68 | and there is no event pending, otherwise waits for the
69 | completion of an event."""
70 | pass
71 |
72 | def beep(self):
73 | pass
74 |
75 | def clear(self):
76 | """Wipe the screen"""
77 | pass
78 |
79 | def finish(self):
80 | """Move the cursor to the end of the display and otherwise get
81 | ready for end. XXX could be merged with restore? Hmm."""
82 | pass
83 |
84 | def flushoutput(self):
85 | """Flush all output to the screen (assuming there's some
86 | buffering going on somewhere)."""
87 | pass
88 |
89 | def forgetinput(self):
90 | """Forget all pending, but not yet processed input."""
91 | pass
92 |
93 | def getpending(self):
94 | """Return the characters that have been typed but not yet
95 | processed."""
96 | pass
97 |
98 | def wait(self):
99 | """Wait for an event."""
100 | pass
101 |
--------------------------------------------------------------------------------
/pyrepl/curses.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright 2000-2010 Michael Hudson-Doyle
3 | # Armin Rigo
4 | #
5 | # All Rights Reserved
6 | #
7 | #
8 | # Permission to use, copy, modify, and distribute this software and
9 | # its documentation for any purpose is hereby granted without fee,
10 | # provided that the above copyright notice appear in all copies and
11 | # that both that copyright notice and this permission notice appear in
12 | # supporting documentation.
13 | #
14 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
15 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
16 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
17 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
18 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
19 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
20 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 |
22 |
23 | from ._minimal_curses import setupterm, tigetstr, tparm, error
24 |
--------------------------------------------------------------------------------
/pyrepl/fancy_termios.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | import termios
21 |
22 | class TermState:
23 | def __init__(self, tuples):
24 | self.iflag, self.oflag, self.cflag, self.lflag, \
25 | self.ispeed, self.ospeed, self.cc = tuples
26 | def as_list(self):
27 | return [self.iflag, self.oflag, self.cflag, self.lflag,
28 | self.ispeed, self.ospeed, self.cc]
29 |
30 | def copy(self):
31 | return self.__class__(self.as_list())
32 |
33 | def tcgetattr(fd):
34 | return TermState(termios.tcgetattr(fd))
35 |
36 | def tcsetattr(fd, when, attrs):
37 | termios.tcsetattr(fd, when, attrs.as_list())
38 |
39 | class Term(TermState):
40 | TS__init__ = TermState.__init__
41 | def __init__(self, fd=0):
42 | self.TS__init__(termios.tcgetattr(fd))
43 | self.fd = fd
44 | self.stack = []
45 | def save(self):
46 | self.stack.append( self.as_list() )
47 | def set(self, when=termios.TCSANOW):
48 | termios.tcsetattr(self.fd, when, self.as_list())
49 | def restore(self):
50 | self.TS__init__(self.stack.pop())
51 | self.set()
52 |
53 |
--------------------------------------------------------------------------------
/pyrepl/historical_reader.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | from pyrepl import reader, commands
21 | from pyrepl.reader import Reader as R
22 |
23 | isearch_keymap = tuple(
24 | [('\\%03o'%c, 'isearch-end') for c in range(256) if chr(c) != '\\'] + \
25 | [(c, 'isearch-add-character')
26 | for c in map(chr, range(32, 127)) if c != '\\'] + \
27 | [('\\%03o'%c, 'isearch-add-character')
28 | for c in range(256) if chr(c).isalpha() and chr(c) != '\\'] + \
29 | [('\\\\', 'self-insert'),
30 | (r'\C-r', 'isearch-backwards'),
31 | (r'\C-s', 'isearch-forwards'),
32 | (r'\C-c', 'isearch-cancel'),
33 | (r'\C-g', 'isearch-cancel'),
34 | (r'\', 'isearch-backspace')])
35 |
36 | if 'c' in globals():
37 | del c
38 |
39 | ISEARCH_DIRECTION_NONE = ''
40 | ISEARCH_DIRECTION_BACKWARDS = 'r'
41 | ISEARCH_DIRECTION_FORWARDS = 'f'
42 |
43 | class next_history(commands.Command):
44 | def do(self):
45 | r = self.reader
46 | if r.historyi == len(r.history):
47 | r.error("end of history list")
48 | return
49 | r.select_item(r.historyi + 1)
50 |
51 | class previous_history(commands.Command):
52 | def do(self):
53 | r = self.reader
54 | if r.historyi == 0:
55 | r.error("start of history list")
56 | return
57 | r.select_item(r.historyi - 1)
58 |
59 | class restore_history(commands.Command):
60 | def do(self):
61 | r = self.reader
62 | if r.historyi != len(r.history):
63 | if r.get_unicode() != r.history[r.historyi]:
64 | r.buffer = list(r.history[r.historyi])
65 | r.pos = len(r.buffer)
66 | r.dirty = 1
67 |
68 | class first_history(commands.Command):
69 | def do(self):
70 | self.reader.select_item(0)
71 |
72 | class last_history(commands.Command):
73 | def do(self):
74 | self.reader.select_item(len(self.reader.history))
75 |
76 | class operate_and_get_next(commands.FinishCommand):
77 | def do(self):
78 | self.reader.next_history = self.reader.historyi + 1
79 |
80 | class yank_arg(commands.Command):
81 | def do(self):
82 | r = self.reader
83 | if r.last_command is self.__class__:
84 | r.yank_arg_i += 1
85 | else:
86 | r.yank_arg_i = 0
87 | if r.historyi < r.yank_arg_i:
88 | r.error("beginning of history list")
89 | return
90 | a = r.get_arg(-1)
91 | # XXX how to split?
92 | words = r.get_item(r.historyi - r.yank_arg_i - 1).split()
93 | if a < -len(words) or a >= len(words):
94 | r.error("no such arg")
95 | return
96 | w = words[a]
97 | b = r.buffer
98 | if r.yank_arg_i > 0:
99 | o = len(r.yank_arg_yanked)
100 | else:
101 | o = 0
102 | b[r.pos - o:r.pos] = list(w)
103 | r.yank_arg_yanked = w
104 | r.pos += len(w) - o
105 | r.dirty = 1
106 |
107 | class forward_history_isearch(commands.Command):
108 | def do(self):
109 | r = self.reader
110 | r.isearch_direction = ISEARCH_DIRECTION_FORWARDS
111 | r.isearch_start = r.historyi, r.pos
112 | r.isearch_term = ''
113 | r.dirty = 1
114 | r.push_input_trans(r.isearch_trans)
115 |
116 |
117 | class reverse_history_isearch(commands.Command):
118 | def do(self):
119 | r = self.reader
120 | r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS
121 | r.dirty = 1
122 | r.isearch_term = ''
123 | r.push_input_trans(r.isearch_trans)
124 | r.isearch_start = r.historyi, r.pos
125 |
126 | class isearch_cancel(commands.Command):
127 | def do(self):
128 | r = self.reader
129 | r.isearch_direction = ISEARCH_DIRECTION_NONE
130 | r.pop_input_trans()
131 | r.select_item(r.isearch_start[0])
132 | r.pos = r.isearch_start[1]
133 | r.dirty = 1
134 |
135 | class isearch_add_character(commands.Command):
136 | def do(self):
137 | r = self.reader
138 | b = r.buffer
139 | r.isearch_term += self.event[-1]
140 | r.dirty = 1
141 | p = r.pos + len(r.isearch_term) - 1
142 | if b[p:p+1] != [r.isearch_term[-1]]:
143 | r.isearch_next()
144 |
145 | class isearch_backspace(commands.Command):
146 | def do(self):
147 | r = self.reader
148 | if len(r.isearch_term) > 0:
149 | r.isearch_term = r.isearch_term[:-1]
150 | r.dirty = 1
151 | else:
152 | r.error("nothing to rubout")
153 |
154 | class isearch_forwards(commands.Command):
155 | def do(self):
156 | r = self.reader
157 | r.isearch_direction = ISEARCH_DIRECTION_FORWARDS
158 | r.isearch_next()
159 |
160 | class isearch_backwards(commands.Command):
161 | def do(self):
162 | r = self.reader
163 | r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS
164 | r.isearch_next()
165 |
166 | class isearch_end(commands.Command):
167 | def do(self):
168 | r = self.reader
169 | r.isearch_direction = ISEARCH_DIRECTION_NONE
170 | r.console.forgetinput()
171 | r.pop_input_trans()
172 | r.dirty = 1
173 |
174 | class HistoricalReader(R):
175 | """Adds history support (with incremental history searching) to the
176 | Reader class.
177 |
178 | Adds the following instance variables:
179 | * history:
180 | a list of strings
181 | * historyi:
182 | * transient_history:
183 | * next_history:
184 | * isearch_direction, isearch_term, isearch_start:
185 | * yank_arg_i, yank_arg_yanked:
186 | used by the yank-arg command; not actually manipulated by any
187 | HistoricalReader instance methods.
188 | """
189 |
190 | def collect_keymap(self):
191 | return super(HistoricalReader, self).collect_keymap() + (
192 | (r'\C-n', 'next-history'),
193 | (r'\C-p', 'previous-history'),
194 | (r'\C-o', 'operate-and-get-next'),
195 | (r'\C-r', 'reverse-history-isearch'),
196 | (r'\C-s', 'forward-history-isearch'),
197 | (r'\M-r', 'restore-history'),
198 | (r'\M-.', 'yank-arg'),
199 | (r'\', 'last-history'),
200 | (r'\', 'first-history'))
201 |
202 |
203 | def __init__(self, console):
204 | super(HistoricalReader, self).__init__(console)
205 | self.history = []
206 | self.historyi = 0
207 | self.transient_history = {}
208 | self.next_history = None
209 | self.isearch_direction = ISEARCH_DIRECTION_NONE
210 | for c in [next_history, previous_history, restore_history,
211 | first_history, last_history, yank_arg,
212 | forward_history_isearch, reverse_history_isearch,
213 | isearch_end, isearch_add_character, isearch_cancel,
214 | isearch_add_character, isearch_backspace,
215 | isearch_forwards, isearch_backwards, operate_and_get_next]:
216 | self.commands[c.__name__] = c
217 | self.commands[c.__name__.replace('_', '-')] = c
218 | from pyrepl import input
219 | self.isearch_trans = input.KeymapTranslator(
220 | isearch_keymap, invalid_cls=isearch_end,
221 | character_cls=isearch_add_character)
222 |
223 | def select_item(self, i):
224 | self.transient_history[self.historyi] = self.get_unicode()
225 | buf = self.transient_history.get(i)
226 | if buf is None:
227 | buf = self.history[i]
228 | self.buffer = list(buf)
229 | self.historyi = i
230 | self.pos = len(self.buffer)
231 | self.dirty = 1
232 |
233 | def get_item(self, i):
234 | if i != len(self.history):
235 | return self.transient_history.get(i, self.history[i])
236 | else:
237 | return self.transient_history.get(i, self.get_unicode())
238 |
239 | def prepare(self):
240 | super(HistoricalReader, self).prepare()
241 | try:
242 | self.transient_history = {}
243 | if self.next_history is not None \
244 | and self.next_history < len(self.history):
245 | self.historyi = self.next_history
246 | self.buffer[:] = list(self.history[self.next_history])
247 | self.pos = len(self.buffer)
248 | self.transient_history[len(self.history)] = ''
249 | else:
250 | self.historyi = len(self.history)
251 | self.next_history = None
252 | except:
253 | self.restore()
254 | raise
255 |
256 | def get_prompt(self, lineno, cursor_on_line):
257 | if cursor_on_line and self.isearch_direction != ISEARCH_DIRECTION_NONE:
258 | d = 'rf'[self.isearch_direction == ISEARCH_DIRECTION_FORWARDS]
259 | return "(%s-search `%s') "%(d, self.isearch_term)
260 | else:
261 | return super(HistoricalReader, self).get_prompt(lineno, cursor_on_line)
262 |
263 | def isearch_next(self):
264 | st = self.isearch_term
265 | p = self.pos
266 | i = self.historyi
267 | s = self.get_unicode()
268 | forwards = self.isearch_direction == ISEARCH_DIRECTION_FORWARDS
269 | while 1:
270 | if forwards:
271 | p = s.find(st, p + 1)
272 | else:
273 | p = s.rfind(st, 0, p + len(st) - 1)
274 | if p != -1:
275 | self.select_item(i)
276 | self.pos = p
277 | return
278 | elif ((forwards and i == len(self.history) - 1)
279 | or (not forwards and i == 0)):
280 | self.error("not found")
281 | return
282 | else:
283 | if forwards:
284 | i += 1
285 | s = self.get_item(i)
286 | p = -1
287 | else:
288 | i -= 1
289 | s = self.get_item(i)
290 | p = len(s)
291 |
292 | def finish(self):
293 | super(HistoricalReader, self).finish()
294 | ret = self.get_unicode()
295 | for i, t in self.transient_history.items():
296 | if i < len(self.history) and i != self.historyi:
297 | self.history[i] = t
298 | if ret:
299 | self.history.append(ret)
300 |
301 | def test():
302 | from pyrepl.unix_console import UnixConsole
303 | reader = HistoricalReader(UnixConsole())
304 | reader.ps1 = "h**> "
305 | reader.ps2 = "h/*> "
306 | reader.ps3 = "h|*> "
307 | reader.ps4 = r"h\*> "
308 | while reader.readline():
309 | pass
310 |
311 | if __name__=='__main__':
312 | test()
313 |
--------------------------------------------------------------------------------
/pyrepl/input.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | # (naming modules after builtin functions is not such a hot idea...)
21 |
22 | # an KeyTrans instance translates Event objects into Command objects
23 |
24 | # hmm, at what level do we want [C-i] and [tab] to be equivalent?
25 | # [meta-a] and [esc a]? obviously, these are going to be equivalent
26 | # for the UnixConsole, but should they be for PygameConsole?
27 |
28 | # it would in any situation seem to be a bad idea to bind, say, [tab]
29 | # and [C-i] to *different* things... but should binding one bind the
30 | # other?
31 |
32 | # executive, temporary decision: [tab] and [C-i] are distinct, but
33 | # [meta-key] is identified with [esc key]. We demand that any console
34 | # class does quite a lot towards emulating a unix terminal.
35 | from __future__ import print_function
36 | import unicodedata
37 | from collections import deque
38 | import pprint
39 | from .trace import trace
40 |
41 |
42 | class InputTranslator(object):
43 | def push(self, evt):
44 | pass
45 | def get(self):
46 | pass
47 | def empty(self):
48 | pass
49 |
50 |
51 | class KeymapTranslator(InputTranslator):
52 |
53 | def __init__(self, keymap, verbose=0,
54 | invalid_cls=None, character_cls=None):
55 | self.verbose = verbose
56 | from pyrepl.keymap import compile_keymap, parse_keys
57 | self.keymap = keymap
58 | self.invalid_cls = invalid_cls
59 | self.character_cls = character_cls
60 | d = {}
61 | for keyspec, command in keymap:
62 | keyseq = tuple(parse_keys(keyspec))
63 | d[keyseq] = command
64 | if verbose:
65 | trace('[input] keymap: {}', pprint.pformat(d))
66 | self.k = self.ck = compile_keymap(d, ())
67 | self.results = deque()
68 | self.stack = []
69 |
70 | def push(self, evt):
71 | trace("[input] pushed {!r}", evt.data)
72 | key = evt.data
73 | d = self.k.get(key)
74 | if isinstance(d, dict):
75 | trace("[input] transition")
76 | self.stack.append(key)
77 | self.k = d
78 | else:
79 | if d is None:
80 | trace("[input] invalid")
81 | if self.stack or len(key) > 1 or unicodedata.category(key) == 'C':
82 | self.results.append(
83 | (self.invalid_cls, self.stack + [key]))
84 | else:
85 | # small optimization:
86 | self.k[key] = self.character_cls
87 | self.results.append(
88 | (self.character_cls, [key]))
89 | else:
90 | trace("[input] matched {}", d)
91 | self.results.append((d, self.stack + [key]))
92 | self.stack = []
93 | self.k = self.ck
94 |
95 | def get(self):
96 | if self.results:
97 | return self.results.popleft()
98 | else:
99 | return None
100 |
101 | def empty(self):
102 | return not self.results
103 |
--------------------------------------------------------------------------------
/pyrepl/keymap.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2008 Michael Hudson-Doyle
2 | # Armin Rigo
3 | #
4 | # All Rights Reserved
5 | #
6 | #
7 | # Permission to use, copy, modify, and distribute this software and
8 | # its documentation for any purpose is hereby granted without fee,
9 | # provided that the above copyright notice appear in all copies and
10 | # that both that copyright notice and this permission notice appear in
11 | # supporting documentation.
12 | #
13 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 |
21 | """
22 | functions for parsing keyspecs
23 |
24 | Support for turning keyspecs into appropriate sequences.
25 |
26 | pyrepl uses it's own bastardized keyspec format, which is meant to be
27 | a strict superset of readline's \"KEYSEQ\" format (which is to say
28 | that if you can come up with a spec readline accepts that this
29 | doesn't, you've found a bug and should tell me about it).
30 |
31 | Note that this is the `\\C-o' style of readline keyspec, not the
32 | `Control-o' sort.
33 |
34 | A keyspec is a string representing a sequence of keypresses that can
35 | be bound to a command.
36 |
37 | All characters other than the backslash represent themselves. In the
38 | traditional manner, a backslash introduces a escape sequence.
39 |
40 | The extension to readline is that the sequence \\ denotes the
41 | sequence of charaters produced by hitting KEY.
42 |
43 | Examples:
44 |
45 | `a' - what you get when you hit the `a' key
46 | `\\EOA' - Escape - O - A (up, on my terminal)
47 | `\\' - the up arrow key
48 | `\\' - ditto (keynames are case insensitive)
49 | `\\C-o', `\\c-o' - control-o
50 | `\\M-.' - meta-period
51 | `\\E.' - ditto (that's how meta works for pyrepl)
52 | `\\', `\\', `\\t', `\\011', '\\x09', '\\X09', '\\C-i', '\\C-I'
53 | - all of these are the tab character. Can you think of any more?
54 | """
55 |
56 | _escapes = {
57 | '\\':'\\',
58 | "'":"'",
59 | '"':'"',
60 | 'a':'\a',
61 | 'b':r'\h',
62 | 'e':'\033',
63 | 'f':'\f',
64 | 'n':'\n',
65 | 'r':'\r',
66 | 't':'\t',
67 | 'v':'\v'
68 | }
69 |
70 | _keynames = {
71 | 'backspace': 'backspace',
72 | 'delete': 'delete',
73 | 'down': 'down',
74 | 'end': 'end',
75 | 'enter': '\r',
76 | 'escape': '\033',
77 | 'f1' : 'f1', 'f2' : 'f2', 'f3' : 'f3', 'f4' : 'f4',
78 | 'f5' : 'f5', 'f6' : 'f6', 'f7' : 'f7', 'f8' : 'f8',
79 | 'f9' : 'f9', 'f10': 'f10', 'f11': 'f11', 'f12': 'f12',
80 | 'f13': 'f13', 'f14': 'f14', 'f15': 'f15', 'f16': 'f16',
81 | 'f17': 'f17', 'f18': 'f18', 'f19': 'f19', 'f20': 'f20',
82 | 'home': 'home',
83 | 'insert': 'insert',
84 | 'left': 'left',
85 | 'page down': 'page down',
86 | 'page up': 'page up',
87 | 'return': '\r',
88 | 'right': 'right',
89 | 'space': ' ',
90 | 'tab': '\t',
91 | 'up': 'up',
92 | 'ctrl left': 'ctrl left',
93 | 'ctrl right': 'ctrl right',
94 | }
95 |
96 | class KeySpecError(Exception):
97 | pass
98 |
99 | def _parse_key1(key, s):
100 | ctrl = 0
101 | meta = 0
102 | ret = ''
103 | while not ret and s < len(key):
104 | if key[s] == '\\':
105 | c = key[s+1].lower()
106 | if c in _escapes:
107 | ret = _escapes[c]
108 | s += 2
109 | elif c == "c":
110 | if key[s + 2] != '-':
111 | raise KeySpecError(
112 | "\\C must be followed by `-' (char %d of %s)"%(
113 | s + 2, repr(key)))
114 | if ctrl:
115 | raise KeySpecError("doubled \\C- (char %d of %s)"%(
116 | s + 1, repr(key)))
117 | ctrl = 1
118 | s += 3
119 | elif c == "m":
120 | if key[s + 2] != '-':
121 | raise KeySpecError(
122 | "\\M must be followed by `-' (char %d of %s)"%(
123 | s + 2, repr(key)))
124 | if meta:
125 | raise KeySpecError("doubled \\M- (char %d of %s)"%(
126 | s + 1, repr(key)))
127 | meta = 1
128 | s += 3
129 | elif c.isdigit():
130 | n = key[s+1:s+4]
131 | ret = chr(int(n, 8))
132 | s += 4
133 | elif c == 'x':
134 | n = key[s+2:s+4]
135 | ret = chr(int(n, 16))
136 | s += 4
137 | elif c == '<':
138 | t = key.find('>', s)
139 | if t == -1:
140 | raise KeySpecError(
141 | "unterminated \\< starting at char %d of %s"%(
142 | s + 1, repr(key)))
143 | ret = key[s+2:t].lower()
144 | if ret not in _keynames:
145 | raise KeySpecError(
146 | "unrecognised keyname `%s' at char %d of %s"%(
147 | ret, s + 2, repr(key)))
148 | ret = _keynames[ret]
149 | s = t + 1
150 | else:
151 | raise KeySpecError(
152 | "unknown backslash escape %s at char %d of %s"%(
153 | repr(c), s + 2, repr(key)))
154 | else:
155 | ret = key[s]
156 | s += 1
157 | if ctrl:
158 | if len(ret) > 1:
159 | raise KeySpecError("\\C- must be followed by a character")
160 | ret = chr(ord(ret) & 0x1f) # curses.ascii.ctrl()
161 | if meta:
162 | ret = ['\033', ret]
163 | else:
164 | ret = [ret]
165 | return ret, s
166 |
167 | def parse_keys(key):
168 | s = 0
169 | r = []
170 | while s < len(key):
171 | k, s = _parse_key1(key, s)
172 | r.extend(k)
173 | return r
174 |
175 | def compile_keymap(keymap, empty=b''):
176 | r = {}
177 | for key, value in keymap.items():
178 | if isinstance(key, bytes):
179 | first = key[:1]
180 | else:
181 | first = key[0]
182 | r.setdefault(first, {})[key[1:]] = value
183 | for key, value in r.items():
184 | if empty in value:
185 | if len(value) != 1:
186 | raise KeySpecError(
187 | "key definitions for %s clash"%(value.values(),))
188 | else:
189 | r[key] = value[empty]
190 | else:
191 | r[key] = compile_keymap(value, empty)
192 | return r
193 |
--------------------------------------------------------------------------------
/pyrepl/keymaps.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | reader_emacs_keymap = tuple(
21 | [(r'\C-a', 'beginning-of-line'),
22 | (r'\C-b', 'left'),
23 | (r'\C-c', 'interrupt'),
24 | (r'\C-d', 'delete'),
25 | (r'\C-e', 'end-of-line'),
26 | (r'\C-f', 'right'),
27 | (r'\C-g', 'cancel'),
28 | (r'\C-h', 'backspace'),
29 | (r'\C-j', 'self-insert'),
30 | (r'\', 'accept'),
31 | (r'\C-k', 'kill-line'),
32 | (r'\C-l', 'clear-screen'),
33 | # (r'\C-m', 'accept'),
34 | (r'\C-q', 'quoted-insert'),
35 | (r'\C-t', 'transpose-characters'),
36 | (r'\C-u', 'unix-line-discard'),
37 | (r'\C-v', 'quoted-insert'),
38 | (r'\C-w', 'unix-word-rubout'),
39 | (r'\C-x\C-u', 'upcase-region'),
40 | (r'\C-y', 'yank'),
41 | (r'\C-z', 'suspend'),
42 |
43 | (r'\M-b', 'backward-word'),
44 | (r'\M-c', 'capitalize-word'),
45 | (r'\M-d', 'kill-word'),
46 | (r'\M-f', 'forward-word'),
47 | (r'\M-l', 'downcase-word'),
48 | (r'\M-t', 'transpose-words'),
49 | (r'\M-u', 'upcase-word'),
50 | (r'\M-y', 'yank-pop'),
51 | (r'\M--', 'digit-arg'),
52 | (r'\M-0', 'digit-arg'),
53 | (r'\M-1', 'digit-arg'),
54 | (r'\M-2', 'digit-arg'),
55 | (r'\M-3', 'digit-arg'),
56 | (r'\M-4', 'digit-arg'),
57 | (r'\M-5', 'digit-arg'),
58 | (r'\M-6', 'digit-arg'),
59 | (r'\M-7', 'digit-arg'),
60 | (r'\M-8', 'digit-arg'),
61 | (r'\M-9', 'digit-arg'),
62 | (r'\M-\n', 'self-insert'),
63 | (r'\', 'self-insert')] + \
64 | [(c, 'self-insert')
65 | for c in map(chr, range(32, 127)) if c <> '\\'] + \
66 | [(c, 'self-insert')
67 | for c in map(chr, range(128, 256)) if c.isalpha()] + \
68 | [(r'\', 'up'),
69 | (r'\', 'down'),
70 | (r'\', 'left'),
71 | (r'\', 'right'),
72 | (r'\', 'quoted-insert'),
73 | (r'\', 'delete'),
74 | (r'\', 'backspace'),
75 | (r'\M-\', 'backward-kill-word'),
76 | (r'\', 'end'),
77 | (r'\', 'home'),
78 | (r'\', 'help'),
79 | (r'\EOF', 'end'), # the entries in the terminfo database for xterms
80 | (r'\EOH', 'home'), # seem to be wrong. this is a less than ideal
81 | # workaround
82 | ])
83 |
84 | hist_emacs_keymap = reader_emacs_keymap + (
85 | (r'\C-n', 'next-history'),
86 | (r'\C-p', 'previous-history'),
87 | (r'\C-o', 'operate-and-get-next'),
88 | (r'\C-r', 'reverse-history-isearch'),
89 | (r'\C-s', 'forward-history-isearch'),
90 | (r'\M-r', 'restore-history'),
91 | (r'\M-.', 'yank-arg'),
92 | (r'\', 'last-history'),
93 | (r'\', 'first-history'))
94 |
95 | comp_emacs_keymap = hist_emacs_keymap + (
96 | (r'\t', 'complete'),)
97 |
98 | python_emacs_keymap = comp_emacs_keymap + (
99 | (r'\n', 'maybe-accept'),
100 | (r'\M-\n', 'self-insert'))
101 |
102 | reader_vi_insert_keymap = tuple(
103 | [(c, 'self-insert')
104 | for c in map(chr, range(32, 127)) if c <> '\\'] + \
105 | [(c, 'self-insert')
106 | for c in map(chr, range(128, 256)) if c.isalpha()] + \
107 | [(r'\C-d', 'delete'),
108 | (r'\', 'backspace'),
109 | ('')])
110 |
111 | reader_vi_command_keymap = tuple(
112 | [
113 | ('E', 'enter-emacs-mode'),
114 | ('R', 'enter-replace-mode'),
115 | ('dw', 'delete-word'),
116 | ('dd', 'delete-line'),
117 |
118 | ('h', 'left'),
119 | ('i', 'enter-insert-mode'),
120 | ('j', 'down'),
121 | ('k', 'up'),
122 | ('l', 'right'),
123 | ('r', 'replace-char'),
124 | ('w', 'forward-word'),
125 | ('x', 'delete'),
126 | ('.', 'repeat-edit'), # argh!
127 | (r'\', 'enter-insert-mode'),
128 | ] +
129 | [(c, 'digit-arg') for c in '01234567689'] +
130 | [])
131 |
132 |
133 | reader_keymaps = {
134 | 'emacs' : reader_emacs_keymap,
135 | 'vi-insert' : reader_vi_insert_keymap,
136 | 'vi-command' : reader_vi_command_keymap
137 | }
138 |
139 | del c # from the listcomps
140 |
141 |
--------------------------------------------------------------------------------
/pyrepl/module_lister.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | import os, sys
21 |
22 | # for the completion support.
23 | # this is all quite nastily written.
24 | _packages = {}
25 |
26 | def _make_module_list_dir(dir, suffs, prefix=''):
27 | l = []
28 | for fname in os.listdir(dir):
29 | file = os.path.join(dir, fname)
30 | if os.path.isfile(file):
31 | for suff in suffs:
32 | if fname.endswith(suff):
33 | l.append( prefix + fname[:-len(suff)] )
34 | break
35 | elif os.path.isdir(file) \
36 | and os.path.exists(os.path.join(file, "__init__.py")):
37 | l.append( prefix + fname )
38 | _packages[prefix + fname] = _make_module_list_dir(
39 | file, suffs, prefix + fname + '.' )
40 | return sorted(set(l))
41 |
42 | def _make_module_list():
43 | import imp
44 | suffs = [x[0] for x in imp.get_suffixes() if x[0] != '.pyc']
45 | suffs.sort(reverse=True)
46 | _packages[''] = list(sys.builtin_module_names)
47 | for dir in sys.path:
48 | if dir == '':
49 | dir = '.'
50 | if os.path.isdir(dir):
51 | _packages[''] += _make_module_list_dir(dir, suffs)
52 | _packages[''].sort()
53 |
54 | def find_modules(stem):
55 | l = stem.split('.')
56 | pack = '.'.join(l[:-1])
57 | try:
58 | mods = _packages[pack]
59 | except KeyError:
60 | raise ImportError("can't find \"%s\" package" % pack)
61 | return [mod for mod in mods if mod.startswith(stem)]
62 |
--------------------------------------------------------------------------------
/pyrepl/pygame_console.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | # the pygame console is currently thoroughly broken.
21 |
22 | # there's a fundamental difference from the UnixConsole: here we're
23 | # the terminal emulator too, in effect. This means, e.g., for pythoni
24 | # we really need a separate process (or thread) to monitor for ^C
25 | # during command execution and zap the executor process. Making this
26 | # work on non-Unix is expected to be even more entertaining.
27 |
28 | from pygame.locals import *
29 | from pyrepl.console import Console, Event
30 | from pyrepl import pygame_keymap
31 | import pygame
32 | import types
33 |
34 | lmargin = 5
35 | rmargin = 5
36 | tmargin = 5
37 | bmargin = 5
38 |
39 | try:
40 | bool
41 | except NameError:
42 | def bool(x):
43 | return not not x
44 |
45 | modcolors = {K_LCTRL:1,
46 | K_RCTRL:1,
47 | K_LMETA:1,
48 | K_RMETA:1,
49 | K_LALT:1,
50 | K_RALT:1,
51 | K_LSHIFT:1,
52 | K_RSHIFT:1}
53 |
54 | class colors:
55 | fg = 250,240,230
56 | bg = 5, 5, 5
57 | cursor = 230, 0, 230
58 | margin = 5, 5, 15
59 |
60 | class FakeStdout:
61 | def __init__(self, con):
62 | self.con = con
63 | def write(self, text):
64 | self.con.write(text)
65 | def flush(self):
66 | pass
67 |
68 | class FakeStdin:
69 | def __init__(self, con):
70 | self.con = con
71 | def read(self, n=None):
72 | # argh!
73 | raise NotImplementedError
74 | def readline(self, n=None):
75 | from reader import Reader
76 | try:
77 | # this isn't quite right: it will clobber any prompt that's
78 | # been printed. Not sure how to get around this...
79 | return Reader(self.con).readline()
80 | except EOFError:
81 | return ''
82 |
83 | class PyGameConsole(Console):
84 | """Attributes:
85 |
86 | (keymap),
87 | (fd),
88 | screen,
89 | height,
90 | width,
91 | """
92 |
93 | def __init__(self):
94 | self.pygame_screen = pygame.display.set_mode((800, 600))
95 | pygame.font.init()
96 | pygame.key.set_repeat(500, 30)
97 | self.font = pygame.font.Font(
98 | "/usr/X11R6/lib/X11/fonts/TTF/luximr.ttf", 15)
99 | self.fw, self.fh = self.fontsize = self.font.size("X")
100 | self.cursor = pygame.Surface(self.fontsize)
101 | self.cursor.fill(colors.cursor)
102 | self.clear()
103 | self.curs_vis = 1
104 | self.height, self.width = self.getheightwidth()
105 | pygame.display.update()
106 | pygame.event.set_allowed(None)
107 | pygame.event.set_allowed(KEYDOWN)
108 |
109 | def install_keymap(self, keymap):
110 | """Install a given keymap.
111 |
112 | keymap is a tuple of 2-element tuples; each small tuple is a
113 | pair (keyspec, event-name). The format for keyspec is
114 | modelled on that used by readline (so read that manual for
115 | now!)."""
116 | self.k = self.keymap = pygame_keymap.compile_keymap(keymap)
117 |
118 | def char_rect(self, x, y):
119 | return self.char_pos(x, y), self.fontsize
120 |
121 | def char_pos(self, x, y):
122 | return (lmargin + x*self.fw,
123 | tmargin + y*self.fh + self.cur_top + self.scroll)
124 |
125 | def paint_margin(self):
126 | s = self.pygame_screen
127 | c = colors.margin
128 | s.fill(c, [0, 0, 800, tmargin])
129 | s.fill(c, [0, 0, lmargin, 600])
130 | s.fill(c, [0, 600 - bmargin, 800, bmargin])
131 | s.fill(c, [800 - rmargin, 0, lmargin, 600])
132 |
133 | def refresh(self, screen, (cx, cy)):
134 | self.screen = screen
135 | self.pygame_screen.fill(colors.bg,
136 | [0, tmargin + self.cur_top + self.scroll,
137 | 800, 600])
138 | self.paint_margin()
139 |
140 | line_top = self.cur_top
141 | width, height = self.fontsize
142 | self.cxy = (cx, cy)
143 | cp = self.char_pos(cx, cy)
144 | if cp[1] < tmargin:
145 | self.scroll = - (cy*self.fh + self.cur_top)
146 | self.repaint()
147 | elif cp[1] + self.fh > 600 - bmargin:
148 | self.scroll += (600 - bmargin) - (cp[1] + self.fh)
149 | self.repaint()
150 | if self.curs_vis:
151 | self.pygame_screen.blit(self.cursor, self.char_pos(cx, cy))
152 | for line in screen:
153 | if 0 <= line_top + self.scroll <= (600 - bmargin - tmargin - self.fh):
154 | if line:
155 | ren = self.font.render(line, 1, colors.fg)
156 | self.pygame_screen.blit(ren, (lmargin,
157 | tmargin + line_top + self.scroll))
158 | line_top += self.fh
159 | pygame.display.update()
160 |
161 | def prepare(self):
162 | self.cmd_buf = ''
163 | self.k = self.keymap
164 | self.height, self.width = self.getheightwidth()
165 | self.curs_vis = 1
166 | self.cur_top = self.pos[0]
167 | self.event_queue = []
168 |
169 | def restore(self):
170 | pass
171 |
172 | def blit_a_char(self, linen, charn):
173 | line = self.screen[linen]
174 | if charn < len(line):
175 | text = self.font.render(line[charn], 1, colors.fg)
176 | self.pygame_screen.blit(text, self.char_pos(charn, linen))
177 |
178 | def move_cursor(self, x, y):
179 | cp = self.char_pos(x, y)
180 | if cp[1] < tmargin or cp[1] + self.fh > 600 - bmargin:
181 | self.event_queue.append(Event('refresh', '', ''))
182 | else:
183 | if self.curs_vis:
184 | cx, cy = self.cxy
185 | self.pygame_screen.fill(colors.bg, self.char_rect(cx, cy))
186 | self.blit_a_char(cy, cx)
187 | self.pygame_screen.blit(self.cursor, cp)
188 | self.blit_a_char(y, x)
189 | pygame.display.update()
190 | self.cxy = (x, y)
191 |
192 | def set_cursor_vis(self, vis):
193 | self.curs_vis = vis
194 | if vis:
195 | self.move_cursor(*self.cxy)
196 | else:
197 | cx, cy = self.cxy
198 | self.pygame_screen.fill(colors.bg, self.char_rect(cx, cy))
199 | self.blit_a_char(cy, cx)
200 | pygame.display.update()
201 |
202 | def getheightwidth(self):
203 | """Return (height, width) where height and width are the height
204 | and width of the terminal window in characters."""
205 | return ((600 - tmargin - bmargin)/self.fh,
206 | (800 - lmargin - rmargin)/self.fw)
207 |
208 | def tr_event(self, pyg_event):
209 | shift = bool(pyg_event.mod & KMOD_SHIFT)
210 | ctrl = bool(pyg_event.mod & KMOD_CTRL)
211 | meta = bool(pyg_event.mod & (KMOD_ALT|KMOD_META))
212 |
213 | try:
214 | return self.k[(pyg_event.unicode, meta, ctrl)], pyg_event.unicode
215 | except KeyError:
216 | try:
217 | return self.k[(pyg_event.key, meta, ctrl)], pyg_event.unicode
218 | except KeyError:
219 | return "invalid-key", pyg_event.unicode
220 |
221 | def get_event(self, block=1):
222 | """Return an Event instance. Returns None if |block| is false
223 | and there is no event pending, otherwise waits for the
224 | completion of an event."""
225 | while 1:
226 | if self.event_queue:
227 | return self.event_queue.pop(0)
228 | elif block:
229 | pyg_event = pygame.event.wait()
230 | else:
231 | pyg_event = pygame.event.poll()
232 | if pyg_event.type == NOEVENT:
233 | return
234 |
235 | if pyg_event.key in modcolors:
236 | continue
237 |
238 | k, c = self.tr_event(pyg_event)
239 | self.cmd_buf += c.encode('ascii', 'replace')
240 | self.k = k
241 |
242 | if not isinstance(k, types.DictType):
243 | e = Event(k, self.cmd_buf, [])
244 | self.k = self.keymap
245 | self.cmd_buf = ''
246 | return e
247 |
248 | def beep(self):
249 | # uhh, can't be bothered now.
250 | # pygame.sound.something, I guess.
251 | pass
252 |
253 | def clear(self):
254 | """Wipe the screen"""
255 | self.pygame_screen.fill(colors.bg)
256 | #self.screen = []
257 | self.pos = [0, 0]
258 | self.grobs = []
259 | self.cur_top = 0
260 | self.scroll = 0
261 |
262 | def finish(self):
263 | """Move the cursor to the end of the display and otherwise get
264 | ready for end. XXX could be merged with restore? Hmm."""
265 | if self.curs_vis:
266 | cx, cy = self.cxy
267 | self.pygame_screen.fill(colors.bg, self.char_rect(cx, cy))
268 | self.blit_a_char(cy, cx)
269 | for line in self.screen:
270 | self.write_line(line, 1)
271 | if self.curs_vis:
272 | self.pygame_screen.blit(self.cursor,
273 | (lmargin + self.pos[1],
274 | tmargin + self.pos[0] + self.scroll))
275 | pygame.display.update()
276 |
277 | def flushoutput(self):
278 | """Flush all output to the screen (assuming there's some
279 | buffering going on somewhere)"""
280 | # no buffering here, ma'am (though perhaps there should be!)
281 | pass
282 |
283 | def forgetinput(self):
284 | """Forget all pending, but not yet processed input."""
285 | while pygame.event.poll().type <> NOEVENT:
286 | pass
287 |
288 | def getpending(self):
289 | """Return the characters that have been typed but not yet
290 | processed."""
291 | events = []
292 | while 1:
293 | event = pygame.event.poll()
294 | if event.type == NOEVENT:
295 | break
296 | events.append(event)
297 |
298 | return events
299 |
300 | def wait(self):
301 | """Wait for an event."""
302 | raise Exception, "erp!"
303 |
304 | def repaint(self):
305 | # perhaps we should consolidate grobs?
306 | self.pygame_screen.fill(colors.bg)
307 | self.paint_margin()
308 | for (y, x), surf, text in self.grobs:
309 | if surf and 0 < y + self.scroll:
310 | self.pygame_screen.blit(surf, (lmargin + x,
311 | tmargin + y + self.scroll))
312 | pygame.display.update()
313 |
314 | def write_line(self, line, ret):
315 | charsleft = (self.width*self.fw - self.pos[1])/self.fw
316 | while len(line) > charsleft:
317 | self.write_line(line[:charsleft], 1)
318 | line = line[charsleft:]
319 | if line:
320 | ren = self.font.render(line, 1, colors.fg, colors.bg)
321 | self.grobs.append((self.pos[:], ren, line))
322 | self.pygame_screen.blit(ren,
323 | (lmargin + self.pos[1],
324 | tmargin + self.pos[0] + self.scroll))
325 | else:
326 | self.grobs.append((self.pos[:], None, line))
327 | if ret:
328 | self.pos[0] += self.fh
329 | if tmargin + self.pos[0] + self.scroll + self.fh > 600 - bmargin:
330 | self.scroll = 600 - bmargin - self.pos[0] - self.fh - tmargin
331 | self.repaint()
332 | self.pos[1] = 0
333 | else:
334 | self.pos[1] += self.fw*len(line)
335 |
336 | def write(self, text):
337 | lines = text.split("\n")
338 | if self.curs_vis:
339 | self.pygame_screen.fill(colors.bg,
340 | (lmargin + self.pos[1],
341 | tmargin + self.pos[0] + self.scroll,
342 | self.fw, self.fh))
343 | for line in lines[:-1]:
344 | self.write_line(line, 1)
345 | self.write_line(lines[-1], 0)
346 | if self.curs_vis:
347 | self.pygame_screen.blit(self.cursor,
348 | (lmargin + self.pos[1],
349 | tmargin + self.pos[0] + self.scroll))
350 | pygame.display.update()
351 |
352 | def flush(self):
353 | pass
354 |
--------------------------------------------------------------------------------
/pyrepl/pygame_keymap.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2008 Michael Hudson-Doyle
2 | # Armin Rigo
3 | #
4 | # All Rights Reserved
5 | #
6 | #
7 | # Permission to use, copy, modify, and distribute this software and
8 | # its documentation for any purpose is hereby granted without fee,
9 | # provided that the above copyright notice appear in all copies and
10 | # that both that copyright notice and this permission notice appear in
11 | # supporting documentation.
12 | #
13 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 |
21 | # keyspec parsing for a pygame console. currently this is simply copy
22 | # n' change from the unix (ie. trad terminal) variant; probably some
23 | # refactoring will happen when I work out how it will work best.
24 |
25 | # A key is represented as *either*
26 |
27 | # a) a (keycode, meta, ctrl) sequence (used for special keys such as
28 | # f1, the up arrow key, etc)
29 | # b) a (unichar, meta, ctrl) sequence (used for printable chars)
30 |
31 | # Because we allow keystokes like '\\C-xu', I'll use the same trick as
32 | # the unix keymap module uses.
33 |
34 | # '\\C-a' --> (K_a, 0, 1)
35 |
36 | # XXX it's actually possible to test this module, so it should have a
37 | # XXX test suite.
38 |
39 | from pygame.locals import *
40 |
41 | _escapes = {
42 | '\\': K_BACKSLASH,
43 | "'" : K_QUOTE,
44 | '"' : K_QUOTEDBL,
45 | # 'a' : '\a',
46 | 'b' : K_BACKSLASH,
47 | 'e' : K_ESCAPE,
48 | # 'f' : '\f',
49 | 'n' : K_RETURN,
50 | 'r' : K_RETURN,
51 | 't' : K_TAB,
52 | # 'v' : '\v'
53 | }
54 |
55 | _keynames = {
56 | 'backspace' : K_BACKSPACE,
57 | 'delete' : K_DELETE,
58 | 'down' : K_DOWN,
59 | 'end' : K_END,
60 | 'enter' : K_KP_ENTER,
61 | 'escape' : K_ESCAPE,
62 | 'f1' : K_F1, 'f2' : K_F2, 'f3' : K_F3, 'f4' : K_F4,
63 | 'f5' : K_F5, 'f6' : K_F6, 'f7' : K_F7, 'f8' : K_F8,
64 | 'f9' : K_F9, 'f10': K_F10,'f11': K_F11,'f12': K_F12,
65 | 'f13': K_F13,'f14': K_F14,'f15': K_F15,
66 | 'home' : K_HOME,
67 | 'insert' : K_INSERT,
68 | 'left' : K_LEFT,
69 | 'pgdown' : K_PAGEDOWN, 'page down' : K_PAGEDOWN,
70 | 'pgup' : K_PAGEUP, 'page up' : K_PAGEUP,
71 | 'return' : K_RETURN,
72 | 'right' : K_RIGHT,
73 | 'space' : K_SPACE,
74 | 'tab' : K_TAB,
75 | 'up' : K_UP,
76 | }
77 |
78 | class KeySpecError(Exception):
79 | pass
80 |
81 | def _parse_key1(key, s):
82 | ctrl = 0
83 | meta = 0
84 | ret = ''
85 | while not ret and s < len(key):
86 | if key[s] == '\\':
87 | c = key[s+1].lower()
88 | if _escapes.has_key(c):
89 | ret = _escapes[c]
90 | s += 2
91 | elif c == "c":
92 | if key[s + 2] != '-':
93 | raise KeySpecError, \
94 | "\\C must be followed by `-' (char %d of %s)"%(
95 | s + 2, repr(key))
96 | if ctrl:
97 | raise KeySpecError, "doubled \\C- (char %d of %s)"%(
98 | s + 1, repr(key))
99 | ctrl = 1
100 | s += 3
101 | elif c == "m":
102 | if key[s + 2] != '-':
103 | raise KeySpecError, \
104 | "\\M must be followed by `-' (char %d of %s)"%(
105 | s + 2, repr(key))
106 | if meta:
107 | raise KeySpecError, "doubled \\M- (char %d of %s)"%(
108 | s + 1, repr(key))
109 | meta = 1
110 | s += 3
111 | elif c.isdigit():
112 | n = key[s+1:s+4]
113 | ret = chr(int(n, 8))
114 | s += 4
115 | elif c == 'x':
116 | n = key[s+2:s+4]
117 | ret = chr(int(n, 16))
118 | s += 4
119 | elif c == '<':
120 | t = key.find('>', s)
121 | if t == -1:
122 | raise KeySpecError, \
123 | "unterminated \\< starting at char %d of %s"%(
124 | s + 1, repr(key))
125 | try:
126 | ret = _keynames[key[s+2:t].lower()]
127 | s = t + 1
128 | except KeyError:
129 | raise KeySpecError, \
130 | "unrecognised keyname `%s' at char %d of %s"%(
131 | key[s+2:t], s + 2, repr(key))
132 | if ret is None:
133 | return None, s
134 | else:
135 | raise KeySpecError, \
136 | "unknown backslash escape %s at char %d of %s"%(
137 | `c`, s + 2, repr(key))
138 | else:
139 | if ctrl:
140 | ret = chr(ord(key[s]) & 0x1f) # curses.ascii.ctrl()
141 | ret = unicode(ret)
142 | else:
143 | ret = unicode(key[s])
144 | s += 1
145 | return (ret, meta, ctrl), s
146 |
147 | def parse_keys(key):
148 | s = 0
149 | r = []
150 | while s < len(key):
151 | k, s = _parse_key1(key, s)
152 | if k is None:
153 | return None
154 | r.append(k)
155 | return tuple(r)
156 |
157 | def _compile_keymap(keymap):
158 | r = {}
159 | for key, value in keymap.items():
160 | r.setdefault(key[0], {})[key[1:]] = value
161 | for key, value in r.items():
162 | if value.has_key(()):
163 | if len(value) <> 1:
164 | raise KeySpecError, \
165 | "key definitions for %s clash"%(value.values(),)
166 | else:
167 | r[key] = value[()]
168 | else:
169 | r[key] = _compile_keymap(value)
170 | return r
171 |
172 | def compile_keymap(keymap):
173 | r = {}
174 | for key, value in keymap:
175 | k = parse_keys(key)
176 | if value is None and r.has_key(k):
177 | del r[k]
178 | if k is not None:
179 | r[k] = value
180 | return _compile_keymap(r)
181 |
182 | def keyname(key):
183 | longest_match = ''
184 | longest_match_name = ''
185 | for name, keyseq in keyset.items():
186 | if keyseq and key.startswith(keyseq) and \
187 | len(keyseq) > len(longest_match):
188 | longest_match = keyseq
189 | longest_match_name = name
190 | if len(longest_match) > 0:
191 | return longest_match_name, len(longest_match)
192 | else:
193 | return None, 0
194 |
195 | _unescapes = {'\r':'\\r', '\n':'\\n', '\177':'^?'}
196 |
197 | #for k,v in _escapes.items():
198 | # _unescapes[v] = k
199 |
200 | def unparse_key(keyseq):
201 | if not keyseq:
202 | return ''
203 | name, s = keyname(keyseq)
204 | if name:
205 | if name <> 'escape' or s == len(keyseq):
206 | return '\\<' + name + '>' + unparse_key(keyseq[s:])
207 | else:
208 | return '\\M-' + unparse_key(keyseq[1:])
209 | else:
210 | c = keyseq[0]
211 | r = keyseq[1:]
212 | if c == '\\':
213 | p = '\\\\'
214 | elif _unescapes.has_key(c):
215 | p = _unescapes[c]
216 | elif ord(c) < ord(' '):
217 | p = '\\C-%s'%(chr(ord(c)+96),)
218 | elif ord(' ') <= ord(c) <= ord('~'):
219 | p = c
220 | else:
221 | p = '\\%03o'%(ord(c),)
222 | return p + unparse_key(r)
223 |
224 | def _unparse_keyf(keyseq):
225 | if not keyseq:
226 | return []
227 | name, s = keyname(keyseq)
228 | if name:
229 | if name <> 'escape' or s == len(keyseq):
230 | return [name] + _unparse_keyf(keyseq[s:])
231 | else:
232 | rest = _unparse_keyf(keyseq[1:])
233 | return ['M-'+rest[0]] + rest[1:]
234 | else:
235 | c = keyseq[0]
236 | r = keyseq[1:]
237 | if c == '\\':
238 | p = '\\'
239 | elif _unescapes.has_key(c):
240 | p = _unescapes[c]
241 | elif ord(c) < ord(' '):
242 | p = 'C-%s'%(chr(ord(c)+96),)
243 | elif ord(' ') <= ord(c) <= ord('~'):
244 | p = c
245 | else:
246 | p = '\\%03o'%(ord(c),)
247 | return [p] + _unparse_keyf(r)
248 |
249 | def unparse_keyf(keyseq):
250 | return " ".join(_unparse_keyf(keyseq))
251 |
--------------------------------------------------------------------------------
/pyrepl/python_reader.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2007 Michael Hudson-Doyle
2 | # Bob Ippolito
3 | # Maciek Fijalkowski
4 | #
5 | # All Rights Reserved
6 | #
7 | #
8 | # Permission to use, copy, modify, and distribute this software and
9 | # its documentation for any purpose is hereby granted without fee,
10 | # provided that the above copyright notice appear in all copies and
11 | # that both that copyright notice and this permission notice appear in
12 | # supporting documentation.
13 | #
14 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
15 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
16 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
17 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
18 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
19 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
20 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 |
22 | # one impressive collections of imports:
23 | from __future__ import print_function
24 | from __future__ import unicode_literals
25 | from pyrepl.completing_reader import CompletingReader
26 | from pyrepl.historical_reader import HistoricalReader
27 | from pyrepl import completing_reader, reader
28 | from pyrepl import commands, completer
29 | from pyrepl import module_lister
30 | import imp, sys, os, re, code, traceback
31 | import atexit, warnings
32 |
33 | try:
34 | unicode
35 | except:
36 | unicode = str
37 |
38 | try:
39 | imp.find_module("twisted")
40 | except ImportError:
41 | default_interactmethod = "interact"
42 | else:
43 | default_interactmethod = "twistedinteract"
44 |
45 | CommandCompiler = code.CommandCompiler
46 |
47 | def eat_it(*args):
48 | """this function eats warnings, if you were wondering"""
49 | pass
50 |
51 | if sys.version_info >= (3,0):
52 | def _reraise(cls, val, tb):
53 | __tracebackhide__ = True
54 | assert hasattr(val, '__traceback__')
55 | raise val
56 | else:
57 | exec ("""
58 | def _reraise(cls, val, tb):
59 | __tracebackhide__ = True
60 | raise cls, val, tb
61 | """)
62 |
63 |
64 |
65 |
66 | class maybe_accept(commands.Command):
67 | def do(self):
68 | r = self.reader
69 | text = r.get_unicode()
70 | try:
71 | # ooh, look at the hack:
72 | code = r.compiler(text)
73 | except (OverflowError, SyntaxError, ValueError):
74 | self.finish = 1
75 | else:
76 | if code is None:
77 | r.insert("\n")
78 | else:
79 | self.finish = 1
80 |
81 | from_line_prog = re.compile(
82 | "^from\s+(?P[A-Za-z_.0-9]*)\s+import\s+(?P[A-Za-z_.0-9]*)")
83 | import_line_prog = re.compile(
84 | "^(?:import|from)\s+(?P[A-Za-z_.0-9]*)\s*$")
85 |
86 | def saver(reader=reader):
87 | try:
88 | with open(os.path.expanduser("~/.pythoni.hist"), "wb") as fp:
89 | fp.write(b'\n'.join(item.encode('unicode_escape')
90 | for item in reader.history))
91 | except IOError as e:
92 | print(e)
93 | pass
94 |
95 | class PythonicReader(CompletingReader, HistoricalReader):
96 | def collect_keymap(self):
97 | return super(PythonicReader, self).collect_keymap() + (
98 | (r'\n', 'maybe-accept'),
99 | (r'\M-\n', 'insert-nl'))
100 |
101 | def __init__(self, console, locals,
102 | compiler=None):
103 | super(PythonicReader, self).__init__(console)
104 | self.completer = completer.Completer(locals)
105 | st = self.syntax_table
106 | for c in "._0123456789":
107 | st[c] = reader.SYNTAX_WORD
108 | self.locals = locals
109 | if compiler is None:
110 | self.compiler = CommandCompiler()
111 | else:
112 | self.compiler = compiler
113 | try:
114 | file = open(os.path.expanduser("~/.pythoni.hist"), 'rb')
115 | except IOError:
116 | self.history = []
117 | else:
118 | try:
119 | lines = file.readlines()
120 | self.history = [ x.rstrip(b'\n').decode('unicode_escape') for x in lines]
121 | except:
122 | self.history = []
123 | self.historyi = len(self.history)
124 | file.close()
125 | atexit.register(lambda: saver(self))
126 | for c in [maybe_accept]:
127 | self.commands[c.__name__] = c
128 | self.commands[c.__name__.replace('_', '-')] = c
129 |
130 | def get_completions(self, stem):
131 | b = self.get_unicode()
132 | m = import_line_prog.match(b)
133 | if m:
134 | if not self._module_list_ready:
135 | module_lister._make_module_list()
136 | self._module_list_ready = True
137 |
138 | mod = m.group("mod")
139 | try:
140 | return module_lister.find_modules(mod)
141 | except ImportError:
142 | pass
143 | m = from_line_prog.match(b)
144 | if m:
145 | mod, name = m.group("mod", "name")
146 | try:
147 | l = module_lister._packages[mod]
148 | except KeyError:
149 | try:
150 | mod = __import__(mod, self.locals, self.locals, [''])
151 | return [x for x in dir(mod) if x.startswith(name)]
152 | except ImportError:
153 | pass
154 | else:
155 | return [x[len(mod) + 1:]
156 | for x in l if x.startswith(mod + '.' + name)]
157 | try:
158 | l = sorted(set(self.completer.complete(stem)))
159 | return l
160 | except (NameError, AttributeError):
161 | return []
162 |
163 | class ReaderConsole(code.InteractiveInterpreter):
164 | II_init = code.InteractiveInterpreter.__init__
165 | def __init__(self, console, locals=None):
166 | if locals is None:
167 | locals = {}
168 | self.II_init(locals)
169 | self.compiler = CommandCompiler()
170 | self.compile = self.compiler.compiler
171 | self.reader = PythonicReader(console, locals, self.compiler)
172 | locals['Reader'] = self.reader
173 |
174 | def run_user_init_file(self):
175 | for key in "PYREPLSTARTUP", "PYTHONSTARTUP":
176 | initfile = os.environ.get(key)
177 | if initfile is not None and os.path.exists(initfile):
178 | break
179 | else:
180 | return
181 | try:
182 | with open(initfile, "r") as f:
183 | exec(compile(f.read(), initfile, "exec"), self.locals, self.locals)
184 | except:
185 | etype, value, tb = sys.exc_info()
186 | traceback.print_exception(etype, value, tb.tb_next)
187 |
188 | def execute(self, text):
189 | try:
190 | # ooh, look at the hack:
191 | code = self.compile(text, '', 'single')
192 | except (OverflowError, SyntaxError, ValueError):
193 | self.showsyntaxerror("")
194 | else:
195 | self.runcode(code)
196 | if sys.stdout and not sys.stdout.closed:
197 | sys.stdout.flush()
198 |
199 | def interact(self):
200 | while 1:
201 | try: # catches EOFError's and KeyboardInterrupts during execution
202 | try: # catches KeyboardInterrupts during editing
203 | try: # warning saver
204 | # can't have warnings spewed onto terminal
205 | sv = warnings.showwarning
206 | warnings.showwarning = eat_it
207 | l = unicode(self.reader.readline(), 'utf-8')
208 | finally:
209 | warnings.showwarning = sv
210 | except KeyboardInterrupt:
211 | print("KeyboardInterrupt")
212 | else:
213 | if l:
214 | self.execute(l)
215 | except EOFError:
216 | break
217 | except KeyboardInterrupt:
218 | continue
219 |
220 | def prepare(self):
221 | self.sv_sw = warnings.showwarning
222 | warnings.showwarning = eat_it
223 | self.reader.prepare()
224 | self.reader.refresh() # we want :after methods...
225 |
226 | def restore(self):
227 | self.reader.restore()
228 | warnings.showwarning = self.sv_sw
229 |
230 | def handle1(self, block=1):
231 | try:
232 | r = 1
233 | r = self.reader.handle1(block)
234 | except KeyboardInterrupt:
235 | self.restore()
236 | print("KeyboardInterrupt")
237 | self.prepare()
238 | else:
239 | if self.reader.finished:
240 | text = self.reader.get_unicode()
241 | self.restore()
242 | if text:
243 | self.execute(text)
244 | self.prepare()
245 | return r
246 |
247 | def tkfilehandler(self, file, mask):
248 | try:
249 | self.handle1(block=0)
250 | except:
251 | self.exc_info = sys.exc_info()
252 |
253 | # how the do you get this to work on Windows (without
254 | # createfilehandler)? threads, I guess
255 | def really_tkinteract(self):
256 | import _tkinter
257 | _tkinter.createfilehandler(
258 | self.reader.console.input_fd, _tkinter.READABLE,
259 | self.tkfilehandler)
260 |
261 | self.exc_info = None
262 | while 1:
263 | # dooneevent will return 0 without blocking if there are
264 | # no Tk windows, 1 after blocking until an event otherwise
265 | # so the following does what we want (this wasn't expected
266 | # to be obvious).
267 | if not _tkinter.dooneevent(_tkinter.ALL_EVENTS):
268 | self.handle1(block=1)
269 | if self.exc_info:
270 | type, value, tb = self.exc_info
271 | self.exc_info = None
272 | _reraise(type, value, tb)
273 |
274 | def tkinteract(self):
275 | """Run a Tk-aware Python interactive session.
276 |
277 | This function simulates the Python top-level in a way that
278 | allows Tk's mainloop to run."""
279 |
280 | # attempting to understand the control flow of this function
281 | # without help may cause internal injuries. so, some
282 | # explanation.
283 |
284 | # The outer while loop is there to restart the interaction if
285 | # the user types control-c when execution is deep in our
286 | # innards. I'm not sure this can't leave internals in an
287 | # inconsistent state, but it's a good start.
288 |
289 | # then the inside loop keeps calling self.handle1 until
290 | # _tkinter gets imported; then control shifts to
291 | # self.really_tkinteract, above.
292 |
293 | # this function can only return via an exception; we mask
294 | # EOFErrors (but they end the interaction) and
295 | # KeyboardInterrupts cause a restart. All other exceptions
296 | # are likely bugs in pyrepl (well, 'cept for SystemExit, of
297 | # course).
298 |
299 | while 1:
300 | try:
301 | try:
302 | self.prepare()
303 | try:
304 | while 1:
305 | if sys.modules.has_key("_tkinter"):
306 | self.really_tkinteract()
307 | # really_tkinteract is not expected to
308 | # return except via an exception, but:
309 | break
310 | self.handle1()
311 | except EOFError:
312 | pass
313 | finally:
314 | self.restore()
315 | except KeyboardInterrupt:
316 | continue
317 | else:
318 | break
319 |
320 | def twistedinteract(self):
321 | from twisted.internet import reactor
322 | from twisted.internet.abstract import FileDescriptor
323 | import signal
324 | outerself = self
325 | class Me(FileDescriptor):
326 | def fileno(self):
327 | """ We want to select on FD 0 """
328 | return 0
329 |
330 | def doRead(self):
331 | """called when input is ready"""
332 | try:
333 | outerself.handle1()
334 | except EOFError:
335 | reactor.stop()
336 |
337 | reactor.addReader(Me())
338 | reactor.callWhenRunning(signal.signal,
339 | signal.SIGINT,
340 | signal.default_int_handler)
341 | self.prepare()
342 | try:
343 | reactor.run()
344 | finally:
345 | self.restore()
346 |
347 |
348 | def cocoainteract(self, inputfilehandle=None, outputfilehandle=None):
349 | # only call this when there's a run loop already going!
350 | # note that unlike the other *interact methods, this returns immediately
351 | from cocoasupport import CocoaInteracter
352 | self.cocoainteracter = CocoaInteracter.alloc().init(self, inputfilehandle, outputfilehandle)
353 |
354 |
355 | def main(use_pygame_console=0, interactmethod=default_interactmethod, print_banner=True, clear_main=True):
356 | si, se, so = sys.stdin, sys.stderr, sys.stdout
357 | try:
358 | if 0 and use_pygame_console: # pygame currently borked
359 | from pyrepl.pygame_console import PyGameConsole, FakeStdin, FakeStdout
360 | con = PyGameConsole()
361 | sys.stderr = sys.stdout = FakeStdout(con)
362 | sys.stdin = FakeStdin(con)
363 | else:
364 | from pyrepl.unix_console import UnixConsole
365 | try:
366 | import locale
367 | except ImportError:
368 | encoding = None
369 | else:
370 | if hasattr(locale, 'nl_langinfo') \
371 | and hasattr(locale, 'CODESET'):
372 | encoding = locale.nl_langinfo(locale.CODESET)
373 | elif os.environ.get('TERM_PROGRAM') == 'Apple_Terminal':
374 | # /me whistles innocently...
375 | code = int(os.popen(
376 | "defaults read com.apple.Terminal StringEncoding"
377 | ).read())
378 | if code == 4:
379 | encoding = 'utf-8'
380 | # More could go here -- and what's here isn't
381 | # bulletproof. What would be? AppleScript?
382 | # Doesn't seem to be possible.
383 | else:
384 | encoding = None
385 | else:
386 | encoding = None # so you get ASCII...
387 | con = UnixConsole(os.dup(0), os.dup(1), None, encoding)
388 | if print_banner:
389 | print("Python", sys.version, "on", sys.platform)
390 | print('Type "help", "copyright", "credits" or "license" '\
391 | 'for more information.')
392 | sys.path.insert(0, os.getcwd())
393 |
394 | if clear_main and __name__ != '__main__':
395 | mainmod = imp.new_module('__main__')
396 | sys.modules['__main__'] = mainmod
397 | else:
398 | mainmod = sys.modules['__main__']
399 |
400 | rc = ReaderConsole(con, mainmod.__dict__)
401 | rc.reader._module_list_ready = False
402 | rc.run_user_init_file()
403 | getattr(rc, interactmethod)()
404 | finally:
405 | sys.stdin, sys.stderr, sys.stdout = si, se, so
406 |
407 | if __name__ == '__main__':
408 | main()
409 |
--------------------------------------------------------------------------------
/pyrepl/reader.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2010 Michael Hudson-Doyle
2 | # Antonio Cuni
3 | # Armin Rigo
4 | #
5 | # All Rights Reserved
6 | #
7 | #
8 | # Permission to use, copy, modify, and distribute this software and
9 | # its documentation for any purpose is hereby granted without fee,
10 | # provided that the above copyright notice appear in all copies and
11 | # that both that copyright notice and this permission notice appear in
12 | # supporting documentation.
13 | #
14 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
15 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
16 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
17 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
18 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
19 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
20 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 |
22 | from __future__ import unicode_literals
23 | import unicodedata
24 | from pyrepl import commands
25 | from pyrepl import input
26 | try:
27 | unicode
28 | except NameError:
29 | unicode = str
30 | unichr = chr
31 | basestring = bytes, str
32 |
33 |
34 | def _make_unctrl_map():
35 | uc_map = {}
36 | for c in map(unichr, range(256)):
37 | if unicodedata.category(c)[0] != 'C':
38 | uc_map[c] = c
39 | for i in range(32):
40 | c = unichr(i)
41 | uc_map[c] = '^' + unichr(ord('A') + i - 1)
42 | uc_map[b'\t'] = ' ' # display TABs as 4 characters
43 | uc_map[b'\177'] = unicode('^?')
44 | for i in range(256):
45 | c = unichr(i)
46 | if c not in uc_map:
47 | uc_map[c] = unicode('\\%03o') % i
48 | return uc_map
49 |
50 |
51 | def _my_unctrl(c, u=_make_unctrl_map()):
52 | if c in u:
53 | return u[c]
54 | else:
55 | if unicodedata.category(c).startswith('C'):
56 | return br'\u%04x' % ord(c)
57 | else:
58 | return c
59 |
60 |
61 | def disp_str(buffer, join=''.join, uc=_my_unctrl):
62 | """ disp_str(buffer:string) -> (string, [int])
63 |
64 | Return the string that should be the printed represenation of
65 | |buffer| and a list detailing where the characters of |buffer|
66 | get used up. E.g.:
67 |
68 | >>> disp_str(chr(3))
69 | ('^C', [1, 0])
70 |
71 | the list always contains 0s or 1s at present; it could conceivably
72 | go higher as and when unicode support happens."""
73 | # disp_str proved to be a bottleneck for large inputs,
74 | # so it needs to be rewritten in C; it's not required though.
75 | s = [uc(x) for x in buffer]
76 | b = [] # XXX: bytearray
77 | for x in s:
78 | b.append(1)
79 | b.extend([0] * (len(x) - 1))
80 | return join(s), b
81 |
82 | del _my_unctrl
83 |
84 | del _make_unctrl_map
85 |
86 | # syntax classes:
87 |
88 | [SYNTAX_WHITESPACE,
89 | SYNTAX_WORD,
90 | SYNTAX_SYMBOL] = range(3)
91 |
92 |
93 | def make_default_syntax_table():
94 | # XXX perhaps should use some unicodedata here?
95 | st = {}
96 | for c in map(unichr, range(256)):
97 | st[c] = SYNTAX_SYMBOL
98 | for c in [a for a in map(unichr, range(256)) if a.isalpha()]:
99 | st[c] = SYNTAX_WORD
100 | st[unicode('\n')] = st[unicode(' ')] = SYNTAX_WHITESPACE
101 | return st
102 |
103 | default_keymap = tuple(
104 | [(r'\C-a', 'beginning-of-line'),
105 | (r'\C-b', 'left'),
106 | (r'\C-c', 'interrupt'),
107 | (r'\C-d', 'delete'),
108 | (r'\C-e', 'end-of-line'),
109 | (r'\C-f', 'right'),
110 | (r'\C-g', 'cancel'),
111 | (r'\C-h', 'backspace'),
112 | (r'\C-j', 'accept'),
113 | (r'\', 'accept'),
114 | (r'\C-k', 'kill-line'),
115 | (r'\C-l', 'clear-screen'),
116 | (r'\C-m', 'accept'),
117 | (r'\C-q', 'quoted-insert'),
118 | (r'\C-t', 'transpose-characters'),
119 | (r'\C-u', 'unix-line-discard'),
120 | (r'\C-v', 'quoted-insert'),
121 | (r'\C-w', 'unix-word-rubout'),
122 | (r'\C-x\C-u', 'upcase-region'),
123 | (r'\C-y', 'yank'),
124 | (r'\C-z', 'suspend'),
125 |
126 | (r'\M-b', 'backward-word'),
127 | (r'\M-c', 'capitalize-word'),
128 | (r'\M-d', 'kill-word'),
129 | (r'\M-f', 'forward-word'),
130 | (r'\M-l', 'downcase-word'),
131 | (r'\M-t', 'transpose-words'),
132 | (r'\M-u', 'upcase-word'),
133 | (r'\M-y', 'yank-pop'),
134 | (r'\M--', 'digit-arg'),
135 | (r'\M-0', 'digit-arg'),
136 | (r'\M-1', 'digit-arg'),
137 | (r'\M-2', 'digit-arg'),
138 | (r'\M-3', 'digit-arg'),
139 | (r'\M-4', 'digit-arg'),
140 | (r'\M-5', 'digit-arg'),
141 | (r'\M-6', 'digit-arg'),
142 | (r'\M-7', 'digit-arg'),
143 | (r'\M-8', 'digit-arg'),
144 | (r'\M-9', 'digit-arg'),
145 | #(r'\M-\n', 'insert-nl'),
146 | ('\\\\', 'self-insert')] +
147 | [(c, 'self-insert')
148 | for c in map(chr, range(32, 127)) if c != '\\'] +
149 | [(c, 'self-insert')
150 | for c in map(chr, range(128, 256)) if c.isalpha()] +
151 | [(r'\', 'up'),
152 | (r'\', 'down'),
153 | (r'\', 'left'),
154 | (r'\', 'right'),
155 | (r'\', 'quoted-insert'),
156 | (r'\', 'delete'),
157 | (r'\', 'backspace'),
158 | (r'\M-\', 'backward-kill-word'),
159 | (r'\', 'end-of-line'), # was 'end'
160 | (r'\', 'beginning-of-line'), # was 'home'
161 | (r'\', 'help'),
162 | (r'\EOF', 'end'), # the entries in the terminfo database for xterms
163 | (r'\EOH', 'home'), # seem to be wrong. this is a less than ideal
164 | # workaround
165 | (r'\', 'backward-word'),
166 | (r'\', 'forward-word'),
167 | ])
168 |
169 | if 'c' in globals(): # only on python 2.x
170 | del c # from the listcomps
171 |
172 |
173 | class Reader(object):
174 | """The Reader class implements the bare bones of a command reader,
175 | handling such details as editing and cursor motion. What it does
176 | not support are such things as completion or history support -
177 | these are implemented elsewhere.
178 |
179 | Instance variables of note include:
180 |
181 | * buffer:
182 | A *list* (*not* a string at the moment :-) containing all the
183 | characters that have been entered.
184 | * console:
185 | Hopefully encapsulates the OS dependent stuff.
186 | * pos:
187 | A 0-based index into `buffer' for where the insertion point
188 | is.
189 | * screeninfo:
190 | Ahem. This list contains some info needed to move the
191 | insertion point around reasonably efficiently. I'd like to
192 | get rid of it, because its contents are obtuse (to put it
193 | mildly) but I haven't worked out if that is possible yet.
194 | * cxy, lxy:
195 | the position of the insertion point in screen ... XXX
196 | * syntax_table:
197 | Dictionary mapping characters to `syntax class'; read the
198 | emacs docs to see what this means :-)
199 | * commands:
200 | Dictionary mapping command names to command classes.
201 | * arg:
202 | The emacs-style prefix argument. It will be None if no such
203 | argument has been provided.
204 | * dirty:
205 | True if we need to refresh the display.
206 | * kill_ring:
207 | The emacs-style kill-ring; manipulated with yank & yank-pop
208 | * ps1, ps2, ps3, ps4:
209 | prompts. ps1 is the prompt for a one-line input; for a
210 | multiline input it looks like:
211 | ps2> first line of input goes here
212 | ps3> second and further
213 | ps3> lines get ps3
214 | ...
215 | ps4> and the last one gets ps4
216 | As with the usual top-level, you can set these to instances if
217 | you like; str() will be called on them (once) at the beginning
218 | of each command. Don't put really long or newline containing
219 | strings here, please!
220 | This is just the default policy; you can change it freely by
221 | overriding get_prompt() (and indeed some standard subclasses
222 | do).
223 | * finished:
224 | handle1 will set this to a true value if a command signals
225 | that we're done.
226 | """
227 |
228 | help_text = """\
229 | This is pyrepl. Hear my roar.
230 |
231 | Helpful text may appear here at some point in the future when I'm
232 | feeling more loquacious than I am now."""
233 |
234 | msg_at_bottom = True
235 |
236 | def __init__(self, console):
237 | self.buffer = []
238 | self.ps1 = "->> "
239 | self.ps2 = "/>> "
240 | self.ps3 = "|.. "
241 | self.ps4 = r"\__ "
242 | self.kill_ring = []
243 | self.arg = None
244 | self.finished = 0
245 | self.console = console
246 | self.commands = {}
247 | self.msg = ''
248 | for v in vars(commands).values():
249 | if (isinstance(v, type) and
250 | issubclass(v, commands.Command) and
251 | v.__name__[0].islower()):
252 | self.commands[v.__name__] = v
253 | self.commands[v.__name__.replace('_', '-')] = v
254 | self.syntax_table = make_default_syntax_table()
255 | self.input_trans_stack = []
256 | self.keymap = self.collect_keymap()
257 | self.input_trans = input.KeymapTranslator(
258 | self.keymap,
259 | invalid_cls='invalid-key',
260 | character_cls='self-insert')
261 |
262 | def collect_keymap(self):
263 | return default_keymap
264 |
265 | def calc_screen(self):
266 | """The purpose of this method is to translate changes in
267 | self.buffer into changes in self.screen. Currently it rips
268 | everything down and starts from scratch, which whilst not
269 | especially efficient is certainly simple(r).
270 | """
271 | lines = self.get_unicode().split("\n")
272 | screen = []
273 | screeninfo = []
274 | w = self.console.width - 1
275 | p = self.pos
276 | for ln, line in zip(range(len(lines)), lines):
277 | ll = len(line)
278 | if 0 <= p <= ll:
279 | if self.msg and not self.msg_at_bottom:
280 | for mline in self.msg.split("\n"):
281 | screen.append(mline)
282 | screeninfo.append((0, []))
283 | self.lxy = p, ln
284 | prompt = self.get_prompt(ln, ll >= p >= 0)
285 | while '\n' in prompt:
286 | pre_prompt, _, prompt = prompt.partition('\n')
287 | screen.append(pre_prompt)
288 | screeninfo.append((0, []))
289 | p -= ll + 1
290 | prompt, lp = self.process_prompt(prompt)
291 | l, l2 = disp_str(line)
292 | wrapcount = (len(l) + lp) // w
293 | if wrapcount == 0:
294 | screen.append(prompt + l)
295 | screeninfo.append((lp, l2 + [1]))
296 | else:
297 | screen.append(prompt + l[:w - lp] + "\\")
298 | screeninfo.append((lp, l2[:w - lp]))
299 | for i in range(-lp + w, -lp + wrapcount * w, w):
300 | screen.append(l[i:i + w] + "\\")
301 | screeninfo.append((0, l2[i:i + w]))
302 | screen.append(l[wrapcount * w - lp:])
303 | screeninfo.append((0, l2[wrapcount * w - lp:] + [1]))
304 | self.screeninfo = screeninfo
305 | self.cxy = self.pos2xy(self.pos)
306 | if self.msg and self.msg_at_bottom:
307 | for mline in self.msg.split("\n"):
308 | screen.append(mline)
309 | screeninfo.append((0, []))
310 | return screen
311 |
312 | def process_prompt(self, prompt):
313 | """ Process the prompt.
314 |
315 | This means calculate the length of the prompt. The character \x01
316 | and \x02 are used to bracket ANSI control sequences and need to be
317 | excluded from the length calculation. So also a copy of the prompt
318 | is returned with these control characters removed. """
319 |
320 | out_prompt = ''
321 | l = len(prompt)
322 | pos = 0
323 | while True:
324 | s = prompt.find('\x01', pos)
325 | if s == -1:
326 | break
327 | e = prompt.find('\x02', s)
328 | if e == -1:
329 | break
330 | # Found start and end brackets, subtract from string length
331 | l = l - (e - s + 1)
332 | out_prompt += prompt[pos:s] + prompt[s + 1:e]
333 | pos = e + 1
334 | out_prompt += prompt[pos:]
335 | return out_prompt, l
336 |
337 | def bow(self, p=None):
338 | """Return the 0-based index of the word break preceding p most
339 | immediately.
340 |
341 | p defaults to self.pos; word boundaries are determined using
342 | self.syntax_table."""
343 | if p is None:
344 | p = self.pos
345 | st = self.syntax_table
346 | b = self.buffer
347 | p -= 1
348 | while p >= 0 and st.get(b[p], SYNTAX_WORD) != SYNTAX_WORD:
349 | p -= 1
350 | while p >= 0 and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD:
351 | p -= 1
352 | return p + 1
353 |
354 | def eow(self, p=None):
355 | """Return the 0-based index of the word break following p most
356 | immediately.
357 |
358 | p defaults to self.pos; word boundaries are determined using
359 | self.syntax_table."""
360 | if p is None:
361 | p = self.pos
362 | st = self.syntax_table
363 | b = self.buffer
364 | while p < len(b) and st.get(b[p], SYNTAX_WORD) != SYNTAX_WORD:
365 | p += 1
366 | while p < len(b) and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD:
367 | p += 1
368 | return p
369 |
370 | def bol(self, p=None):
371 | """Return the 0-based index of the line break preceding p most
372 | immediately.
373 |
374 | p defaults to self.pos."""
375 | # XXX there are problems here.
376 | if p is None:
377 | p = self.pos
378 | b = self.buffer
379 | p -= 1
380 | while p >= 0 and b[p] != '\n':
381 | p -= 1
382 | return p + 1
383 |
384 | def eol(self, p=None):
385 | """Return the 0-based index of the line break following p most
386 | immediately.
387 |
388 | p defaults to self.pos."""
389 | if p is None:
390 | p = self.pos
391 | b = self.buffer
392 | while p < len(b) and b[p] != '\n':
393 | p += 1
394 | return p
395 |
396 | def get_arg(self, default=1):
397 | """Return any prefix argument that the user has supplied,
398 | returning `default' if there is None. `default' defaults
399 | (groan) to 1."""
400 | if self.arg is None:
401 | return default
402 | else:
403 | return self.arg
404 |
405 | def get_prompt(self, lineno, cursor_on_line):
406 | """Return what should be in the left-hand margin for line
407 | `lineno'."""
408 | if self.arg is not None and cursor_on_line:
409 | return "(arg: %s) " % self.arg
410 | if "\n" in self.buffer:
411 | if lineno == 0:
412 | res = self.ps2
413 | elif lineno == self.buffer.count("\n"):
414 | res = self.ps4
415 | else:
416 | res = self.ps3
417 | else:
418 | res = self.ps1
419 | # Lazily call str() on self.psN, and cache the results using as key
420 | # the object on which str() was called. This ensures that even if the
421 | # same object is used e.g. for ps1 and ps2, str() is called only once.
422 | if res not in self._pscache:
423 | self._pscache[res] = str(res)
424 | return self._pscache[res]
425 |
426 | def push_input_trans(self, itrans):
427 | self.input_trans_stack.append(self.input_trans)
428 | self.input_trans = itrans
429 |
430 | def pop_input_trans(self):
431 | self.input_trans = self.input_trans_stack.pop()
432 |
433 | def pos2xy(self, pos):
434 | """Return the x, y coordinates of position 'pos'."""
435 | # this *is* incomprehensible, yes.
436 | y = 0
437 | assert 0 <= pos <= len(self.buffer)
438 | if pos == len(self.buffer):
439 | y = len(self.screeninfo) - 1
440 | p, l2 = self.screeninfo[y]
441 | return p + len(l2) - 1, y
442 | else:
443 | for p, l2 in self.screeninfo:
444 | l = l2.count(1)
445 | if l > pos:
446 | break
447 | else:
448 | pos -= l
449 | y += 1
450 | c = 0
451 | i = 0
452 | while c < pos:
453 | c += l2[i]
454 | i += 1
455 | while l2[i] == 0:
456 | i += 1
457 | return p + i, y
458 |
459 | def insert(self, text):
460 | """Insert 'text' at the insertion point."""
461 | self.buffer[self.pos:self.pos] = list(text)
462 | self.pos += len(text)
463 | self.dirty = 1
464 |
465 | def update_cursor(self):
466 | """Move the cursor to reflect changes in self.pos"""
467 | self.cxy = self.pos2xy(self.pos)
468 | self.console.move_cursor(*self.cxy)
469 |
470 | def after_command(self, cmd):
471 | """This function is called to allow post command cleanup."""
472 | if getattr(cmd, "kills_digit_arg", 1):
473 | if self.arg is not None:
474 | self.dirty = 1
475 | self.arg = None
476 |
477 | def prepare(self):
478 | """Get ready to run. Call restore when finished. You must not
479 | write to the console in between the calls to prepare and
480 | restore."""
481 | try:
482 | self.console.prepare()
483 | self.arg = None
484 | self.screeninfo = []
485 | self.finished = 0
486 | del self.buffer[:]
487 | self.pos = 0
488 | self.dirty = 1
489 | self.last_command = None
490 | self._pscache = {}
491 | except:
492 | self.restore()
493 | raise
494 |
495 | def last_command_is(self, klass):
496 | if not self.last_command:
497 | return 0
498 | return issubclass(klass, self.last_command)
499 |
500 | def restore(self):
501 | """Clean up after a run."""
502 | self.console.restore()
503 |
504 | def finish(self):
505 | """Called when a command signals that we're finished."""
506 | pass
507 |
508 | def error(self, msg="none"):
509 | self.msg = "! " + msg + " "
510 | self.dirty = 1
511 | self.console.beep()
512 |
513 | def update_screen(self):
514 | if self.dirty:
515 | self.refresh()
516 |
517 | def refresh(self):
518 | """Recalculate and refresh the screen."""
519 | # this call sets up self.cxy, so call it first.
520 | screen = self.calc_screen()
521 | self.console.refresh(screen, self.cxy)
522 | self.dirty = 0 # forgot this for a while (blush)
523 |
524 | def do_cmd(self, cmd):
525 | #print cmd
526 | if isinstance(cmd[0], basestring):
527 | #XXX: unify to text
528 | cmd = self.commands.get(cmd[0],
529 | commands.invalid_command)(self, *cmd)
530 | elif isinstance(cmd[0], type):
531 | cmd = cmd[0](self, *cmd)
532 | else:
533 | return # nothing to do
534 |
535 | cmd.do()
536 |
537 | self.after_command(cmd)
538 |
539 | if self.dirty:
540 | self.refresh()
541 | else:
542 | self.update_cursor()
543 |
544 | if not isinstance(cmd, commands.digit_arg):
545 | self.last_command = cmd.__class__
546 |
547 | self.finished = cmd.finish
548 | if self.finished:
549 | self.console.finish()
550 | self.finish()
551 |
552 | def handle1(self, block=1):
553 | """Handle a single event. Wait as long as it takes if block
554 | is true (the default), otherwise return None if no event is
555 | pending."""
556 |
557 | if self.msg:
558 | self.msg = ''
559 | self.dirty = 1
560 |
561 | while 1:
562 | event = self.console.get_event(block)
563 | if not event: # can only happen if we're not blocking
564 | return None
565 |
566 | translate = True
567 |
568 | if event.evt == 'key':
569 | self.input_trans.push(event)
570 | elif event.evt == 'scroll':
571 | self.refresh()
572 | elif event.evt == 'resize':
573 | self.refresh()
574 | else:
575 | translate = False
576 |
577 | if translate:
578 | cmd = self.input_trans.get()
579 | else:
580 | cmd = event.evt, event.data
581 |
582 | if cmd is None:
583 | if block:
584 | continue
585 | else:
586 | return None
587 |
588 | self.do_cmd(cmd)
589 | return 1
590 |
591 | def push_char(self, char):
592 | self.console.push_char(char)
593 | self.handle1(0)
594 |
595 | def readline(self, returns_unicode=False, startup_hook=None):
596 | """Read a line. The implementation of this method also shows
597 | how to drive Reader if you want more control over the event
598 | loop."""
599 | self.prepare()
600 | try:
601 | if startup_hook is not None:
602 | startup_hook()
603 | self.refresh()
604 | while not self.finished:
605 | self.handle1()
606 | if returns_unicode:
607 | return self.get_unicode()
608 | return self.get_buffer()
609 | finally:
610 | self.restore()
611 |
612 | def bind(self, spec, command):
613 | self.keymap = self.keymap + ((spec, command),)
614 | self.input_trans = input.KeymapTranslator(
615 | self.keymap,
616 | invalid_cls='invalid-key',
617 | character_cls='self-insert')
618 |
619 | def get_buffer(self, encoding=None):
620 | if encoding is None:
621 | encoding = self.console.encoding
622 | return unicode('').join(self.buffer).encode(self.console.encoding)
623 |
624 | def get_unicode(self):
625 | """Return the current buffer as a unicode string."""
626 | return unicode('').join(self.buffer)
627 |
628 |
629 | def test():
630 | from pyrepl.unix_console import UnixConsole
631 | reader = Reader(UnixConsole())
632 | reader.ps1 = "**> "
633 | reader.ps2 = "/*> "
634 | reader.ps3 = "|*> "
635 | reader.ps4 = r"\*> "
636 | while reader.readline():
637 | pass
638 |
639 |
640 | if __name__ == '__main__':
641 | test()
642 |
--------------------------------------------------------------------------------
/pyrepl/readline.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2010 Michael Hudson-Doyle
2 | # Alex Gaynor
3 | # Antonio Cuni
4 | # Armin Rigo
5 | # Holger Krekel
6 | #
7 | # All Rights Reserved
8 | #
9 | #
10 | # Permission to use, copy, modify, and distribute this software and
11 | # its documentation for any purpose is hereby granted without fee,
12 | # provided that the above copyright notice appear in all copies and
13 | # that both that copyright notice and this permission notice appear in
14 | # supporting documentation.
15 | #
16 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
17 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
18 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
19 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
20 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
21 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
22 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
23 |
24 | """A compatibility wrapper reimplementing the 'readline' standard module
25 | on top of pyrepl. Not all functionalities are supported. Contains
26 | extensions for multiline input.
27 | """
28 |
29 | import sys
30 | import os
31 | from pyrepl import commands
32 | from pyrepl.historical_reader import HistoricalReader
33 | from pyrepl.completing_reader import CompletingReader
34 | from pyrepl.unix_console import UnixConsole, _error
35 | try:
36 | unicode
37 | PY3 = False
38 | except NameError:
39 | PY3 = True
40 | unicode = str
41 | unichr = chr
42 | basestring = bytes, str
43 |
44 |
45 | ENCODING = sys.getfilesystemencoding() or 'latin1' # XXX review
46 |
47 | __all__ = [
48 | 'add_history',
49 | 'clear_history',
50 | 'get_begidx',
51 | 'get_completer',
52 | 'get_completer_delims',
53 | 'get_current_history_length',
54 | 'get_endidx',
55 | 'get_history_item',
56 | 'get_history_length',
57 | 'get_line_buffer',
58 | 'insert_text',
59 | 'parse_and_bind',
60 | 'read_history_file',
61 | 'read_init_file',
62 | 'redisplay',
63 | 'remove_history_item',
64 | 'replace_history_item',
65 | 'set_completer',
66 | 'set_completer_delims',
67 | 'set_history_length',
68 | 'set_pre_input_hook',
69 | 'set_startup_hook',
70 | 'write_history_file',
71 | # ---- multiline extensions ----
72 | 'multiline_input',
73 | ]
74 |
75 | # ____________________________________________________________
76 |
77 |
78 | class ReadlineConfig(object):
79 | readline_completer = None
80 | completer_delims = dict.fromkeys(' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?')
81 |
82 |
83 | class ReadlineAlikeReader(HistoricalReader, CompletingReader):
84 | assume_immutable_completions = False
85 | use_brackets = False
86 | sort_in_column = True
87 |
88 | def error(self, msg="none"):
89 | pass # don't show error messages by default
90 |
91 | def get_stem(self):
92 | b = self.buffer
93 | p = self.pos - 1
94 | completer_delims = self.config.completer_delims
95 | while p >= 0 and b[p] not in completer_delims:
96 | p -= 1
97 | return ''.join(b[p+1:self.pos])
98 |
99 | def get_completions(self, stem):
100 | result = []
101 | function = self.config.readline_completer
102 | if function is not None:
103 | try:
104 | stem = str(stem) # rlcompleter.py seems to not like unicode
105 | except UnicodeEncodeError:
106 | pass # but feed unicode anyway if we have no choice
107 | state = 0
108 | while True:
109 | try:
110 | next = function(stem, state)
111 | except:
112 | break
113 | if not isinstance(next, str):
114 | break
115 | result.append(next)
116 | state += 1
117 | # emulate the behavior of the standard readline that sorts
118 | # the completions before displaying them.
119 | result.sort()
120 | return result
121 |
122 | def get_trimmed_history(self, maxlength):
123 | if maxlength >= 0:
124 | cut = len(self.history) - maxlength
125 | if cut < 0:
126 | cut = 0
127 | else:
128 | cut = 0
129 | return self.history[cut:]
130 |
131 | # --- simplified support for reading multiline Python statements ---
132 |
133 | # This duplicates small parts of pyrepl.python_reader. I'm not
134 | # reusing the PythonicReader class directly for two reasons. One is
135 | # to try to keep as close as possible to CPython's prompt. The
136 | # other is that it is the readline module that we are ultimately
137 | # implementing here, and I don't want the built-in raw_input() to
138 | # start trying to read multiline inputs just because what the user
139 | # typed look like valid but incomplete Python code. So we get the
140 | # multiline feature only when using the multiline_input() function
141 | # directly (see _pypy_interact.py).
142 |
143 | more_lines = None
144 |
145 | def collect_keymap(self):
146 | return super(ReadlineAlikeReader, self).collect_keymap() + (
147 | (r'\n', 'maybe-accept'),)
148 |
149 | def __init__(self, console):
150 | super(ReadlineAlikeReader, self).__init__(console)
151 | self.commands['maybe_accept'] = maybe_accept
152 | self.commands['maybe-accept'] = maybe_accept
153 |
154 | def after_command(self, cmd):
155 | super(ReadlineAlikeReader, self).after_command(cmd)
156 | if self.more_lines is None:
157 | # Force single-line input if we are in raw_input() mode.
158 | # Although there is no direct way to add a \n in this mode,
159 | # multiline buffers can still show up using various
160 | # commands, e.g. navigating the history.
161 | try:
162 | index = self.buffer.index("\n")
163 | except ValueError:
164 | pass
165 | else:
166 | self.buffer = self.buffer[:index]
167 | if self.pos > len(self.buffer):
168 | self.pos = len(self.buffer)
169 |
170 |
171 | class maybe_accept(commands.Command):
172 | def do(self):
173 | r = self.reader
174 | r.dirty = 1 # this is needed to hide the completion menu, if visible
175 | #
176 | # if there are already several lines and the cursor
177 | # is not on the last one, always insert a new \n.
178 | text = r.get_unicode()
179 | if "\n" in r.buffer[r.pos:]:
180 | r.insert("\n")
181 | elif r.more_lines is not None and r.more_lines(text):
182 | r.insert("\n")
183 | else:
184 | self.finish = 1
185 |
186 |
187 | class _ReadlineWrapper(object):
188 | reader = None
189 | saved_history_length = -1
190 | startup_hook = None
191 | config = ReadlineConfig()
192 | stdin = None
193 | stdout = None
194 | stderr = None
195 |
196 | def __init__(self, f_in=None, f_out=None):
197 | self.f_in = f_in if f_in is not None else os.dup(0)
198 | self.f_out = f_out if f_out is not None else os.dup(1)
199 |
200 | def setup_std_streams(self, stdin, stdout, stderr):
201 | self.stdin = stdin
202 | self.stdout = stdout
203 | self.stderr = stderr
204 |
205 | def get_reader(self):
206 | if self.reader is None:
207 | console = UnixConsole(self.f_in, self.f_out, encoding=ENCODING)
208 | self.reader = ReadlineAlikeReader(console)
209 | self.reader.config = self.config
210 | return self.reader
211 |
212 | def raw_input(self, prompt=''):
213 | try:
214 | reader = self.get_reader()
215 | except _error:
216 | return _old_raw_input(prompt)
217 | reader.ps1 = prompt
218 |
219 | # the builtin raw_input calls PyOS_StdioReadline, which flushes
220 | # stdout/stderr before displaying the prompt. Try to mimic this
221 | # behavior: it seems to be the correct thing to do, and moreover it
222 | # mitigates this pytest issue:
223 | # https://github.com/pytest-dev/pytest/issues/5134
224 | if self.stdout and hasattr(self.stdout, 'flush'):
225 | self.stdout.flush()
226 | if self.stderr and hasattr(self.stderr, 'flush'):
227 | self.stderr.flush()
228 |
229 | ret = reader.readline(startup_hook=self.startup_hook)
230 | if not PY3:
231 | return ret
232 |
233 | # Unicode/str is required for Python 3 (3.5.2).
234 | # Ref: https://bitbucket.org/pypy/pyrepl/issues/20/#comment-30647029
235 | return unicode(ret, ENCODING)
236 |
237 | def multiline_input(self, more_lines, ps1, ps2, returns_unicode=False):
238 | """Read an input on possibly multiple lines, asking for more
239 | lines as long as 'more_lines(unicodetext)' returns an object whose
240 | boolean value is true.
241 | """
242 | reader = self.get_reader()
243 | saved = reader.more_lines
244 | try:
245 | reader.more_lines = more_lines
246 | reader.ps1 = reader.ps2 = ps1
247 | reader.ps3 = reader.ps4 = ps2
248 | return reader.readline(returns_unicode=returns_unicode)
249 | finally:
250 | reader.more_lines = saved
251 |
252 | def parse_and_bind(self, string):
253 | pass # XXX we don't support parsing GNU-readline-style init files
254 |
255 | def set_completer(self, function=None):
256 | self.config.readline_completer = function
257 |
258 | def get_completer(self):
259 | return self.config.readline_completer
260 |
261 | def set_completer_delims(self, string):
262 | self.config.completer_delims = dict.fromkeys(string)
263 |
264 | def get_completer_delims(self):
265 | chars = list(self.config.completer_delims.keys())
266 | chars.sort()
267 | return ''.join(chars)
268 |
269 | def _histline(self, line):
270 | line = line.rstrip('\n')
271 | if PY3:
272 | return line
273 |
274 | try:
275 | return unicode(line, ENCODING)
276 | except UnicodeDecodeError: # bah, silently fall back...
277 | return unicode(line, 'utf-8', 'replace')
278 |
279 | def get_history_length(self):
280 | return self.saved_history_length
281 |
282 | def set_history_length(self, length):
283 | self.saved_history_length = length
284 |
285 | def get_current_history_length(self):
286 | return len(self.get_reader().history)
287 |
288 | def read_history_file(self, filename='~/.history'):
289 | # multiline extension (really a hack) for the end of lines that
290 | # are actually continuations inside a single multiline_input()
291 | # history item: we use \r\n instead of just \n. If the history
292 | # file is passed to GNU readline, the extra \r are just ignored.
293 | history = self.get_reader().history
294 | f = open(os.path.expanduser(filename), 'r')
295 | buffer = []
296 | for line in f:
297 | if line.endswith('\r\n'):
298 | buffer.append(line)
299 | else:
300 | line = self._histline(line)
301 | if buffer:
302 | line = ''.join(buffer).replace('\r', '') + line
303 | del buffer[:]
304 | if line:
305 | history.append(line)
306 | f.close()
307 |
308 | def write_history_file(self, filename='~/.history'):
309 | maxlength = self.saved_history_length
310 | history = self.get_reader().get_trimmed_history(maxlength)
311 | entries = ''
312 | for entry in history:
313 | # if we are on py3k, we don't need to encode strings before
314 | # writing it to a file
315 | if isinstance(entry, unicode) and sys.version_info < (3,):
316 | entry = entry.encode('utf-8')
317 | entry = entry.replace('\n', '\r\n') # multiline history support
318 | entries += entry + '\n'
319 |
320 | fname = os.path.expanduser(filename)
321 | if PY3:
322 | f = open(fname, 'w', encoding='utf-8')
323 | else:
324 | f = open(fname, 'w')
325 | f.write(entries)
326 | f.close()
327 |
328 | def clear_history(self):
329 | del self.get_reader().history[:]
330 |
331 | def get_history_item(self, index):
332 | history = self.get_reader().history
333 | if 1 <= index <= len(history):
334 | return history[index-1]
335 | else:
336 | return None # blame readline.c for not raising
337 |
338 | def remove_history_item(self, index):
339 | history = self.get_reader().history
340 | if 0 <= index < len(history):
341 | del history[index]
342 | else:
343 | raise ValueError("No history item at position %d" % index)
344 | # blame readline.c for raising ValueError
345 |
346 | def replace_history_item(self, index, line):
347 | history = self.get_reader().history
348 | if 0 <= index < len(history):
349 | history[index] = self._histline(line)
350 | else:
351 | raise ValueError("No history item at position %d" % index)
352 | # blame readline.c for raising ValueError
353 |
354 | def add_history(self, line):
355 | self.get_reader().history.append(self._histline(line))
356 |
357 | def set_startup_hook(self, function=None):
358 | self.startup_hook = function
359 |
360 | def get_line_buffer(self):
361 | if PY3:
362 | return self.get_reader().get_unicode()
363 | return self.get_reader().get_buffer()
364 |
365 | def _get_idxs(self):
366 | start = cursor = self.get_reader().pos
367 | buf = self.get_line_buffer()
368 | for i in range(cursor - 1, -1, -1):
369 | if buf[i] in self.get_completer_delims():
370 | break
371 | start = i
372 | return start, cursor
373 |
374 | def get_begidx(self):
375 | return self._get_idxs()[0]
376 |
377 | def get_endidx(self):
378 | return self._get_idxs()[1]
379 |
380 | def insert_text(self, text):
381 | return self.get_reader().insert(text)
382 |
383 |
384 | _wrapper = _ReadlineWrapper()
385 |
386 | # ____________________________________________________________
387 | # Public API
388 |
389 | parse_and_bind = _wrapper.parse_and_bind
390 | set_completer = _wrapper.set_completer
391 | get_completer = _wrapper.get_completer
392 | set_completer_delims = _wrapper.set_completer_delims
393 | get_completer_delims = _wrapper.get_completer_delims
394 | get_history_length = _wrapper.get_history_length
395 | set_history_length = _wrapper.set_history_length
396 | get_current_history_length = _wrapper.get_current_history_length
397 | read_history_file = _wrapper.read_history_file
398 | write_history_file = _wrapper.write_history_file
399 | clear_history = _wrapper.clear_history
400 | get_history_item = _wrapper.get_history_item
401 | remove_history_item = _wrapper.remove_history_item
402 | replace_history_item = _wrapper.replace_history_item
403 | add_history = _wrapper.add_history
404 | set_startup_hook = _wrapper.set_startup_hook
405 | get_line_buffer = _wrapper.get_line_buffer
406 | get_begidx = _wrapper.get_begidx
407 | get_endidx = _wrapper.get_endidx
408 | insert_text = _wrapper.insert_text
409 |
410 | # Extension
411 | multiline_input = _wrapper.multiline_input
412 |
413 | # Internal hook
414 | _get_reader = _wrapper.get_reader
415 |
416 | # ____________________________________________________________
417 | # Stubs
418 |
419 |
420 | def _make_stub(_name, _ret):
421 | def stub(*args, **kwds):
422 | import warnings
423 | warnings.warn("readline.%s() not implemented" % _name, stacklevel=2)
424 | stub.func_name = _name
425 | globals()[_name] = stub
426 |
427 | for _name, _ret in [
428 | ('read_init_file', None),
429 | ('redisplay', None),
430 | ('set_pre_input_hook', None),
431 | ]:
432 | assert _name not in globals(), _name
433 | _make_stub(_name, _ret)
434 |
435 |
436 | def _setup():
437 | global _old_raw_input
438 | if _old_raw_input is not None:
439 | return
440 | # don't run _setup twice
441 |
442 | try:
443 | f_in = sys.stdin.fileno()
444 | f_out = sys.stdout.fileno()
445 | except (AttributeError, ValueError):
446 | return
447 | if not os.isatty(f_in) or not os.isatty(f_out):
448 | return
449 |
450 | _wrapper.f_in = f_in
451 | _wrapper.f_out = f_out
452 | _wrapper.setup_std_streams(sys.stdin, sys.stdout, sys.stderr)
453 |
454 | if '__pypy__' in sys.builtin_module_names: # PyPy
455 |
456 | def _old_raw_input(prompt=''):
457 | # sys.__raw_input__() is only called when stdin and stdout are
458 | # as expected and are ttys. If it is the case, then get_reader()
459 | # should not really fail in _wrapper.raw_input(). If it still
460 | # does, then we will just cancel the redirection and call again
461 | # the built-in raw_input().
462 | try:
463 | del sys.__raw_input__
464 | except AttributeError:
465 | pass
466 | return raw_input(prompt)
467 | sys.__raw_input__ = _wrapper.raw_input
468 |
469 | else:
470 | # this is not really what readline.c does. Better than nothing I guess
471 | try:
472 | import __builtin__
473 | _old_raw_input = __builtin__.raw_input
474 | __builtin__.raw_input = _wrapper.raw_input
475 | except ImportError:
476 | import builtins
477 | _old_raw_input = builtins.input
478 | builtins.input = _wrapper.raw_input
479 |
480 | _old_raw_input = None
481 | _setup()
482 |
--------------------------------------------------------------------------------
/pyrepl/simple_interact.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2010 Michael Hudson-Doyle
2 | # Armin Rigo
3 | #
4 | # All Rights Reserved
5 | #
6 | #
7 | # Permission to use, copy, modify, and distribute this software and
8 | # its documentation for any purpose is hereby granted without fee,
9 | # provided that the above copyright notice appear in all copies and
10 | # that both that copyright notice and this permission notice appear in
11 | # supporting documentation.
12 | #
13 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 |
21 | """This is an alternative to python_reader which tries to emulate
22 | the CPython prompt as closely as possible, with the exception of
23 | allowing multiline input and multiline history entries.
24 | """
25 |
26 | import sys
27 | from pyrepl.readline import multiline_input, _error, _get_reader
28 |
29 |
30 | def check(): # returns False if there is a problem initializing the state
31 | try:
32 | _get_reader()
33 | except _error:
34 | return False
35 | return True
36 |
37 |
38 | def run_multiline_interactive_console(mainmodule=None, future_flags=0):
39 | import code
40 | import __main__
41 | mainmodule = mainmodule or __main__
42 | console = code.InteractiveConsole(mainmodule.__dict__, filename='')
43 | if future_flags:
44 | console.compile.compiler.flags |= future_flags
45 |
46 | def more_lines(unicodetext):
47 | if sys.version_info < (3, ):
48 | # ooh, look at the hack:
49 | src = "#coding:utf-8\n"+unicodetext.encode('utf-8')
50 | else:
51 | src = unicodetext
52 | try:
53 | code = console.compile(src, '', 'single')
54 | except (OverflowError, SyntaxError, ValueError):
55 | return False
56 | else:
57 | return code is None
58 |
59 | while 1:
60 | try:
61 | ps1 = getattr(sys, 'ps1', '>>> ')
62 | ps2 = getattr(sys, 'ps2', '... ')
63 | try:
64 | statement = multiline_input(more_lines, ps1, ps2,
65 | returns_unicode=True)
66 | except EOFError:
67 | break
68 | more = console.push(statement)
69 | assert not more
70 | except KeyboardInterrupt:
71 | console.write("\nKeyboardInterrupt\n")
72 | console.resetbuffer()
73 |
--------------------------------------------------------------------------------
/pyrepl/trace.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | trace_filename = os.environ.get("PYREPL_TRACE")
4 |
5 | if trace_filename is not None:
6 | trace_file = open(trace_filename, 'a')
7 | else:
8 | trace_file = None
9 |
10 | def trace(line, *k, **kw):
11 | if trace_file is None:
12 | return
13 | if k or kw:
14 | line = line.format(*k, **kw)
15 | trace_file.write(line+'\n')
16 | trace_file.flush()
17 |
18 |
--------------------------------------------------------------------------------
/pyrepl/unix_console.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2010 Michael Hudson-Doyle
2 | # Antonio Cuni
3 | # Armin Rigo
4 | #
5 | # All Rights Reserved
6 | #
7 | #
8 | # Permission to use, copy, modify, and distribute this software and
9 | # its documentation for any purpose is hereby granted without fee,
10 | # provided that the above copyright notice appear in all copies and
11 | # that both that copyright notice and this permission notice appear in
12 | # supporting documentation.
13 | #
14 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
15 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
16 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
17 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
18 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
19 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
20 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 |
22 | import termios
23 | import select
24 | import os
25 | import struct
26 | import errno
27 | import signal
28 | import re
29 | import time
30 | import sys
31 | from fcntl import ioctl
32 | from . import curses
33 | from .fancy_termios import tcgetattr, tcsetattr
34 | from .console import Console, Event
35 | from .unix_eventqueue import EventQueue
36 | from .trace import trace
37 |
38 |
39 | class InvalidTerminal(RuntimeError):
40 | pass
41 |
42 | try:
43 | unicode
44 | except NameError:
45 | unicode = str
46 |
47 | _error = (termios.error, curses.error, InvalidTerminal)
48 |
49 | # there are arguments for changing this to "refresh"
50 | SIGWINCH_EVENT = 'repaint'
51 |
52 | FIONREAD = getattr(termios, "FIONREAD", None)
53 | TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", None)
54 |
55 |
56 | def _my_getstr(cap, optional=0):
57 | r = curses.tigetstr(cap)
58 | if not optional and r is None:
59 | raise InvalidTerminal(
60 | "terminal doesn't have the required '%s' capability" % cap)
61 | return r
62 |
63 |
64 | # at this point, can we say: AAAAAAAAAAAAAAAAAAAAAARGH!
65 | def maybe_add_baudrate(dict, rate):
66 | name = 'B%d' % rate
67 | if hasattr(termios, name):
68 | dict[getattr(termios, name)] = rate
69 |
70 | ratedict = {}
71 | for r in [0, 110, 115200, 1200, 134, 150, 1800, 19200, 200, 230400,
72 | 2400, 300, 38400, 460800, 4800, 50, 57600, 600, 75, 9600]:
73 | maybe_add_baudrate(ratedict, r)
74 |
75 | del r, maybe_add_baudrate
76 |
77 | delayprog = re.compile(b"\\$<([0-9]+)((?:/|\\*){0,2})>")
78 |
79 | try:
80 | poll = select.poll
81 | except AttributeError:
82 | # this is exactly the minumum necessary to support what we
83 | # do with poll objects
84 | class poll:
85 | def __init__(self):
86 | pass
87 |
88 | def register(self, fd, flag):
89 | self.fd = fd
90 |
91 | def poll(self, timeout=None):
92 | r, w, e = select.select([self.fd], [], [], timeout)
93 | return r
94 |
95 | POLLIN = getattr(select, "POLLIN", None)
96 |
97 |
98 | required_curses_tistrings = 'bel clear cup el'
99 | optional_curses_tistrings = (
100 | 'civis cnorm cub cub1 cud cud1 cud cud1 cuf '
101 | 'cuf1 cuu cuu1 dch dch1 hpa ich ich1 ind pad ri rmkx smkx')
102 |
103 |
104 | class UnixConsole(Console):
105 | def __init__(self, f_in=0, f_out=1, term=None, encoding=None):
106 | if encoding is None:
107 | encoding = sys.getdefaultencoding()
108 |
109 | self.encoding = encoding
110 |
111 | if isinstance(f_in, int):
112 | self.input_fd = f_in
113 | else:
114 | self.input_fd = f_in.fileno()
115 |
116 | if isinstance(f_out, int):
117 | self.output_fd = f_out
118 | else:
119 | self.output_fd = f_out.fileno()
120 |
121 | self.pollob = poll()
122 | self.pollob.register(self.input_fd, POLLIN)
123 | curses.setupterm(term, self.output_fd)
124 | self.term = term
125 |
126 | for name in required_curses_tistrings.split():
127 | setattr(self, '_' + name, _my_getstr(name))
128 |
129 | for name in optional_curses_tistrings.split():
130 | setattr(self, '_' + name, _my_getstr(name, optional=1))
131 |
132 | ## work out how we're going to sling the cursor around
133 | # hpa don't work in windows telnet :-(
134 | if 0 and self._hpa:
135 | self.__move_x = self.__move_x_hpa
136 | elif self._cub and self._cuf:
137 | self.__move_x = self.__move_x_cub_cuf
138 | elif self._cub1 and self._cuf1:
139 | self.__move_x = self.__move_x_cub1_cuf1
140 | else:
141 | raise RuntimeError("insufficient terminal (horizontal)")
142 |
143 | if self._cuu and self._cud:
144 | self.__move_y = self.__move_y_cuu_cud
145 | elif self._cuu1 and self._cud1:
146 | self.__move_y = self.__move_y_cuu1_cud1
147 | else:
148 | raise RuntimeError("insufficient terminal (vertical)")
149 |
150 | if self._dch1:
151 | self.dch1 = self._dch1
152 | elif self._dch:
153 | self.dch1 = curses.tparm(self._dch, 1)
154 | else:
155 | self.dch1 = None
156 |
157 | if self._ich1:
158 | self.ich1 = self._ich1
159 | elif self._ich:
160 | self.ich1 = curses.tparm(self._ich, 1)
161 | else:
162 | self.ich1 = None
163 |
164 | self.__move = self.__move_short
165 |
166 | self.event_queue = EventQueue(self.input_fd, self.encoding)
167 | self.cursor_visible = 1
168 |
169 | def refresh(self, screen, c_xy):
170 | # this function is still too long (over 90 lines)
171 | cx, cy = c_xy
172 | if not self.__gone_tall:
173 | while len(self.screen) < min(len(screen), self.height):
174 | self.__hide_cursor()
175 | self.__move(0, len(self.screen) - 1)
176 | self.__write("\n")
177 | self.__posxy = 0, len(self.screen)
178 | self.screen.append("")
179 | else:
180 | while len(self.screen) < len(screen):
181 | self.screen.append("")
182 |
183 | if len(screen) > self.height:
184 | self.__gone_tall = 1
185 | self.__move = self.__move_tall
186 |
187 | px, py = self.__posxy
188 | old_offset = offset = self.__offset
189 | height = self.height
190 |
191 | # we make sure the cursor is on the screen, and that we're
192 | # using all of the screen if we can
193 | if cy < offset:
194 | offset = cy
195 | elif cy >= offset + height:
196 | offset = cy - height + 1
197 | elif offset > 0 and len(screen) < offset + height:
198 | offset = max(len(screen) - height, 0)
199 | screen.append("")
200 |
201 | oldscr = self.screen[old_offset:old_offset + height]
202 | newscr = screen[offset:offset + height]
203 |
204 | # use hardware scrolling if we have it.
205 | if old_offset > offset and self._ri:
206 | self.__hide_cursor()
207 | self.__write_code(self._cup, 0, 0)
208 | self.__posxy = 0, old_offset
209 | for i in range(old_offset - offset):
210 | self.__write_code(self._ri)
211 | oldscr.pop(-1)
212 | oldscr.insert(0, "")
213 | elif old_offset < offset and self._ind:
214 | self.__hide_cursor()
215 | self.__write_code(self._cup, self.height - 1, 0)
216 | self.__posxy = 0, old_offset + self.height - 1
217 | for i in range(offset - old_offset):
218 | self.__write_code(self._ind)
219 | oldscr.pop(0)
220 | oldscr.append("")
221 |
222 | self.__offset = offset
223 |
224 | for y, oldline, newline, in zip(range(offset, offset + height),
225 | oldscr,
226 | newscr):
227 | if oldline != newline:
228 | self.__write_changed_line(y, oldline, newline, px)
229 |
230 | y = len(newscr)
231 | while y < len(oldscr):
232 | self.__hide_cursor()
233 | self.__move(0, y)
234 | self.__posxy = 0, y
235 | self.__write_code(self._el)
236 | y += 1
237 |
238 | self.__show_cursor()
239 |
240 | self.screen = screen
241 | self.move_cursor(cx, cy)
242 | self.flushoutput()
243 |
244 | def __write_changed_line(self, y, oldline, newline, px):
245 | # this is frustrating; there's no reason to test (say)
246 | # self.dch1 inside the loop -- but alternative ways of
247 | # structuring this function are equally painful (I'm trying to
248 | # avoid writing code generators these days...)
249 | x = 0
250 | minlen = min(len(oldline), len(newline))
251 | #
252 | # reuse the oldline as much as possible, but stop as soon as we
253 | # encounter an ESCAPE, because it might be the start of an escape
254 | # sequene
255 | #XXX unicode check!
256 | while x < minlen and oldline[x] == newline[x] and newline[x] != '\x1b':
257 | x += 1
258 | if oldline[x:] == newline[x+1:] and self.ich1:
259 | if (y == self.__posxy[1] and x > self.__posxy[0] and
260 | oldline[px:x] == newline[px+1:x+1]):
261 | x = px
262 | self.__move(x, y)
263 | self.__write_code(self.ich1)
264 | self.__write(newline[x])
265 | self.__posxy = x + 1, y
266 | elif x < minlen and oldline[x + 1:] == newline[x + 1:]:
267 | self.__move(x, y)
268 | self.__write(newline[x])
269 | self.__posxy = x + 1, y
270 | elif (self.dch1 and self.ich1 and len(newline) == self.width
271 | and x < len(newline) - 2
272 | and newline[x+1:-1] == oldline[x:-2]):
273 | self.__hide_cursor()
274 | self.__move(self.width - 2, y)
275 | self.__posxy = self.width - 2, y
276 | self.__write_code(self.dch1)
277 | self.__move(x, y)
278 | self.__write_code(self.ich1)
279 | self.__write(newline[x])
280 | self.__posxy = x + 1, y
281 | else:
282 | self.__hide_cursor()
283 | self.__move(x, y)
284 | if len(oldline) > len(newline):
285 | self.__write_code(self._el)
286 | self.__write(newline[x:])
287 | self.__posxy = len(newline), y
288 |
289 | #XXX: check for unicode mess
290 | if '\x1b' in newline:
291 | # ANSI escape characters are present, so we can't assume
292 | # anything about the position of the cursor. Moving the cursor
293 | # to the left margin should work to get to a known position.
294 | self.move_cursor(0, y)
295 |
296 | def __write(self, text):
297 | self.__buffer.append((text, 0))
298 |
299 | def __write_code(self, fmt, *args):
300 |
301 | self.__buffer.append((curses.tparm(fmt, *args), 1))
302 |
303 | def __maybe_write_code(self, fmt, *args):
304 | if fmt:
305 | self.__write_code(fmt, *args)
306 |
307 | def __move_y_cuu1_cud1(self, y):
308 | dy = y - self.__posxy[1]
309 | if dy > 0:
310 | self.__write_code(dy*self._cud1)
311 | elif dy < 0:
312 | self.__write_code((-dy)*self._cuu1)
313 |
314 | def __move_y_cuu_cud(self, y):
315 | dy = y - self.__posxy[1]
316 | if dy > 0:
317 | self.__write_code(self._cud, dy)
318 | elif dy < 0:
319 | self.__write_code(self._cuu, -dy)
320 |
321 | def __move_x_hpa(self, x):
322 | if x != self.__posxy[0]:
323 | self.__write_code(self._hpa, x)
324 |
325 | def __move_x_cub1_cuf1(self, x):
326 | dx = x - self.__posxy[0]
327 | if dx > 0:
328 | self.__write_code(self._cuf1*dx)
329 | elif dx < 0:
330 | self.__write_code(self._cub1*(-dx))
331 |
332 | def __move_x_cub_cuf(self, x):
333 | dx = x - self.__posxy[0]
334 | if dx > 0:
335 | self.__write_code(self._cuf, dx)
336 | elif dx < 0:
337 | self.__write_code(self._cub, -dx)
338 |
339 | def __move_short(self, x, y):
340 | self.__move_x(x)
341 | self.__move_y(y)
342 |
343 | def __move_tall(self, x, y):
344 | assert 0 <= y - self.__offset < self.height, y - self.__offset
345 | self.__write_code(self._cup, y - self.__offset, x)
346 |
347 | def move_cursor(self, x, y):
348 | if y < self.__offset or y >= self.__offset + self.height:
349 | self.event_queue.insert(Event('scroll', None))
350 | else:
351 | self.__move(x, y)
352 | self.__posxy = x, y
353 | self.flushoutput()
354 |
355 | def prepare(self):
356 | # per-readline preparations:
357 | self.__svtermstate = tcgetattr(self.input_fd)
358 | raw = self.__svtermstate.copy()
359 | raw.iflag |= termios.ICRNL
360 | raw.iflag &= ~(termios.BRKINT | termios.INPCK |
361 | termios.ISTRIP | termios.IXON)
362 | raw.cflag &= ~(termios.CSIZE | termios.PARENB)
363 | raw.cflag |= (termios.CS8)
364 | raw.lflag &= ~(termios.ICANON | termios.ECHO |
365 | termios.IEXTEN | (termios.ISIG * 1))
366 | raw.cc[termios.VMIN] = 1
367 | raw.cc[termios.VTIME] = 0
368 | tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
369 |
370 | self.screen = []
371 | self.height, self.width = self.getheightwidth()
372 |
373 | self.__buffer = []
374 |
375 | self.__posxy = 0, 0
376 | self.__gone_tall = 0
377 | self.__move = self.__move_short
378 | self.__offset = 0
379 |
380 | self.__maybe_write_code(self._smkx)
381 |
382 | try:
383 | self.old_sigwinch = signal.signal(
384 | signal.SIGWINCH, self.__sigwinch)
385 | except ValueError:
386 | pass
387 |
388 | def restore(self):
389 | self.__maybe_write_code(self._rmkx)
390 | self.flushoutput()
391 | tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
392 |
393 | if hasattr(self, 'old_sigwinch'):
394 | try:
395 | signal.signal(signal.SIGWINCH, self.old_sigwinch)
396 | del self.old_sigwinch
397 | except ValueError:
398 | # signal only works in main thread.
399 | pass
400 |
401 | def __sigwinch(self, signum, frame):
402 | self.height, self.width = self.getheightwidth()
403 | self.event_queue.insert(Event('resize', None))
404 | if self.old_sigwinch != signal.SIG_DFL:
405 | self.old_sigwinch(signum, frame)
406 |
407 | def push_char(self, char):
408 | trace('push char {char!r}', char=char)
409 | self.event_queue.push(char)
410 |
411 | def get_event(self, block=1):
412 | while self.event_queue.empty():
413 | while 1:
414 | # All hail Unix!
415 | try:
416 | self.push_char(os.read(self.input_fd, 1))
417 | except (IOError, OSError) as err:
418 | if err.errno == errno.EINTR:
419 | if not self.event_queue.empty():
420 | return self.event_queue.get()
421 | else:
422 | continue
423 | else:
424 | raise
425 | else:
426 | break
427 | if not block:
428 | break
429 | return self.event_queue.get()
430 |
431 | def wait(self):
432 | self.pollob.poll()
433 |
434 | def set_cursor_vis(self, vis):
435 | if vis:
436 | self.__show_cursor()
437 | else:
438 | self.__hide_cursor()
439 |
440 | def __hide_cursor(self):
441 | if self.cursor_visible:
442 | self.__maybe_write_code(self._civis)
443 | self.cursor_visible = 0
444 |
445 | def __show_cursor(self):
446 | if not self.cursor_visible:
447 | self.__maybe_write_code(self._cnorm)
448 | self.cursor_visible = 1
449 |
450 | def repaint_prep(self):
451 | if not self.__gone_tall:
452 | self.__posxy = 0, self.__posxy[1]
453 | self.__write("\r")
454 | ns = len(self.screen)*['\000'*self.width]
455 | self.screen = ns
456 | else:
457 | self.__posxy = 0, self.__offset
458 | self.__move(0, self.__offset)
459 | ns = self.height*['\000'*self.width]
460 | self.screen = ns
461 |
462 | if TIOCGWINSZ:
463 | def getheightwidth(self):
464 | try:
465 | return int(os.environ["LINES"]), int(os.environ["COLUMNS"])
466 | except KeyError:
467 | height, width = struct.unpack(
468 | "hhhh", ioctl(self.input_fd, TIOCGWINSZ, "\000"*8))[0:2]
469 | if not height:
470 | return 25, 80
471 | return height, width
472 | else:
473 | def getheightwidth(self):
474 | try:
475 | return int(os.environ["LINES"]), int(os.environ["COLUMNS"])
476 | except KeyError:
477 | return 25, 80
478 |
479 | def forgetinput(self):
480 | termios.tcflush(self.input_fd, termios.TCIFLUSH)
481 |
482 | def flushoutput(self):
483 | for text, iscode in self.__buffer:
484 | if iscode:
485 | self.__tputs(text)
486 | else:
487 | os.write(self.output_fd, text.encode(self.encoding, 'replace'))
488 | del self.__buffer[:]
489 |
490 | def __tputs(self, fmt, prog=delayprog):
491 | """A Python implementation of the curses tputs function; the
492 | curses one can't really be wrapped in a sane manner.
493 |
494 | I have the strong suspicion that this is complexity that
495 | will never do anyone any good."""
496 | # using .get() means that things will blow up
497 | # only if the bps is actually needed (which I'm
498 | # betting is pretty unlkely)
499 | bps = ratedict.get(self.__svtermstate.ospeed)
500 | while 1:
501 | m = prog.search(fmt)
502 | if not m:
503 | os.write(self.output_fd, fmt)
504 | break
505 | x, y = m.span()
506 | os.write(self.output_fd, fmt[:x])
507 | fmt = fmt[y:]
508 | delay = int(m.group(1))
509 | if '*' in m.group(2):
510 | delay *= self.height
511 | if self._pad:
512 | nchars = (bps*delay)/1000
513 | os.write(self.output_fd, self._pad*nchars)
514 | else:
515 | time.sleep(float(delay)/1000.0)
516 |
517 | def finish(self):
518 | y = len(self.screen) - 1
519 | while y >= 0 and not self.screen[y]:
520 | y -= 1
521 | self.__move(0, min(y, self.height + self.__offset - 1))
522 | self.__write("\n\r")
523 | self.flushoutput()
524 |
525 | def beep(self):
526 | self.__maybe_write_code(self._bel)
527 | self.flushoutput()
528 |
529 | if FIONREAD:
530 | def getpending(self):
531 | e = Event('key', '', '')
532 |
533 | while not self.event_queue.empty():
534 | e2 = self.event_queue.get()
535 | e.data += e2.data
536 | e.raw += e.raw
537 |
538 | amount = struct.unpack(
539 | "i", ioctl(self.input_fd, FIONREAD, "\0\0\0\0"))[0]
540 | data = os.read(self.input_fd, amount)
541 | raw = unicode(data, self.encoding, 'replace')
542 | #XXX: something is wrong here
543 | e.data += raw
544 | e.raw += raw
545 | return e
546 | else:
547 | def getpending(self):
548 | e = Event('key', '', '')
549 |
550 | while not self.event_queue.empty():
551 | e2 = self.event_queue.get()
552 | e.data += e2.data
553 | e.raw += e.raw
554 |
555 | amount = 10000
556 | data = os.read(self.input_fd, amount)
557 | raw = unicode(data, self.encoding, 'replace')
558 | #XXX: something is wrong here
559 | e.data += raw
560 | e.raw += raw
561 | return e
562 |
563 | def clear(self):
564 | self.__write_code(self._clear)
565 | self.__gone_tall = 1
566 | self.__move = self.__move_tall
567 | self.__posxy = 0, 0
568 | self.screen = []
569 |
--------------------------------------------------------------------------------
/pyrepl/unix_eventqueue.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2008 Michael Hudson-Doyle
2 | # Armin Rigo
3 | #
4 | # All Rights Reserved
5 | #
6 | #
7 | # Permission to use, copy, modify, and distribute this software and
8 | # its documentation for any purpose is hereby granted without fee,
9 | # provided that the above copyright notice appear in all copies and
10 | # that both that copyright notice and this permission notice appear in
11 | # supporting documentation.
12 | #
13 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 |
21 | # Bah, this would be easier to test if curses/terminfo didn't have so
22 | # much non-introspectable global state.
23 |
24 | from collections import deque
25 |
26 | from pyrepl import keymap
27 | from pyrepl.console import Event
28 | from pyrepl import curses
29 | from .trace import trace
30 | from termios import tcgetattr, VERASE
31 | import os
32 | try:
33 | unicode
34 | except NameError:
35 | unicode = str
36 |
37 |
38 | _keynames = {
39 | "delete": "kdch1",
40 | "down": "kcud1",
41 | "end": "kend",
42 | "enter": "kent",
43 | "home": "khome",
44 | "insert": "kich1",
45 | "left": "kcub1",
46 | "page down": "knp",
47 | "page up": "kpp",
48 | "right": "kcuf1",
49 | "up": "kcuu1",
50 | }
51 |
52 |
53 | #function keys x in 1-20 -> fX: kfX
54 | _keynames.update(('f%d' % i, 'kf%d' % i) for i in range(1, 21))
55 |
56 | # this is a bit of a hack: CTRL-left and CTRL-right are not standardized
57 | # termios sequences: each terminal emulator implements its own slightly
58 | # different incarnation, and as far as I know, there is no way to know
59 | # programmatically which sequences correspond to CTRL-left and
60 | # CTRL-right. In bash, these keys usually work because there are bindings
61 | # in ~/.inputrc, but pyrepl does not support it. The workaround is to
62 | # hard-code here a bunch of known sequences, which will be seen as "ctrl
63 | # left" and "ctrl right" keys, which can be finally be mapped to commands
64 | # by the reader's keymaps.
65 | #
66 | CTRL_ARROW_KEYCODE = {
67 | # for xterm, gnome-terminal, xfce terminal, etc.
68 | b'\033[1;5D': 'ctrl left',
69 | b'\033[1;5C': 'ctrl right',
70 | # for rxvt
71 | b'\033Od': 'ctrl left',
72 | b'\033Oc': 'ctrl right',
73 | }
74 |
75 | def general_keycodes():
76 | keycodes = {}
77 | for key, tiname in _keynames.items():
78 | keycode = curses.tigetstr(tiname)
79 | trace('key {key} tiname {tiname} keycode {keycode!r}', **locals())
80 | if keycode:
81 | keycodes[keycode] = key
82 | keycodes.update(CTRL_ARROW_KEYCODE)
83 | return keycodes
84 |
85 |
86 | def EventQueue(fd, encoding):
87 | keycodes = general_keycodes()
88 | if os.isatty(fd):
89 | backspace = tcgetattr(fd)[6][VERASE]
90 | keycodes[backspace] = unicode('backspace')
91 | k = keymap.compile_keymap(keycodes)
92 | trace('keymap {k!r}', k=k)
93 | return EncodedQueue(k, encoding)
94 |
95 |
96 | class EncodedQueue(object):
97 | def __init__(self, keymap, encoding):
98 | self.k = self.ck = keymap
99 | self.events = deque()
100 | self.buf = bytearray()
101 | self.encoding = encoding
102 |
103 | def get(self):
104 | if self.events:
105 | return self.events.popleft()
106 | else:
107 | return None
108 |
109 | def empty(self):
110 | return not self.events
111 |
112 | def flush_buf(self):
113 | old = self.buf
114 | self.buf = bytearray()
115 | return old
116 |
117 | def insert(self, event):
118 | trace('added event {event}', event=event)
119 | self.events.append(event)
120 |
121 | def push(self, char):
122 | ord_char = char if isinstance(char, int) else ord(char)
123 | char = bytes(bytearray((ord_char,)))
124 | self.buf.append(ord_char)
125 | if char in self.k:
126 | if self.k is self.ck:
127 | #sanity check, buffer is empty when a special key comes
128 | assert len(self.buf) == 1
129 | k = self.k[char]
130 | trace('found map {k!r}', k=k)
131 | if isinstance(k, dict):
132 | self.k = k
133 | else:
134 | self.insert(Event('key', k, self.flush_buf()))
135 | self.k = self.ck
136 |
137 | elif self.buf and self.buf[0] == 27: # escape
138 | # escape sequence not recognized by our keymap: propagate it
139 | # outside so that i can be recognized as an M-... key (see also
140 | # the docstring in keymap.py, in particular the line \\E.
141 | trace('unrecognized escape sequence, propagating...')
142 | self.k = self.ck
143 | self.insert(Event('key', '\033', bytearray(b'\033')))
144 | for c in self.flush_buf()[1:]:
145 | self.push(chr(c))
146 |
147 | else:
148 | try:
149 | decoded = bytes(self.buf).decode(self.encoding)
150 | except UnicodeError:
151 | return
152 | else:
153 | self.insert(Event('key', decoded, self.flush_buf()))
154 | self.k = self.ck
155 |
--------------------------------------------------------------------------------
/pythoni:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Copyright 2000-2002 Michael Hudson mwh@python.net
4 | #
5 | # All Rights Reserved
6 | #
7 | #
8 | # Permission to use, copy, modify, and distribute this software and
9 | # its documentation for any purpose is hereby granted without fee,
10 | # provided that the above copyright notice appear in all copies and
11 | # that both that copyright notice and this permission notice appear in
12 | # supporting documentation.
13 | #
14 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
15 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
16 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
17 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
18 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
19 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
20 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 |
22 | import locale, pdb, sys
23 | # I forget exactly why this is necessary:
24 | try:
25 | locale.setlocale(locale.LC_ALL, '')
26 | except locale.Error:
27 | pass # oh well
28 |
29 |
30 | from pyrepl.python_reader import main
31 | from pyrepl import cmdrepl
32 |
33 | # whizzy feature: graft pyrepl support onto pdb
34 | #pdb.Pdb = cmdrepl.replize(pdb.Pdb, 1)
35 |
36 | main(use_pygame_console=('pg' in sys.argv))
37 |
--------------------------------------------------------------------------------
/pythoni1:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """ This is an alternative to pythoni which tries to look like the
3 | CPython prompt as much as possible, with the exception of allowing
4 | multiline input and multiline history entries.
5 | """
6 |
7 | import os
8 | import sys
9 |
10 | from pyrepl import readline
11 | from pyrepl.simple_interact import run_multiline_interactive_console
12 |
13 | sys.modules['readline'] = readline
14 |
15 | if os.getenv('PYTHONSTARTUP'):
16 | exec(open(os.getenv('PYTHONSTARTUP')).read())
17 |
18 | print('Python', sys.version)
19 | run_multiline_interactive_console()
20 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson mwh@python.net
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | from setuptools import setup
21 |
22 | long_desc = """\
23 | pyrepl is a Python library, inspired by readline, for building flexible
24 | command line interfaces, featuring:
25 | * sane multi-line editing
26 | * history, with incremental search
27 | * completion, including displaying of available options
28 | * a fairly large subset of the readline emacs-mode keybindings
29 | * a liberal, Python-style, license
30 | * a new python top-level."""
31 |
32 |
33 | setup(
34 | name="pyrepl",
35 | setup_requires="setupmeta",
36 | versioning="devcommit",
37 | author="Michael Hudson-Doyle",
38 | author_email="micahel@gmail.com",
39 | maintainer="Daniel Hahler",
40 | url="https://github.com/pypy/pyrepl",
41 | license="MIT X11 style",
42 | description="A library for building flexible command line interfaces",
43 | platforms=["unix", "linux"],
44 | packages=["pyrepl"],
45 | scripts=["pythoni", "pythoni1"],
46 | long_description=long_desc,
47 | )
48 |
--------------------------------------------------------------------------------
/testing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pypy/pyrepl/ca192a80b76700118b9bfd261a3d098b92ccfc31/testing/__init__.py
--------------------------------------------------------------------------------
/testing/infrastructure.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | from __future__ import print_function
21 | from pyrepl.reader import Reader
22 | from pyrepl.console import Console, Event
23 |
24 |
25 | class EqualsAnything(object):
26 | def __eq__(self, other):
27 | return True
28 |
29 |
30 | EA = EqualsAnything()
31 |
32 |
33 | class TestConsole(Console):
34 | height = 24
35 | width = 80
36 | encoding = 'utf-8'
37 |
38 | def __init__(self, events, verbose=False):
39 | self.events = events
40 | self.next_screen = None
41 | self.verbose = verbose
42 |
43 | def refresh(self, screen, xy):
44 | if self.next_screen is not None:
45 | assert screen == self.next_screen, "[ %s != %s after %r ]" % (
46 | screen, self.next_screen, self.last_event_name)
47 |
48 | def get_event(self, block=1):
49 | ev, sc = self.events.pop(0)
50 | self.next_screen = sc
51 | if not isinstance(ev, tuple):
52 | ev = (ev, None)
53 | self.last_event_name = ev[0]
54 | if self.verbose:
55 | print("event", ev)
56 | return Event(*ev)
57 |
58 | def getpending(self):
59 | """Nothing pending, but do not return None here."""
60 | return Event('key', '', b'')
61 |
62 |
63 | class TestReader(Reader):
64 | __test__ = False
65 |
66 | def get_prompt(self, lineno, cursor_on_line):
67 | return ''
68 |
69 | def refresh(self):
70 | Reader.refresh(self)
71 | self.dirty = True
72 |
73 |
74 | def read_spec(test_spec, reader_class=TestReader):
75 | # remember to finish your test_spec with 'accept' or similar!
76 | con = TestConsole(test_spec, verbose=True)
77 | reader = reader_class(con)
78 | reader.readline()
79 |
--------------------------------------------------------------------------------
/testing/test_basic.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 | import pytest
20 | from .infrastructure import read_spec
21 |
22 |
23 | def test_basic():
24 | read_spec([(('self-insert', 'a'), ['a']),
25 | ( 'accept', ['a'])])
26 |
27 |
28 | def test_repeat():
29 | read_spec([(('digit-arg', '3'), ['']),
30 | (('self-insert', 'a'), ['aaa']),
31 | ( 'accept', ['aaa'])])
32 |
33 |
34 | def test_kill_line():
35 | read_spec([(('self-insert', 'abc'), ['abc']),
36 | ( 'left', None),
37 | ( 'kill-line', ['ab']),
38 | ( 'accept', ['ab'])])
39 |
40 |
41 | def test_unix_line_discard():
42 | read_spec([(('self-insert', 'abc'), ['abc']),
43 | ( 'left', None),
44 | ( 'unix-word-rubout', ['c']),
45 | ( 'accept', ['c'])])
46 |
47 |
48 | def test_kill_word():
49 | read_spec([(('self-insert', 'ab cd'), ['ab cd']),
50 | ( 'beginning-of-line', ['ab cd']),
51 | ( 'kill-word', [' cd']),
52 | ( 'accept', [' cd'])])
53 |
54 |
55 | def test_backward_kill_word():
56 | read_spec([(('self-insert', 'ab cd'), ['ab cd']),
57 | ( 'backward-kill-word', ['ab ']),
58 | ( 'accept', ['ab '])])
59 |
60 |
61 | def test_yank():
62 | read_spec([(('self-insert', 'ab cd'), ['ab cd']),
63 | ( 'backward-kill-word', ['ab ']),
64 | ( 'beginning-of-line', ['ab ']),
65 | ( 'yank', ['cdab ']),
66 | ( 'accept', ['cdab '])])
67 |
68 |
69 | def test_yank_pop():
70 | read_spec([(('self-insert', 'ab cd'), ['ab cd']),
71 | ( 'backward-kill-word', ['ab ']),
72 | ( 'left', ['ab ']),
73 | ( 'backward-kill-word', [' ']),
74 | ( 'yank', ['ab ']),
75 | ( 'yank-pop', ['cd ']),
76 | ( 'accept', ['cd '])])
77 |
78 |
79 | def test_interrupt():
80 | with pytest.raises(KeyboardInterrupt):
81 | read_spec([('interrupt', [''])])
82 |
83 |
84 | # test_suspend -- hah
85 | def test_up():
86 | read_spec([(('self-insert', 'ab\ncd'), ['ab', 'cd']),
87 | ( 'up', ['ab', 'cd']),
88 | (('self-insert', 'e'), ['abe', 'cd']),
89 | ( 'accept', ['abe', 'cd'])])
90 |
91 |
92 | def test_down():
93 | read_spec([(('self-insert', 'ab\ncd'), ['ab', 'cd']),
94 | ( 'up', ['ab', 'cd']),
95 | (('self-insert', 'e'), ['abe', 'cd']),
96 | ( 'down', ['abe', 'cd']),
97 | (('self-insert', 'f'), ['abe', 'cdf']),
98 | ( 'accept', ['abe', 'cdf'])])
99 |
100 |
101 | def test_left():
102 | read_spec([(('self-insert', 'ab'), ['ab']),
103 | ( 'left', ['ab']),
104 | (('self-insert', 'c'), ['acb']),
105 | ( 'accept', ['acb'])])
106 |
107 |
108 | def test_right():
109 | read_spec([(('self-insert', 'ab'), ['ab']),
110 | ( 'left', ['ab']),
111 | (('self-insert', 'c'), ['acb']),
112 | ( 'right', ['acb']),
113 | (('self-insert', 'd'), ['acbd']),
114 | ( 'accept', ['acbd'])])
115 |
--------------------------------------------------------------------------------
/testing/test_bugs.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | from pyrepl.historical_reader import HistoricalReader
21 | from .infrastructure import EA, TestReader, read_spec
22 |
23 | # this test case should contain as-verbatim-as-possible versions of
24 | # (applicable) bug reports
25 |
26 | import pytest
27 |
28 |
29 | class HistoricalTestReader(HistoricalReader, TestReader):
30 | pass
31 |
32 |
33 | @pytest.mark.xfail(reason='event missing', run=False)
34 | def test_transpose_at_start():
35 | read_spec([
36 | ('transpose', [EA, '']),
37 | ('accept', [''])])
38 |
39 |
40 | def test_cmd_instantiation_crash():
41 | spec = [
42 | ('reverse-history-isearch', ["(r-search `') "]),
43 | (('key', 'left'), ['']),
44 | ('accept', [''])
45 | ]
46 | read_spec(spec, HistoricalTestReader)
47 |
48 |
49 | def test_signal_failure(monkeypatch):
50 | import os
51 | import pty
52 | import signal
53 | from pyrepl.unix_console import UnixConsole
54 |
55 | def failing_signal(a, b):
56 | raise ValueError
57 |
58 | def really_failing_signal(a, b):
59 | raise AssertionError
60 |
61 | mfd, sfd = pty.openpty()
62 | try:
63 | c = UnixConsole(sfd, sfd)
64 | c.prepare()
65 | c.restore()
66 | monkeypatch.setattr(signal, 'signal', failing_signal)
67 | c.prepare()
68 | monkeypatch.setattr(signal, 'signal', really_failing_signal)
69 | c.restore()
70 | finally:
71 | os.close(mfd)
72 | os.close(sfd)
73 |
--------------------------------------------------------------------------------
/testing/test_curses.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pyrepl.curses import setupterm
3 | import pyrepl
4 |
5 |
6 | def test_setupterm(monkeypatch):
7 | assert setupterm(None, 0) is None
8 |
9 | with pytest.raises(
10 | pyrepl._minimal_curses.error,
11 | match=r"setupterm\(b?'term_does_not_exist', 0\) failed \(err=0\)",
12 | ):
13 | setupterm("term_does_not_exist", 0)
14 |
15 | monkeypatch.setenv('TERM', 'xterm')
16 | assert setupterm(None, 0) is None
17 |
18 | monkeypatch.delenv('TERM')
19 | with pytest.raises(
20 | pyrepl._minimal_curses.error,
21 | match=r"setupterm\(None, 0\) failed \(err=-1\)",
22 | ):
23 | setupterm(None, 0)
24 |
--------------------------------------------------------------------------------
/testing/test_functional.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2007 Michael Hudson-Doyle
2 | # Maciek Fijalkowski
3 | # License: MIT
4 | # some functional tests, to see if this is really working
5 |
6 | import os
7 | import signal
8 | import sys
9 | import textwrap
10 |
11 | import pytest
12 |
13 | try:
14 | import pexpect
15 | except ImportError as exc:
16 | pytest.skip("could not import pexpect: {}".format(exc),
17 | allow_module_level=True)
18 |
19 |
20 | @pytest.fixture
21 | def start_child():
22 | ret_childs = []
23 |
24 | def start_child_func(env_update=None):
25 | assert not ret_childs, "child started already"
26 |
27 | env = {k: v for k, v in os.environ.items() if k in (
28 | "TERM",
29 | )}
30 | if env_update:
31 | env.update(env_update)
32 | child = pexpect.spawn(sys.executable, timeout=5, env=env)
33 | if sys.version_info >= (3, ):
34 | child.logfile = sys.stdout.buffer
35 | else:
36 | child.logfile = sys.stdout
37 | child.expect_exact(">>> ")
38 | child.sendline('from pyrepl.python_reader import main')
39 | ret_childs.append(child)
40 | return child
41 |
42 | yield start_child_func
43 |
44 | assert ret_childs, "child was not started"
45 | child = ret_childs[0]
46 |
47 | child.sendeof()
48 | child.expect_exact(">>> ")
49 | # Verify there's no error, e.g. when signal.SIG_DFL would be called.
50 | before = child.before.decode()
51 | assert "Traceback (most recent call last):" not in before
52 | child.sendeof()
53 | assert child.wait() == 0
54 |
55 |
56 | @pytest.fixture
57 | def child(start_child):
58 | child = start_child()
59 | child.sendline("main()")
60 | return child
61 |
62 |
63 | def test_basic(child):
64 | child.expect_exact("->> ")
65 | child.sendline('a = 40 + 2')
66 | child.expect_exact("->> ")
67 | child.sendline('a')
68 | child.expect_exact('42')
69 | child.expect_exact("->> ")
70 |
71 |
72 | def test_sigwinch_default(child):
73 | child.expect_exact("->> ")
74 | os.kill(child.pid, signal.SIGWINCH)
75 |
76 |
77 | def test_sigwinch_forwarded(start_child, tmpdir):
78 | with open(str(tmpdir.join("initfile")), "w") as initfile:
79 | initfile.write(textwrap.dedent(
80 | """
81 | import signal
82 |
83 | called = []
84 |
85 | def custom_handler(signum, frame):
86 | called.append([signum, frame])
87 |
88 | signal.signal(signal.SIGWINCH, custom_handler)
89 |
90 | print("PYREPLSTARTUP called")
91 | """
92 | ))
93 |
94 | child = start_child(env_update={"PYREPLSTARTUP": initfile.name})
95 | child.sendline("main()")
96 | child.expect_exact("PYREPLSTARTUP called")
97 | child.expect_exact("->> ")
98 | os.kill(child.pid, signal.SIGWINCH)
99 | child.sendline('"called={}".format(len(called))')
100 | child.expect_exact("called=1")
101 |
--------------------------------------------------------------------------------
/testing/test_keymap.py:
--------------------------------------------------------------------------------
1 | from pyrepl.keymap import compile_keymap
2 |
3 |
4 | def test_compile_keymap():
5 | k = compile_keymap({
6 | b'a': 'test',
7 | b'bc': 'test2',
8 | })
9 |
10 | assert k == {b'a': 'test', b'b': {b'c': 'test2'}}
11 |
--------------------------------------------------------------------------------
/testing/test_readline.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pty
3 | import sys
4 |
5 | import pytest
6 | from pyrepl.readline import _ReadlineWrapper
7 |
8 |
9 | @pytest.fixture
10 | def readline_wrapper():
11 | master, slave = pty.openpty()
12 | return _ReadlineWrapper(slave, slave)
13 |
14 |
15 | if sys.version_info < (3, ):
16 | bytes_type = str
17 | unicode_type = unicode # noqa: F821
18 | else:
19 | bytes_type = bytes
20 | unicode_type = str
21 |
22 |
23 | def test_readline():
24 | master, slave = pty.openpty()
25 | readline_wrapper = _ReadlineWrapper(slave, slave)
26 | os.write(master, b'input\n')
27 |
28 | result = readline_wrapper.get_reader().readline()
29 | assert result == b'input'
30 | assert isinstance(result, bytes_type)
31 |
32 |
33 | def test_readline_returns_unicode():
34 | master, slave = pty.openpty()
35 | readline_wrapper = _ReadlineWrapper(slave, slave)
36 | os.write(master, b'input\n')
37 |
38 | result = readline_wrapper.get_reader().readline(returns_unicode=True)
39 | assert result == 'input'
40 | assert isinstance(result, unicode_type)
41 |
42 |
43 | def test_raw_input():
44 | master, slave = pty.openpty()
45 | readline_wrapper = _ReadlineWrapper(slave, slave)
46 | os.write(master, b'input\n')
47 |
48 | result = readline_wrapper.raw_input('prompt:')
49 | if sys.version_info < (3, ):
50 | assert result == b'input'
51 | assert isinstance(result, bytes_type)
52 | else:
53 | assert result == 'input'
54 | assert isinstance(result, unicode_type)
55 |
56 |
57 | def test_read_history_file(readline_wrapper, tmp_path):
58 | histfile = tmp_path / "history"
59 | histfile.touch()
60 |
61 | assert readline_wrapper.reader is None
62 |
63 | readline_wrapper.read_history_file(str(histfile))
64 | assert readline_wrapper.reader.history == []
65 |
66 | histfile.write_bytes(b"foo\nbar\n")
67 | readline_wrapper.read_history_file(str(histfile))
68 | assert readline_wrapper.reader.history == ["foo", "bar"]
69 |
70 |
71 | def test_write_history_file(readline_wrapper, tmp_path):
72 | histfile = tmp_path / "history"
73 |
74 | reader = readline_wrapper.get_reader()
75 | history = reader.history
76 | assert history == []
77 | history.extend(["foo", "bar"])
78 |
79 | readline_wrapper.write_history_file(str(histfile))
80 |
81 | with open(str(histfile), "r") as f:
82 | assert f.readlines() == ["foo\n", "bar\n"]
83 |
84 |
85 | def test_write_history_file_with_exception(readline_wrapper, tmp_path):
86 | """The history file should not get nuked on inner exceptions.
87 |
88 | This was the case with unicode decoding previously."""
89 | histfile = tmp_path / "history"
90 | histfile.write_bytes(b"foo\nbar\n")
91 |
92 | class BadEntryException(Exception):
93 | pass
94 |
95 | class BadEntry(object):
96 | @classmethod
97 | def replace(cls, *args):
98 | raise BadEntryException
99 |
100 | history = readline_wrapper.get_reader().history
101 | history.extend([BadEntry])
102 |
103 | with pytest.raises(BadEntryException):
104 | readline_wrapper.write_history_file(str(histfile))
105 |
106 | with open(str(histfile), "r") as f:
107 | assert f.readlines() == ["foo\n", "bar\n"]
108 |
--------------------------------------------------------------------------------
/testing/test_unix_reader.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | from pyrepl.unix_eventqueue import EncodedQueue, Event
3 |
4 |
5 | def test_simple():
6 | q = EncodedQueue({}, 'utf-8')
7 |
8 | a = u'\u1234'
9 | b = a.encode('utf-8')
10 | for c in b:
11 | q.push(c)
12 |
13 | event = q.get()
14 | assert q.get() is None
15 | assert event.data == a
16 | assert event.raw == b
17 |
18 |
19 | def test_propagate_escape():
20 | def send(keys):
21 | for c in keys:
22 | q.push(c)
23 |
24 | events = []
25 | while True:
26 | event = q.get()
27 | if event is None:
28 | break
29 | events.append(event)
30 | return events
31 |
32 | keymap = {
33 | b'\033': {b'U': 'up', b'D': 'down'},
34 | b'\xf7': 'backspace',
35 | }
36 | q = EncodedQueue(keymap, 'utf-8')
37 |
38 | # normal behaviour
39 | assert send(b'\033U') == [Event('key', 'up', bytearray(b'\033U'))]
40 | assert send(b'\xf7') == [Event('key', 'backspace', bytearray(b'\xf7'))]
41 |
42 | # escape propagation: simulate M-backspace
43 | events = send(b'\033\xf7')
44 | assert events == [
45 | Event('key', '\033', bytearray(b'\033')),
46 | Event('key', 'backspace', bytearray(b'\xf7'))
47 | ]
48 |
--------------------------------------------------------------------------------
/testing/test_wishes.py:
--------------------------------------------------------------------------------
1 | # Copyright 2000-2004 Michael Hudson-Doyle
2 | #
3 | # All Rights Reserved
4 | #
5 | #
6 | # Permission to use, copy, modify, and distribute this software and
7 | # its documentation for any purpose is hereby granted without fee,
8 | # provided that the above copyright notice appear in all copies and
9 | # that both that copyright notice and this permission notice appear in
10 | # supporting documentation.
11 | #
12 | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13 | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14 | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15 | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16 | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 |
20 | from .infrastructure import read_spec
21 |
22 | # this test case should contain as-verbatim-as-possible versions of
23 | # (applicable) feature requests
24 |
25 |
26 | def test_quoted_insert_repeat():
27 | read_spec([
28 | (('digit-arg', '3'), ['']),
29 | (('quoted-insert', None), ['']),
30 | (('key', '\033'), ['^[^[^[']),
31 | (('accept', None), None)])
32 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py{27,34,35,36,37,38,39,py,py3}, flake8
3 |
4 | [testenv]
5 | deps =
6 | pytest
7 | pexpect
8 | coverage: coverage
9 | commands =
10 | {env:_PYREPL_TOX_RUN_CMD:pytest} {posargs}
11 | coverage: coverage combine
12 | coverage: coverage report -m
13 | coverage: coverage xml
14 | passenv =
15 | PYTEST_ADDOPTS
16 | TERM
17 | setenv =
18 | coverage: _PYREPL_TOX_RUN_CMD=coverage run -m pytest
19 |
20 | [testenv:qa]
21 | deps =
22 | flake8
23 | mccabe
24 | commands = flake8 --max-complexity=10 setup.py pyrepl testing pythoni pythoni1
25 |
26 | [pytest]
27 | testpaths = testing
28 | addopts = -ra
29 | filterwarnings =
30 | error
31 |
32 | [coverage:run]
33 | include = pyrepl/*, testing/*
34 | parallel = 1
35 | branch = 1
36 |
37 | [coverage:paths]
38 | source = pyrepl/
39 | */lib/python*/site-packages/pyrepl/
40 | */pypy*/site-packages/pyrepl/
41 | *\Lib\site-packages\pyrepl\
42 |
--------------------------------------------------------------------------------