├── .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 | --------------------------------------------------------------------------------