Adding upstream version 3.0.16.
Signed-off-by: Daniel Baumann <daniel@debian.org>
38
.github/workflows/test.yaml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push: # any branch
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-ubuntu:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt remove python3-pip
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install . black isort mypy pytest readme_renderer
|
||||||
|
pip list
|
||||||
|
- name: Type Checker
|
||||||
|
run: |
|
||||||
|
mypy ptpython
|
||||||
|
isort -c --profile black ptpython examples setup.py
|
||||||
|
black --check ptpython examples setup.py
|
||||||
|
- name: Run Tests
|
||||||
|
run: |
|
||||||
|
./tests/run_tests.py
|
||||||
|
- name: Validate README.md
|
||||||
|
# Ensure that the README renders correctly (required for uploading to PyPI).
|
||||||
|
run: |
|
||||||
|
python -m readme_renderer README.rst > /dev/null
|
54
.gitignore
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
587
CHANGELOG
Normal file
|
@ -0,0 +1,587 @@
|
||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
3.0.16: 2020-02-11
|
||||||
|
------------------
|
||||||
|
|
||||||
|
(Commit 7f619e was missing in previous release.)
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Several fixes to the completion code:
|
||||||
|
* Give dictionary completions priority over path completions.
|
||||||
|
* Always call non-fuzzy completer after fuzzy completer to prevent that some
|
||||||
|
completions were missed out if the fuzzy completer doesn't find them.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.15: 2020-02-11
|
||||||
|
------------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- When pressing control-w, only delete characters until a punctuation.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Fix `AttributeError` during retrieval of signatures with type annotations.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.14: 2020-02-10
|
||||||
|
------------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Display of signature and completion drop down together.
|
||||||
|
- If `DictionaryCompleter` is enabled, also retrieve signatures when Jedi
|
||||||
|
fails, using the same logic.
|
||||||
|
- List function parameters first and private attributes at the end in the
|
||||||
|
completion menu.
|
||||||
|
- Cleanup of the completion code.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Handle exceptions raised when `repr()` is called.
|
||||||
|
- Fix leakage of `exc_info` from eval to exec call.
|
||||||
|
- Fix handling of `KeyboardInterrupt` in REPL during evaluation of `__repr__`.
|
||||||
|
- Fix style for signature toolbar.
|
||||||
|
- Hide signature when sidebar is visible.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.13: 2020-01-26
|
||||||
|
------------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Added 'print all' option to pager.
|
||||||
|
- Improve handling of indented code:
|
||||||
|
* Allow multiline input to be indented as a whole (we will unindent before
|
||||||
|
executing).
|
||||||
|
* Correctly visualize tabs (instead of ^I, which happens when pasted in
|
||||||
|
bracketed paste).
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Fix line ending bug in pager.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.12: 2020-01-24
|
||||||
|
------------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Expose a `get_ptpython` function in the global namespace, to get programmatic
|
||||||
|
access to the REPL.
|
||||||
|
- Expose `embed()` at the top level of the package. Make it possible to do
|
||||||
|
`from ptpython import embed`.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Properly handle exceptions when trying to access `__pt_repr__`.
|
||||||
|
- Properly handle `SystemExit`.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.11: 2020-01-20
|
||||||
|
------------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Add support for top-level await.
|
||||||
|
- Refactoring of event loop usage:
|
||||||
|
|
||||||
|
* The ptpython input UI will now run in a separate thread. This makes it
|
||||||
|
possible to properly embed ptpython in an asyncio application, without
|
||||||
|
having to deal with nested event loops (which asyncio does not support).
|
||||||
|
|
||||||
|
* The "eval" part doesn't anymore take place within a ptpython coroutine, so
|
||||||
|
it can spawn its own loop if needed. This also fixes `asyncio.run()` usage
|
||||||
|
in the REPL, which was broken before.
|
||||||
|
|
||||||
|
- Added syntax highlighting and autocompletion for !-style system commands.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Remove unexpected additional line after output.
|
||||||
|
- Fix system prompt. Accept !-style inputs again.
|
||||||
|
- Don't execute PYTHONSTARTUP when -i flag was given.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.10: 2020-01-13
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Do dictionary completion on Sequence and Mapping objects (from
|
||||||
|
collections.abc). Note that dictionary completion is still turned off by
|
||||||
|
default.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.9: 2020-01-10
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Allow replacing `PythonInput.completer` at runtime (useful for tools build on
|
||||||
|
top of ptpython).
|
||||||
|
- Show REPL title in pager.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.8: 2020-01-05
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Optional output formatting using Black.
|
||||||
|
- Optional pager for displaying outputs that don't fit on the screen.
|
||||||
|
- Added --light-bg and --dark-bg flags to automatically optimize the brightness
|
||||||
|
of the colors according to the terminal background.
|
||||||
|
- Addd `PTPYTHON_CONFIG_HOME` for explicitely setting the config directory.
|
||||||
|
- Show completion suffixes (like '(' for functions).
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Fix dictionary completion on Pandas objects.
|
||||||
|
- Stop using deprecated Jedi functions.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.7: 2020-09-25
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Option to show/hide private attributes during a completion
|
||||||
|
- Added `insert_blank_line_after_input` option similar to
|
||||||
|
`insert_blank_line_after_output`.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Fixed some formatting issues of `__pt_repr__`.
|
||||||
|
- Abbreviate completion meta information for dictionary completer if needed.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.6: 2020-09-23
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- (Experimental) support for `__pt_repr__` methods. If objects implement this
|
||||||
|
method, this will be used to print the result in the REPL instead of the
|
||||||
|
normal `__repr__`.
|
||||||
|
- Added py.typed file, to enable type checking for applications that are
|
||||||
|
embedding ptpython.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.5: 2020-08-10
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Handle bug in dictionary completion when numeric keys are used.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.4: 2020-08-10
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Allow leading whitespace before single line expressions.
|
||||||
|
- Show full syntax error in validator.
|
||||||
|
- Added `vi_start_in_navigation_mode` and `vi_keep_last_used_mode` options.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Improved dictionary completion: handle keys that contain spaces and don't
|
||||||
|
recognize numbers as variable names.
|
||||||
|
- Fix in exit confirmation.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.3: 2020-07-10
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Sort attribute names for `DictionaryCompleter` and move underscored
|
||||||
|
attributes to the end.
|
||||||
|
- Handle unhandled exceptions in `get_compiler_flags`.
|
||||||
|
- Improved `run_async` code.
|
||||||
|
- Fix --version parameter.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.2: 2020-04-14
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Improved custom dictionary completion:
|
||||||
|
* Also complete list indexes.
|
||||||
|
* Also complete attributes after doing a dictionary lookup.
|
||||||
|
* Also complete iterators in a for-loop.
|
||||||
|
- Added a 'title' option, so that applications embedding ptpython can set a
|
||||||
|
title in the status bar.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.1: 2020-02-24
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
- Fix backwards-compatibility of the `run_config` function. (used by
|
||||||
|
django-extensions).
|
||||||
|
- Fix input mode in status bar for block selection.
|
||||||
|
|
||||||
|
|
||||||
|
3.0.0: 2020-01-29
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 3.0.
|
||||||
|
Requires at least Python 3.6.
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Uses XDG base directory specification.
|
||||||
|
|
||||||
|
|
||||||
|
2.0.5: 2019-10-09
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Added dictionary completer (off by default).
|
||||||
|
- Added fuzzy completion (off by default).
|
||||||
|
- Highlight keywords in completion dropdown menu.
|
||||||
|
- Enable universal wheels.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Fixed embedding repl as asyncio coroutine.
|
||||||
|
- Fixed patching stdout in embedded repl.
|
||||||
|
- Fixed ResourceWarning in setup.py.
|
||||||
|
|
||||||
|
|
||||||
|
2.0.4: 2018-10-30
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
- Fixed ptipython.
|
||||||
|
- Fixed config: setting of color depth.
|
||||||
|
- Fixed auto-suggest key bindings.
|
||||||
|
- Fixed Control-D key binding for exiting REPL when (confirm_exit=False).
|
||||||
|
- Correctly focus/unfocus sidebar.
|
||||||
|
- Fixed open_in_editor and suspend key bindings.
|
||||||
|
|
||||||
|
|
||||||
|
2.0.3: 2018-10-12
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
- Allow changing the min/max brightness.
|
||||||
|
- Some changes for compatibility with the latest prompt_toolkit.
|
||||||
|
|
||||||
|
2.0.2: 2018-09-30
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Don't crash the history browser when there was no history.
|
||||||
|
- Set last exception in the sys module, when an exception was raised.
|
||||||
|
- Require prompt_toolkit 2.0.5.
|
||||||
|
|
||||||
|
|
||||||
|
2.0.1: 2018-09-30
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 2.0.x.
|
||||||
|
|
||||||
|
|
||||||
|
0.36: 2016-10-16
|
||||||
|
----------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Support for editing in Vi block mode. (Only enabled for
|
||||||
|
prompt_toolkit>=1.0.8.)
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Handle two Jedi crashes. (GitHub ptpython issues #136 and #91.)
|
||||||
|
|
||||||
|
0.35: 2016-07-19
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Fix in completer. Don't hang when pasting a long string with many
|
||||||
|
backslashes.
|
||||||
|
- Fix Python2 bug: crash when filenames contain non-ascii characters.
|
||||||
|
- Added `pt[i]pythonX` and `pt[i]pythonX.X` commands.
|
||||||
|
- Compatibility with IPython 5.0.
|
||||||
|
|
||||||
|
|
||||||
|
0.34: 2016-05-06
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Bugfix in ptipython: reset input buffer before every read in run().
|
||||||
|
|
||||||
|
|
||||||
|
0.33: 2016-05-05
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 1.0.0
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
- Unindent after typing 'pass'.
|
||||||
|
- Make it configurable whether or not a blank line has to be inserted after the output.
|
||||||
|
|
||||||
|
|
||||||
|
0.32: 2016-03-29
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Fixed bug when PYTHONSTARTUP was not found.
|
||||||
|
- Support $PYTHONSTARTUP for ptipython.
|
||||||
|
|
||||||
|
|
||||||
|
0.31: 2016-03-14
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.60
|
||||||
|
|
||||||
|
|
||||||
|
0.30: 2016-02-27
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.59
|
||||||
|
|
||||||
|
|
||||||
|
0.29: 2016-02-24
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.58
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Improved mouse support
|
||||||
|
|
||||||
|
|
||||||
|
0.28: 2016-01-04
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.57
|
||||||
|
|
||||||
|
|
||||||
|
0.27: 2016-01-03
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.56
|
||||||
|
|
||||||
|
|
||||||
|
0.26: 2016-01-03
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.55
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Handle several bugs in Jedi.
|
||||||
|
- Correctly handle sys.argv when pt(i)python is started with --interactive.
|
||||||
|
- Support for 24bit true color.
|
||||||
|
- Take compiler flags into account for ptipython.
|
||||||
|
|
||||||
|
|
||||||
|
0.25: 2015-10-29
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.54
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Consider input multiline when there's a colon at the end of the line.
|
||||||
|
- Handle bug in Jedi.
|
||||||
|
- Enable search bindings in history browser.
|
||||||
|
|
||||||
|
|
||||||
|
0.24: 2015-09-24
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.52
|
||||||
|
|
||||||
|
|
||||||
|
0.23: 2015-09-24
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.51
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Mouse support
|
||||||
|
- Fish style auto suggestion.
|
||||||
|
- Optionally disabling of line wraps.
|
||||||
|
- Use Python3Lexer for Python 3.
|
||||||
|
|
||||||
|
|
||||||
|
0.22: 2015-09-06
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.50
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Correctly accept file parameter in the print function of
|
||||||
|
asyncssh_repl.ReplSSHServerSession.
|
||||||
|
- Create config directory if it doesn't exist yet (For IPython entry point.)
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Implementation of history-selection: a tool to select lines from the history.
|
||||||
|
- Make exit message configurable.
|
||||||
|
- Improve start-up time: Lazy load completer grammar and lazy-import Jedi.
|
||||||
|
- Make multi-column the default completion visualisation.
|
||||||
|
- Implementation of a custom prompts. In_tokens and out_tokens can be
|
||||||
|
customized.
|
||||||
|
- Made an option to show/hide highlighting for matching parenthesis.
|
||||||
|
- Some styling improvements.
|
||||||
|
|
||||||
|
|
||||||
|
0.21: 2015-08-08
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.46
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Correctly add current directory to sys.path.
|
||||||
|
- Only show search highlighting when the search is the current input buffer.
|
||||||
|
- Styling fix.
|
||||||
|
|
||||||
|
|
||||||
|
0.20: 2015-07-30
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.45
|
||||||
|
|
||||||
|
|
||||||
|
0.19: 2015-07-30
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.44
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Added --interactive option for ptipython.
|
||||||
|
- A few style improvements.
|
||||||
|
|
||||||
|
|
||||||
|
0.18: 2015-07-15
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Python 2.6 compatibility.
|
||||||
|
|
||||||
|
|
||||||
|
0.17: 2015-07-15
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.43
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Integration with Tk eventloop. (This makes turtle and other Tk stuff work
|
||||||
|
again from the REPL.)
|
||||||
|
- Multi column completion visualisation.
|
||||||
|
|
||||||
|
|
||||||
|
0.16: 2015-06-25
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.42
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Workaround for Jedi bug. (Signatures of functions with keyword-only arguments.)
|
||||||
|
- Correctly show traceback on Python 3.
|
||||||
|
- Better styling of the options sidebar.
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Exit REPL when input starts with Control-Z.
|
||||||
|
- Set terminal title.
|
||||||
|
- Display help text in options sidebar.
|
||||||
|
- Better colorscheme for Windows.
|
||||||
|
|
||||||
|
|
||||||
|
0.15: 2015-06-20
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.41
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Correct tokens for IPython prompt.
|
||||||
|
- Syntax fix in asyncssh_repl.
|
||||||
|
|
||||||
|
|
||||||
|
0.14: 2015-06-16
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
- Correct dependency for prompt_toolkit.
|
||||||
|
|
||||||
|
0.13: 2015-06-15
|
||||||
|
---------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Upgrade to prompt_toolkit 0.40
|
||||||
|
- Options sidebar.
|
||||||
|
- Custom color schemes.
|
||||||
|
- Syntax highlighting of the output.
|
||||||
|
- Input validation can now be turned off.
|
||||||
|
- Determine compiler flags dynamically. (Fixes importing unicode_literals).
|
||||||
|
- Exit confirmation and dialog.
|
||||||
|
- Autocompletion of IPython %cat command.
|
||||||
|
- Correctly render tracebacks on Windows.
|
||||||
|
|
||||||
|
0.12: 2015-06-04
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt_toolkit 0.39
|
||||||
|
|
||||||
|
0.11: 2015-05-31
|
||||||
|
---------------
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Upgrade to prompt-toolkit 0.38.
|
||||||
|
- Upgrade to Jedi 0.9.0
|
||||||
|
- Fixed default globals for repl (correct __name, __builtins__, etc...)
|
||||||
|
- Display deprecation warnings in the REPL.
|
||||||
|
- Added configuration support.
|
||||||
|
- Added asyncio-ssh-python-embed example.
|
||||||
|
|
||||||
|
|
||||||
|
0.10: 2015-05-11
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt-toolkit 0.37.
|
||||||
|
|
||||||
|
|
||||||
|
0.9: 2015-05-07
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Upgrade to prompt-toolkit 0.35.
|
||||||
|
|
||||||
|
|
||||||
|
0.8: 2015-04-26
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
- eval() doesn't run using unicode_literals anymore.
|
||||||
|
- Upgrade to prompt-toolkit 0.34.
|
||||||
|
|
||||||
|
|
||||||
|
0.7: 2015-04-25
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
- Upgrade to prompt-toolkit 0.33.
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Added complete_while_typing option.
|
||||||
|
|
||||||
|
|
||||||
|
0.6: 2015-04-22
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
- Upgrade to prompt-toolkit 0.32 which has many new features.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Removal of tab pages + clean up.
|
||||||
|
- Pressing enter twice will now always automatically execute the input.
|
||||||
|
- Improved Control-D key binding.
|
||||||
|
- Hide docstring by default.
|
||||||
|
|
||||||
|
|
||||||
|
0.5: 2015-01-30
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
- Tab autocompletion on first line.
|
||||||
|
- Upgrade to prompt-toolkit 0.31
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Simple autocompletion for IPython magics.
|
||||||
|
|
||||||
|
0.4: 2015-01-26
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
- Upgrade to prompt-toolkit 0.30
|
||||||
|
|
||||||
|
0.3: 2015-01-25
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
- Upgrade to prompt-toolkit 0.28
|
||||||
|
|
||||||
|
0.2: 2015-01-25
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Moved ptpython code from prompt-toolkit inside this repository.
|
||||||
|
|
||||||
|
0.1: 2014-09-29
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Initial ptpython version. (Source code was still in the
|
||||||
|
prompt-toolkit repository itself.)
|
27
LICENSE
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2015, Jonathan Slenders
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the {organization} nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
3
MANIFEST.in
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
include *rst LICENSE CHANGELOG MANIFEST.in
|
||||||
|
recursive-include examples *.py
|
||||||
|
prune examples/sample?/build
|
245
README.rst
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
ptpython
|
||||||
|
========
|
||||||
|
|
||||||
|
|Build Status| |PyPI| |License|
|
||||||
|
|
||||||
|
*A better Python REPL*
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pip install ptpython
|
||||||
|
|
||||||
|
.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/example1.png
|
||||||
|
|
||||||
|
Ptpython is an advanced Python REPL. It should work on all
|
||||||
|
Python versions from 2.6 up to 3.9 and work cross platform (Linux,
|
||||||
|
BSD, OS X and Windows).
|
||||||
|
|
||||||
|
Note: this version of ptpython requires at least Python 3.6. Install ptpython
|
||||||
|
2.0.5 for older Python versions.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
************
|
||||||
|
|
||||||
|
Install it using pip:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pip install ptpython
|
||||||
|
|
||||||
|
Start it by typing ``ptpython``.
|
||||||
|
|
||||||
|
|
||||||
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
|
- Syntax highlighting.
|
||||||
|
- Multiline editing (the up arrow works).
|
||||||
|
- Autocompletion.
|
||||||
|
- Mouse support. [1]
|
||||||
|
- Support for color schemes.
|
||||||
|
- Support for `bracketed paste <https://cirw.in/blog/bracketed-paste>`_ [2].
|
||||||
|
- Both Vi and Emacs key bindings.
|
||||||
|
- Support for double width (Chinese) characters.
|
||||||
|
- ... and many other things.
|
||||||
|
|
||||||
|
|
||||||
|
[1] Disabled by default. (Enable in the menu.)
|
||||||
|
|
||||||
|
[2] If the terminal supports it (most terminals do), this allows pasting
|
||||||
|
without going into paste mode. It will keep the indentation.
|
||||||
|
|
||||||
|
__pt_repr__: A nicer repr with colors
|
||||||
|
*************************************
|
||||||
|
|
||||||
|
When classes implement a ``__pt_repr__`` method, this will be used instead of
|
||||||
|
``__repr__`` for printing. Any `prompt_toolkit "formatted text"
|
||||||
|
<https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html>`_
|
||||||
|
can be returned from here. In order to avoid writing a ``__repr__`` as well,
|
||||||
|
the ``ptpython.utils.ptrepr_to_repr`` decorator can be applied. For instance:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from ptpython.utils import ptrepr_to_repr
|
||||||
|
from prompt_toolkit.formatted_text import HTML
|
||||||
|
|
||||||
|
@ptrepr_to_repr
|
||||||
|
class MyClass:
|
||||||
|
def __pt_repr__(self):
|
||||||
|
return HTML('<yellow>Hello world!</yellow>')
|
||||||
|
|
||||||
|
More screenshots
|
||||||
|
****************
|
||||||
|
|
||||||
|
The configuration menu:
|
||||||
|
|
||||||
|
.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ptpython-menu.png
|
||||||
|
|
||||||
|
The history page and its help:
|
||||||
|
|
||||||
|
.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ptpython-history-help.png
|
||||||
|
|
||||||
|
Autocompletion:
|
||||||
|
|
||||||
|
.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/file-completion.png
|
||||||
|
|
||||||
|
|
||||||
|
Embedding the REPL
|
||||||
|
******************
|
||||||
|
|
||||||
|
Embedding the REPL in any Python application is easy:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from ptpython.repl import embed
|
||||||
|
embed(globals(), locals())
|
||||||
|
|
||||||
|
You can make ptpython your default Python REPL by creating a `PYTHONSTARTUP file
|
||||||
|
<https://docs.python.org/3/tutorial/appendix.html#the-interactive-startup-file>`_ containing code
|
||||||
|
like this:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
from ptpython.repl import embed
|
||||||
|
except ImportError:
|
||||||
|
print("ptpython is not available: falling back to standard prompt")
|
||||||
|
else:
|
||||||
|
sys.exit(embed(globals(), locals()))
|
||||||
|
|
||||||
|
|
||||||
|
Multiline editing
|
||||||
|
*****************
|
||||||
|
|
||||||
|
Multi-line editing mode will automatically turn on when you press enter after a
|
||||||
|
colon.
|
||||||
|
|
||||||
|
To execute the input in multi-line mode, you can either press ``Alt+Enter``, or
|
||||||
|
``Esc`` followed by ``Enter``. (If you want the first to work in the OS X
|
||||||
|
terminal, you have to check the "Use option as meta key" checkbox in your
|
||||||
|
terminal settings. For iTerm2, you have to check "Left option acts as +Esc" in
|
||||||
|
the options.)
|
||||||
|
|
||||||
|
.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/multiline.png
|
||||||
|
|
||||||
|
|
||||||
|
Syntax validation
|
||||||
|
*****************
|
||||||
|
|
||||||
|
Before execution, ``ptpython`` will see whether the input is syntactically
|
||||||
|
correct Python code. If not, it will show a warning, and move the cursor to the
|
||||||
|
error.
|
||||||
|
|
||||||
|
.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/validation.png
|
||||||
|
|
||||||
|
|
||||||
|
Additional features
|
||||||
|
*******************
|
||||||
|
|
||||||
|
Running system commands: Press ``Meta-!`` in Emacs mode or just ``!`` in Vi
|
||||||
|
navigation mode to see the "Shell command" prompt. There you can enter system
|
||||||
|
commands without leaving the REPL.
|
||||||
|
|
||||||
|
Selecting text: Press ``Control+Space`` in Emacs mode or ``V`` (major V) in Vi
|
||||||
|
navigation mode.
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
*************
|
||||||
|
|
||||||
|
It is possible to create a ``config.py`` file to customize configuration.
|
||||||
|
ptpython will look in an appropriate platform-specific directory via `appdirs
|
||||||
|
<https://pypi.org/project/appdirs/>`. See the ``appdirs`` documentation for the
|
||||||
|
precise location for your platform. A ``PTPYTHON_CONFIG_HOME`` environment
|
||||||
|
variable, if set, can also be used to explicitly override where configuration
|
||||||
|
is looked for.
|
||||||
|
|
||||||
|
Have a look at this example to see what is possible:
|
||||||
|
`config.py <https://github.com/jonathanslenders/ptpython/blob/master/examples/ptpython_config/config.py>`_
|
||||||
|
|
||||||
|
|
||||||
|
IPython support
|
||||||
|
***************
|
||||||
|
|
||||||
|
Run ``ptipython`` (prompt_toolkit - IPython), to get a nice interactive shell
|
||||||
|
with all the power that IPython has to offer, like magic functions and shell
|
||||||
|
integration. Make sure that IPython has been installed. (``pip install
|
||||||
|
ipython``)
|
||||||
|
|
||||||
|
.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ipython.png
|
||||||
|
|
||||||
|
This is also available for embedding:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from ptpython.ipython.repl import embed
|
||||||
|
embed(globals(), locals())
|
||||||
|
|
||||||
|
|
||||||
|
Django support
|
||||||
|
**************
|
||||||
|
|
||||||
|
`django-extensions <https://github.com/django-extensions/django-extensions>`_
|
||||||
|
has a ``shell_plus`` management command. When ``ptpython`` has been installed,
|
||||||
|
it will by default use ``ptpython`` or ``ptipython``.
|
||||||
|
|
||||||
|
|
||||||
|
PDB
|
||||||
|
***
|
||||||
|
|
||||||
|
There is an experimental PDB replacement: `ptpdb
|
||||||
|
<https://github.com/jonathanslenders/ptpdb>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Windows support
|
||||||
|
***************
|
||||||
|
|
||||||
|
``prompt_toolkit`` and ``ptpython`` works better on Linux and OS X than on
|
||||||
|
Windows. Some things might not work, but it is usable:
|
||||||
|
|
||||||
|
.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/windows.png
|
||||||
|
|
||||||
|
|
||||||
|
FAQ
|
||||||
|
***
|
||||||
|
|
||||||
|
**Q**: The ``Ctrl-S`` forward search doesn't work and freezes my terminal.
|
||||||
|
|
||||||
|
**A**: Try to run ``stty -ixon`` in your terminal to disable flow control.
|
||||||
|
|
||||||
|
**Q**: The ``Meta``-key doesn't work.
|
||||||
|
|
||||||
|
**A**: For some terminals you have to enable the Alt-key to act as meta key, but you
|
||||||
|
can also type ``Escape`` before any key instead.
|
||||||
|
|
||||||
|
|
||||||
|
Alternatives
|
||||||
|
************
|
||||||
|
|
||||||
|
- `BPython <http://bpython-interpreter.org/downloads.html>`_
|
||||||
|
- `IPython <https://ipython.org/>`_
|
||||||
|
|
||||||
|
If you find another alternative, you can create an issue and we'll list it
|
||||||
|
here. If you find a nice feature somewhere that is missing in ``ptpython``,
|
||||||
|
also create a GitHub issue and maybe we'll implement it.
|
||||||
|
|
||||||
|
|
||||||
|
Special thanks to
|
||||||
|
*****************
|
||||||
|
|
||||||
|
- `Pygments <http://pygments.org/>`_: Syntax highlighter.
|
||||||
|
- `Jedi <http://jedi.jedidjah.ch/en/latest/>`_: Autocompletion library.
|
||||||
|
- `wcwidth <https://github.com/jquast/wcwidth>`_: Determine columns needed for a wide characters.
|
||||||
|
- `prompt_toolkit <http://github.com/jonathanslenders/python-prompt-toolkit>`_ for the interface.
|
||||||
|
|
||||||
|
.. |Build Status| image:: https://api.travis-ci.org/prompt-toolkit/ptpython.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/prompt-toolkit/ptpython#
|
||||||
|
|
||||||
|
.. |License| image:: https://img.shields.io/github/license/prompt-toolkit/ptpython.svg
|
||||||
|
:target: https://github.com/prompt-toolkit/ptpython/blob/master/LICENSE
|
||||||
|
|
||||||
|
.. |PyPI| image:: https://pypip.in/version/ptpython/badge.svg
|
||||||
|
:target: https://pypi.python.org/pypi/ptpython/
|
||||||
|
:alt: Latest Version
|
91
docs/concurrency-challenges.rst
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
|
||||||
|
Concurrency-related challenges regarding embedding of ptpython in asyncio code
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
Things we want to be possible
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
- Embed blocking ptpython in non-asyncio code (the normal use case).
|
||||||
|
- Embed blocking ptpython in asyncio code (the event loop will block).
|
||||||
|
- Embed awaitable ptpython in asyncio code (the loop will continue).
|
||||||
|
- React to resize events (SIGWINCH).
|
||||||
|
- Support top-level await.
|
||||||
|
- Be able to patch_stdout, so that logging messages from another thread will be
|
||||||
|
printed above the prompt.
|
||||||
|
- It should be possible to handle `KeyboardInterrupt` during evaluation of an
|
||||||
|
expression.
|
||||||
|
- The "eval" should happen in the same thread from where embed() was called.
|
||||||
|
|
||||||
|
|
||||||
|
Limitations of asyncio/python
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
- We can only listen to SIGWINCH signal (resize) events in the main thread.
|
||||||
|
|
||||||
|
- Usage of Control-C for triggering a `KeyboardInterrupt` only works for code
|
||||||
|
running in the main thread. (And only if the terminal was not set in raw
|
||||||
|
input mode).
|
||||||
|
|
||||||
|
- Spawning a new event loop from within a coroutine, that's being executed in
|
||||||
|
an existing event loop is not allowed in asyncio. We can however spawn any
|
||||||
|
event loop in a separate thread, and wait for that thread to finish.
|
||||||
|
|
||||||
|
- For patch_stdout to work correctly, we have to know what prompt_toolkit
|
||||||
|
application is running on the terminal, then tell that application to print
|
||||||
|
the output and redraw itself.
|
||||||
|
|
||||||
|
|
||||||
|
Additional challenges for IPython
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
IPython supports integration of 3rd party event loops (for various GUI
|
||||||
|
toolkits). These event loops are supposed to continue running while we are
|
||||||
|
prompting for input. In an asyncio environment, it means that there are
|
||||||
|
situations where we have to juggle three event loops:
|
||||||
|
|
||||||
|
- The asyncio loop in which the code was embedded.
|
||||||
|
- The asyncio loop from the prompt.
|
||||||
|
- The 3rd party GUI loop.
|
||||||
|
|
||||||
|
Approach taken in ptpython 3.0.11
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
For ptpython, the most reliable solution is to to run the prompt_toolkit input
|
||||||
|
prompt in a separate background thread. This way it can use its own asyncio
|
||||||
|
event loop without ever having to interfere with whatever runs in the main
|
||||||
|
thread.
|
||||||
|
|
||||||
|
Then, depending on how we embed, we do the following:
|
||||||
|
When a normal blocking embed is used:
|
||||||
|
* We start the UI thread for the input, and do a blocking wait on
|
||||||
|
`thread.join()` here.
|
||||||
|
* The "eval" happens in the main thread.
|
||||||
|
* The "print" happens also in the main thread. Unless a pager is shown,
|
||||||
|
which is also a prompt_toolkit application, then the pager itself is runs
|
||||||
|
also in another thread, similar to the way we do the input.
|
||||||
|
|
||||||
|
When an awaitable embed is used, for embedding in a coroutine, but having the
|
||||||
|
event loop continue:
|
||||||
|
* We run the input method from the blocking embed in an asyncio executor
|
||||||
|
and do an `await loop.run_in_excecutor(...)`.
|
||||||
|
* The "eval" happens again in the main thread.
|
||||||
|
* "print" is also similar, except that the pager code (if used) runs in an
|
||||||
|
executor too.
|
||||||
|
|
||||||
|
This means that the prompt_toolkit application code will always run in a
|
||||||
|
different thread. It means it won't be able to respond to SIGWINCH (window
|
||||||
|
resize events), but prompt_toolkit's 3.0.11 has now terminal size polling which
|
||||||
|
solves this.
|
||||||
|
|
||||||
|
Control-C key presses won't interrupt the main thread while we wait for input,
|
||||||
|
because the prompt_toolkit application turns the terminal in raw mode, while
|
||||||
|
it's reading, which means that it will receive control-c key presses as raw
|
||||||
|
data in its own thread.
|
||||||
|
|
||||||
|
Top-level await works in most situations as expected.
|
||||||
|
- If a blocking embed is used. We execute ``loop.run_until_complete(code)``.
|
||||||
|
This assumes that the blocking embed is not used in a coroutine of a running
|
||||||
|
event loop, otherwise, this will attempt to start a nested event loop, which
|
||||||
|
asyncio does not support. In that case we will get an exception.
|
||||||
|
- If an awaitable embed is used. We literally execute ``await code``. This will
|
||||||
|
integrate nicely in the current event loop.
|
BIN
docs/images/example1.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/images/file-completion.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/images/ipython.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/images/multiline.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
docs/images/ptpython-history-help.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
docs/images/ptpython-menu.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
docs/images/ptpython.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/images/validation.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
docs/images/windows.png
Normal file
After Width: | Height: | Size: 18 KiB |
56
examples/asyncio-python-embed.py
Executable file
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
(Python >3.3)
|
||||||
|
|
||||||
|
This is an example of how we can embed a Python REPL into an asyncio
|
||||||
|
application. In this example, we have one coroutine that runs in the
|
||||||
|
background, prints some output and alters a global state. The REPL, which runs
|
||||||
|
inside another coroutine can access and change this global state, interacting
|
||||||
|
with the running asyncio application.
|
||||||
|
The ``patch_stdout`` option makes sure that when another coroutine is writing
|
||||||
|
to stdout, it won't break the input line, but instead writes nicely above the
|
||||||
|
prompt.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from ptpython.repl import embed
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
counter = [0]
|
||||||
|
|
||||||
|
|
||||||
|
async def print_counter():
|
||||||
|
"""
|
||||||
|
Coroutine that prints counters and saves it in a global variable.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
print("Counter: %i" % counter[0])
|
||||||
|
counter[0] += 1
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
|
async def interactive_shell():
|
||||||
|
"""
|
||||||
|
Coroutine that starts a Python REPL from which we can access the global
|
||||||
|
counter variable.
|
||||||
|
"""
|
||||||
|
print(
|
||||||
|
'You should be able to read and update the "counter[0]" variable from this shell.'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True)
|
||||||
|
except EOFError:
|
||||||
|
# Stop the loop when quitting the repl. (Ctrl-D press.)
|
||||||
|
loop.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
asyncio.ensure_future(print_counter())
|
||||||
|
asyncio.ensure_future(interactive_shell())
|
||||||
|
|
||||||
|
loop.run_forever()
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
62
examples/asyncio-ssh-python-embed.py
Executable file
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Example of running the Python REPL through an SSH connection in an asyncio process.
|
||||||
|
This requires Python 3, asyncio and asyncssh.
|
||||||
|
|
||||||
|
Run this example and then SSH to localhost, port 8222.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import asyncssh
|
||||||
|
|
||||||
|
from ptpython.contrib.asyncssh_repl import ReplSSHServerSession
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
class MySSHServer(asyncssh.SSHServer):
|
||||||
|
"""
|
||||||
|
Server without authentication, running `ReplSSHServerSession`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_namespace):
|
||||||
|
self.get_namespace = get_namespace
|
||||||
|
|
||||||
|
def begin_auth(self, username):
|
||||||
|
# No authentication.
|
||||||
|
return False
|
||||||
|
|
||||||
|
def session_requested(self):
|
||||||
|
return ReplSSHServerSession(self.get_namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def main(port=8222):
|
||||||
|
"""
|
||||||
|
Example that starts the REPL through an SSH server.
|
||||||
|
"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
# Namespace exposed in the REPL.
|
||||||
|
environ = {"hello": "world"}
|
||||||
|
|
||||||
|
# Start SSH server.
|
||||||
|
def create_server():
|
||||||
|
return MySSHServer(lambda: environ)
|
||||||
|
|
||||||
|
print("Listening on :%i" % port)
|
||||||
|
print('To connect, do "ssh localhost -p %i"' % port)
|
||||||
|
|
||||||
|
loop.run_until_complete(
|
||||||
|
asyncssh.create_server(
|
||||||
|
create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run eventloop.
|
||||||
|
loop.run_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
198
examples/ptpython_config/config.py
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
"""
|
||||||
|
Configuration example for ``ptpython``.
|
||||||
|
|
||||||
|
Copy this file to $XDG_CONFIG_HOME/ptpython/config.py
|
||||||
|
On Linux, this is: ~/.config/ptpython/config.py
|
||||||
|
"""
|
||||||
|
from prompt_toolkit.filters import ViInsertMode
|
||||||
|
from prompt_toolkit.key_binding.key_processor import KeyPress
|
||||||
|
from prompt_toolkit.keys import Keys
|
||||||
|
from prompt_toolkit.styles import Style
|
||||||
|
|
||||||
|
from ptpython.layout import CompletionVisualisation
|
||||||
|
|
||||||
|
__all__ = ["configure"]
|
||||||
|
|
||||||
|
|
||||||
|
def configure(repl):
|
||||||
|
"""
|
||||||
|
Configuration method. This is called during the start-up of ptpython.
|
||||||
|
|
||||||
|
:param repl: `PythonRepl` instance.
|
||||||
|
"""
|
||||||
|
# Show function signature (bool).
|
||||||
|
repl.show_signature = True
|
||||||
|
|
||||||
|
# Show docstring (bool).
|
||||||
|
repl.show_docstring = False
|
||||||
|
|
||||||
|
# Show the "[Meta+Enter] Execute" message when pressing [Enter] only
|
||||||
|
# inserts a newline instead of executing the code.
|
||||||
|
repl.show_meta_enter_message = True
|
||||||
|
|
||||||
|
# Show completions. (NONE, POP_UP, MULTI_COLUMN or TOOLBAR)
|
||||||
|
repl.completion_visualisation = CompletionVisualisation.POP_UP
|
||||||
|
|
||||||
|
# When CompletionVisualisation.POP_UP has been chosen, use this
|
||||||
|
# scroll_offset in the completion menu.
|
||||||
|
repl.completion_menu_scroll_offset = 0
|
||||||
|
|
||||||
|
# Show line numbers (when the input contains multiple lines.)
|
||||||
|
repl.show_line_numbers = False
|
||||||
|
|
||||||
|
# Show status bar.
|
||||||
|
repl.show_status_bar = True
|
||||||
|
|
||||||
|
# When the sidebar is visible, also show the help text.
|
||||||
|
repl.show_sidebar_help = True
|
||||||
|
|
||||||
|
# Swap light/dark colors on or off
|
||||||
|
repl.swap_light_and_dark = False
|
||||||
|
|
||||||
|
# Highlight matching parethesis.
|
||||||
|
repl.highlight_matching_parenthesis = True
|
||||||
|
|
||||||
|
# Line wrapping. (Instead of horizontal scrolling.)
|
||||||
|
repl.wrap_lines = True
|
||||||
|
|
||||||
|
# Mouse support.
|
||||||
|
repl.enable_mouse_support = True
|
||||||
|
|
||||||
|
# Complete while typing. (Don't require tab before the
|
||||||
|
# completion menu is shown.)
|
||||||
|
repl.complete_while_typing = True
|
||||||
|
|
||||||
|
# Fuzzy and dictionary completion.
|
||||||
|
repl.enable_fuzzy_completion = False
|
||||||
|
repl.enable_dictionary_completion = False
|
||||||
|
|
||||||
|
# Vi mode.
|
||||||
|
repl.vi_mode = False
|
||||||
|
|
||||||
|
# Paste mode. (When True, don't insert whitespace after new line.)
|
||||||
|
repl.paste_mode = False
|
||||||
|
|
||||||
|
# Use the classic prompt. (Display '>>>' instead of 'In [1]'.)
|
||||||
|
repl.prompt_style = "classic" # 'classic' or 'ipython'
|
||||||
|
|
||||||
|
# Don't insert a blank line after the output.
|
||||||
|
repl.insert_blank_line_after_output = False
|
||||||
|
|
||||||
|
# History Search.
|
||||||
|
# When True, going back in history will filter the history on the records
|
||||||
|
# starting with the current input. (Like readline.)
|
||||||
|
# Note: When enable, please disable the `complete_while_typing` option.
|
||||||
|
# otherwise, when there is a completion available, the arrows will
|
||||||
|
# browse through the available completions instead of the history.
|
||||||
|
repl.enable_history_search = False
|
||||||
|
|
||||||
|
# Enable auto suggestions. (Pressing right arrow will complete the input,
|
||||||
|
# based on the history.)
|
||||||
|
repl.enable_auto_suggest = False
|
||||||
|
|
||||||
|
# Enable open-in-editor. Pressing C-x C-e in emacs mode or 'v' in
|
||||||
|
# Vi navigation mode will open the input in the current editor.
|
||||||
|
repl.enable_open_in_editor = True
|
||||||
|
|
||||||
|
# Enable system prompt. Pressing meta-! will display the system prompt.
|
||||||
|
# Also enables Control-Z suspend.
|
||||||
|
repl.enable_system_bindings = True
|
||||||
|
|
||||||
|
# Ask for confirmation on exit.
|
||||||
|
repl.confirm_exit = True
|
||||||
|
|
||||||
|
# Enable input validation. (Don't try to execute when the input contains
|
||||||
|
# syntax errors.)
|
||||||
|
repl.enable_input_validation = True
|
||||||
|
|
||||||
|
# Use this colorscheme for the code.
|
||||||
|
repl.use_code_colorscheme("default")
|
||||||
|
# repl.use_code_colorscheme("pastie")
|
||||||
|
|
||||||
|
# Set color depth (keep in mind that not all terminals support true color).
|
||||||
|
|
||||||
|
# repl.color_depth = "DEPTH_1_BIT" # Monochrome.
|
||||||
|
# repl.color_depth = "DEPTH_4_BIT" # ANSI colors only.
|
||||||
|
repl.color_depth = "DEPTH_8_BIT" # The default, 256 colors.
|
||||||
|
# repl.color_depth = "DEPTH_24_BIT" # True color.
|
||||||
|
|
||||||
|
# Min/max brightness
|
||||||
|
repl.min_brightness = 0.0 # Increase for dark terminal backgrounds.
|
||||||
|
repl.max_brightness = 1.0 # Decrease for light terminal backgrounds.
|
||||||
|
|
||||||
|
# Syntax.
|
||||||
|
repl.enable_syntax_highlighting = True
|
||||||
|
|
||||||
|
# Get into Vi navigation mode at startup
|
||||||
|
repl.vi_start_in_navigation_mode = False
|
||||||
|
|
||||||
|
# Preserve last used Vi input mode between main loop iterations
|
||||||
|
repl.vi_keep_last_used_mode = False
|
||||||
|
|
||||||
|
# Install custom colorscheme named 'my-colorscheme' and use it.
|
||||||
|
"""
|
||||||
|
repl.install_ui_colorscheme("my-colorscheme", Style.from_dict(_custom_ui_colorscheme))
|
||||||
|
repl.use_ui_colorscheme("my-colorscheme")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add custom key binding for PDB.
|
||||||
|
"""
|
||||||
|
@repl.add_key_binding("c-b")
|
||||||
|
def _(event):
|
||||||
|
" Pressing Control-B will insert "pdb.set_trace()" "
|
||||||
|
event.cli.current_buffer.insert_text("\nimport pdb; pdb.set_trace()\n")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Typing ControlE twice should also execute the current command.
|
||||||
|
# (Alternative for Meta-Enter.)
|
||||||
|
"""
|
||||||
|
@repl.add_key_binding("c-e", "c-e")
|
||||||
|
def _(event):
|
||||||
|
event.current_buffer.validate_and_handle()
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Typing 'jj' in Vi Insert mode, should send escape. (Go back to navigation
|
||||||
|
# mode.)
|
||||||
|
"""
|
||||||
|
@repl.add_key_binding("j", "j", filter=ViInsertMode())
|
||||||
|
def _(event):
|
||||||
|
" Map 'jj' to Escape. "
|
||||||
|
event.cli.key_processor.feed(KeyPress("escape"))
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Custom key binding for some simple autocorrection while typing.
|
||||||
|
"""
|
||||||
|
corrections = {
|
||||||
|
"impotr": "import",
|
||||||
|
"pritn": "print",
|
||||||
|
}
|
||||||
|
|
||||||
|
@repl.add_key_binding(" ")
|
||||||
|
def _(event):
|
||||||
|
" When a space is pressed. Check & correct word before cursor. "
|
||||||
|
b = event.cli.current_buffer
|
||||||
|
w = b.document.get_word_before_cursor()
|
||||||
|
|
||||||
|
if w is not None:
|
||||||
|
if w in corrections:
|
||||||
|
b.delete_before_cursor(count=len(w))
|
||||||
|
b.insert_text(corrections[w])
|
||||||
|
|
||||||
|
b.insert_text(" ")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add a custom title to the status bar. This is useful when ptpython is
|
||||||
|
# embedded in other applications.
|
||||||
|
"""
|
||||||
|
repl.title = "My custom prompt."
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Custom colorscheme for the UI. See `ptpython/layout.py` and
|
||||||
|
# `ptpython/style.py` for all possible tokens.
|
||||||
|
_custom_ui_colorscheme = {
|
||||||
|
# Blue prompt.
|
||||||
|
"prompt": "bg:#eeeeff #000000 bold",
|
||||||
|
# Make the status toolbar red.
|
||||||
|
"status-toolbar": "bg:#ff0000 #000000",
|
||||||
|
}
|
38
examples/python-embed-with-custom-prompt.py
Executable file
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Example of embedding a Python REPL, and setting a custom prompt.
|
||||||
|
"""
|
||||||
|
from prompt_toolkit.formatted_text import HTML
|
||||||
|
|
||||||
|
from ptpython.prompt_style import PromptStyle
|
||||||
|
from ptpython.repl import embed
|
||||||
|
|
||||||
|
|
||||||
|
def configure(repl):
|
||||||
|
# Probably, the best is to add a new PromptStyle to `all_prompt_styles` and
|
||||||
|
# activate it. This way, the other styles are still selectable from the
|
||||||
|
# menu.
|
||||||
|
class CustomPrompt(PromptStyle):
|
||||||
|
def in_prompt(self):
|
||||||
|
return HTML("<ansigreen>Input[%s]</ansigreen>: ") % (
|
||||||
|
repl.current_statement_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
def in2_prompt(self, width):
|
||||||
|
return "...: ".rjust(width)
|
||||||
|
|
||||||
|
def out_prompt(self):
|
||||||
|
return HTML("<ansired>Result[%s]</ansired>: ") % (
|
||||||
|
repl.current_statement_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
repl.all_prompt_styles["custom"] = CustomPrompt()
|
||||||
|
repl.prompt_style = "custom"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
embed(globals(), locals(), configure=configure)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
12
examples/python-embed.py
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
from ptpython.repl import embed
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
embed(globals(), locals(), vi_mode=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
15
examples/python-input.py
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
from ptpython.python_input import PythonInput
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
prompt = PythonInput()
|
||||||
|
|
||||||
|
text = prompt.app.run()
|
||||||
|
print("You said: " + text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
49
examples/ssh-and-telnet-embed.py
Executable file
|
@ -0,0 +1,49 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Serve a ptpython console using both telnet and ssh.
|
||||||
|
|
||||||
|
Thanks to Vincent Michel for this!
|
||||||
|
https://gist.github.com/vxgmichel/7685685b3e5ead04ada4a3ba75a48eef
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import asyncssh
|
||||||
|
from prompt_toolkit import print_formatted_text
|
||||||
|
from prompt_toolkit.contrib.ssh.server import PromptToolkitSSHServer
|
||||||
|
from prompt_toolkit.contrib.telnet.server import TelnetServer
|
||||||
|
|
||||||
|
from ptpython.repl import embed
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_key(filename="ssh_host_key"):
|
||||||
|
path = pathlib.Path(filename)
|
||||||
|
if not path.exists():
|
||||||
|
rsa_key = asyncssh.generate_private_key("ssh-rsa")
|
||||||
|
path.write_bytes(rsa_key.export_private_key())
|
||||||
|
return str(path)
|
||||||
|
|
||||||
|
|
||||||
|
async def interact(connection=None):
|
||||||
|
global_dict = {**globals(), "print": print_formatted_text}
|
||||||
|
await embed(return_asyncio_coroutine=True, globals=global_dict)
|
||||||
|
|
||||||
|
|
||||||
|
async def main(ssh_port=8022, telnet_port=8023):
|
||||||
|
ssh_server = PromptToolkitSSHServer(interact=interact)
|
||||||
|
await asyncssh.create_server(
|
||||||
|
lambda: ssh_server, "", ssh_port, server_host_keys=[ensure_key()]
|
||||||
|
)
|
||||||
|
print(f"Running ssh server on port {ssh_port}...")
|
||||||
|
|
||||||
|
telnet_server = TelnetServer(interact=interact, port=telnet_port)
|
||||||
|
telnet_server.start()
|
||||||
|
print(f"Running telnet server on port {telnet_port}...")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
24
examples/test-cases/ptpython-in-other-thread.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Example of running ptpython in another thread.
|
||||||
|
|
||||||
|
(For testing whether it's working fine if it's not embedded in the main
|
||||||
|
thread.)
|
||||||
|
"""
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from ptpython.repl import embed
|
||||||
|
|
||||||
|
|
||||||
|
def in_thread():
|
||||||
|
embed(globals(), locals(), vi_mode=False)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
th = threading.Thread(target=in_thread)
|
||||||
|
th.start()
|
||||||
|
th.join()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
6
mypy.ini
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[mypy]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
no_implicit_optional = True
|
||||||
|
platform = win32
|
||||||
|
strict_equality = True
|
||||||
|
strict_optional = True
|
3
ptpython/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .repl import embed
|
||||||
|
|
||||||
|
__all__ = ["embed"]
|
6
ptpython/__main__.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
"""
|
||||||
|
Make `python -m ptpython` an alias for running `./ptpython`.
|
||||||
|
"""
|
||||||
|
from .entry_points.run_ptpython import run
|
||||||
|
|
||||||
|
run()
|
650
ptpython/completer.py
Normal file
|
@ -0,0 +1,650 @@
|
||||||
|
import ast
|
||||||
|
import collections.abc as collections_abc
|
||||||
|
import inspect
|
||||||
|
import keyword
|
||||||
|
import re
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional
|
||||||
|
|
||||||
|
from prompt_toolkit.completion import (
|
||||||
|
CompleteEvent,
|
||||||
|
Completer,
|
||||||
|
Completion,
|
||||||
|
PathCompleter,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.contrib.completers.system import SystemCompleter
|
||||||
|
from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar
|
||||||
|
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
from prompt_toolkit.formatted_text import fragment_list_to_text, to_formatted_text
|
||||||
|
|
||||||
|
from ptpython.utils import get_jedi_script_from_document
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prompt_toolkit.contrib.regular_languages.compiler import _CompiledGrammar
|
||||||
|
|
||||||
|
__all__ = ["PythonCompleter", "CompletePrivateAttributes", "HidePrivateCompleter"]
|
||||||
|
|
||||||
|
|
||||||
|
class CompletePrivateAttributes(Enum):
|
||||||
|
"""
|
||||||
|
Should we display private attributes in the completion pop-up?
|
||||||
|
"""
|
||||||
|
|
||||||
|
NEVER = "NEVER"
|
||||||
|
IF_NO_PUBLIC = "IF_NO_PUBLIC"
|
||||||
|
ALWAYS = "ALWAYS"
|
||||||
|
|
||||||
|
|
||||||
|
class PythonCompleter(Completer):
|
||||||
|
"""
|
||||||
|
Completer for Python code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
get_globals: Callable[[], dict],
|
||||||
|
get_locals: Callable[[], dict],
|
||||||
|
enable_dictionary_completion: Callable[[], bool],
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.get_globals = get_globals
|
||||||
|
self.get_locals = get_locals
|
||||||
|
self.enable_dictionary_completion = enable_dictionary_completion
|
||||||
|
|
||||||
|
self._system_completer = SystemCompleter()
|
||||||
|
self._jedi_completer = JediCompleter(get_globals, get_locals)
|
||||||
|
self._dictionary_completer = DictionaryCompleter(get_globals, get_locals)
|
||||||
|
|
||||||
|
self._path_completer_cache: Optional[GrammarCompleter] = None
|
||||||
|
self._path_completer_grammar_cache: Optional["_CompiledGrammar"] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _path_completer(self) -> GrammarCompleter:
|
||||||
|
if self._path_completer_cache is None:
|
||||||
|
self._path_completer_cache = GrammarCompleter(
|
||||||
|
self._path_completer_grammar,
|
||||||
|
{
|
||||||
|
"var1": PathCompleter(expanduser=True),
|
||||||
|
"var2": PathCompleter(expanduser=True),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self._path_completer_cache
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _path_completer_grammar(self) -> "_CompiledGrammar":
|
||||||
|
"""
|
||||||
|
Return the grammar for matching paths inside strings inside Python
|
||||||
|
code.
|
||||||
|
"""
|
||||||
|
# We make this lazy, because it delays startup time a little bit.
|
||||||
|
# This way, the grammar is build during the first completion.
|
||||||
|
if self._path_completer_grammar_cache is None:
|
||||||
|
self._path_completer_grammar_cache = self._create_path_completer_grammar()
|
||||||
|
return self._path_completer_grammar_cache
|
||||||
|
|
||||||
|
def _create_path_completer_grammar(self) -> "_CompiledGrammar":
|
||||||
|
def unwrapper(text: str) -> str:
|
||||||
|
return re.sub(r"\\(.)", r"\1", text)
|
||||||
|
|
||||||
|
def single_quoted_wrapper(text: str) -> str:
|
||||||
|
return text.replace("\\", "\\\\").replace("'", "\\'")
|
||||||
|
|
||||||
|
def double_quoted_wrapper(text: str) -> str:
|
||||||
|
return text.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
|
||||||
|
grammar = r"""
|
||||||
|
# Text before the current string.
|
||||||
|
(
|
||||||
|
[^'"#] | # Not quoted characters.
|
||||||
|
''' ([^'\\]|'(?!')|''(?!')|\\.])* ''' | # Inside single quoted triple strings
|
||||||
|
"" " ([^"\\]|"(?!")|""(?!^)|\\.])* "" " | # Inside double quoted triple strings
|
||||||
|
|
||||||
|
\#[^\n]*(\n|$) | # Comment.
|
||||||
|
"(?!"") ([^"\\]|\\.)*" | # Inside double quoted strings.
|
||||||
|
'(?!'') ([^'\\]|\\.)*' # Inside single quoted strings.
|
||||||
|
|
||||||
|
# Warning: The negative lookahead in the above two
|
||||||
|
# statements is important. If we drop that,
|
||||||
|
# then the regex will try to interpret every
|
||||||
|
# triple quoted string also as a single quoted
|
||||||
|
# string, making this exponentially expensive to
|
||||||
|
# execute!
|
||||||
|
)*
|
||||||
|
# The current string that we're completing.
|
||||||
|
(
|
||||||
|
' (?P<var1>([^\n'\\]|\\.)*) | # Inside a single quoted string.
|
||||||
|
" (?P<var2>([^\n"\\]|\\.)*) # Inside a double quoted string.
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return compile_grammar(
|
||||||
|
grammar,
|
||||||
|
escape_funcs={"var1": single_quoted_wrapper, "var2": double_quoted_wrapper},
|
||||||
|
unescape_funcs={"var1": unwrapper, "var2": unwrapper},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _complete_path_while_typing(self, document: Document) -> bool:
|
||||||
|
char_before_cursor = document.char_before_cursor
|
||||||
|
return bool(
|
||||||
|
document.text
|
||||||
|
and (char_before_cursor.isalnum() or char_before_cursor in "/.~")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _complete_python_while_typing(self, document: Document) -> bool:
|
||||||
|
"""
|
||||||
|
When `complete_while_typing` is set, only return completions when this
|
||||||
|
returns `True`.
|
||||||
|
"""
|
||||||
|
text = document.text_before_cursor # .rstrip()
|
||||||
|
char_before_cursor = text[-1:]
|
||||||
|
return bool(
|
||||||
|
text and (char_before_cursor.isalnum() or char_before_cursor in "_.([,")
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_completions(
|
||||||
|
self, document: Document, complete_event: CompleteEvent
|
||||||
|
) -> Iterable[Completion]:
|
||||||
|
"""
|
||||||
|
Get Python completions.
|
||||||
|
"""
|
||||||
|
# If the input starts with an exclamation mark. Use the system completer.
|
||||||
|
if document.text.lstrip().startswith("!"):
|
||||||
|
yield from self._system_completer.get_completions(
|
||||||
|
Document(
|
||||||
|
text=document.text[1:], cursor_position=document.cursor_position - 1
|
||||||
|
),
|
||||||
|
complete_event,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Do dictionary key completions.
|
||||||
|
if complete_event.completion_requested or self._complete_python_while_typing(
|
||||||
|
document
|
||||||
|
):
|
||||||
|
if self.enable_dictionary_completion():
|
||||||
|
has_dict_completions = False
|
||||||
|
for c in self._dictionary_completer.get_completions(
|
||||||
|
document, complete_event
|
||||||
|
):
|
||||||
|
if c.text not in "[.":
|
||||||
|
# If we get the [ or . completion, still include the other
|
||||||
|
# completions.
|
||||||
|
has_dict_completions = True
|
||||||
|
yield c
|
||||||
|
if has_dict_completions:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Do Path completions (if there were no dictionary completions).
|
||||||
|
if complete_event.completion_requested or self._complete_path_while_typing(
|
||||||
|
document
|
||||||
|
):
|
||||||
|
yield from self._path_completer.get_completions(document, complete_event)
|
||||||
|
|
||||||
|
# Do Jedi completions.
|
||||||
|
if complete_event.completion_requested or self._complete_python_while_typing(
|
||||||
|
document
|
||||||
|
):
|
||||||
|
# If we are inside a string, Don't do Jedi completion.
|
||||||
|
if not self._path_completer_grammar.match(document.text_before_cursor):
|
||||||
|
|
||||||
|
# Do Jedi Python completions.
|
||||||
|
yield from self._jedi_completer.get_completions(
|
||||||
|
document, complete_event
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JediCompleter(Completer):
|
||||||
|
"""
|
||||||
|
Autocompleter that uses the Jedi library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_globals, get_locals) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.get_globals = get_globals
|
||||||
|
self.get_locals = get_locals
|
||||||
|
|
||||||
|
def get_completions(
|
||||||
|
self, document: Document, complete_event: CompleteEvent
|
||||||
|
) -> Iterable[Completion]:
|
||||||
|
script = get_jedi_script_from_document(
|
||||||
|
document, self.get_locals(), self.get_globals()
|
||||||
|
)
|
||||||
|
|
||||||
|
if script:
|
||||||
|
try:
|
||||||
|
jedi_completions = script.complete(
|
||||||
|
column=document.cursor_position_col,
|
||||||
|
line=document.cursor_position_row + 1,
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
# Issue #9: bad syntax causes completions() to fail in jedi.
|
||||||
|
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/9
|
||||||
|
pass
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# Issue #43: UnicodeDecodeError on OpenBSD
|
||||||
|
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/43
|
||||||
|
pass
|
||||||
|
except AttributeError:
|
||||||
|
# Jedi issue #513: https://github.com/davidhalter/jedi/issues/513
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
# Jedi issue: "ValueError: invalid \x escape"
|
||||||
|
pass
|
||||||
|
except KeyError:
|
||||||
|
# Jedi issue: "KeyError: u'a_lambda'."
|
||||||
|
# https://github.com/jonathanslenders/ptpython/issues/89
|
||||||
|
pass
|
||||||
|
except IOError:
|
||||||
|
# Jedi issue: "IOError: No such file or directory."
|
||||||
|
# https://github.com/jonathanslenders/ptpython/issues/71
|
||||||
|
pass
|
||||||
|
except AssertionError:
|
||||||
|
# In jedi.parser.__init__.py: 227, in remove_last_newline,
|
||||||
|
# the assertion "newline.value.endswith('\n')" can fail.
|
||||||
|
pass
|
||||||
|
except SystemError:
|
||||||
|
# In jedi.api.helpers.py: 144, in get_stack_at_position
|
||||||
|
# raise SystemError("This really shouldn't happen. There's a bug in Jedi.")
|
||||||
|
pass
|
||||||
|
except NotImplementedError:
|
||||||
|
# See: https://github.com/jonathanslenders/ptpython/issues/223
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
# Supress all other Jedi exceptions.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Move function parameters to the top.
|
||||||
|
jedi_completions = sorted(
|
||||||
|
jedi_completions,
|
||||||
|
key=lambda jc: (
|
||||||
|
# Params first.
|
||||||
|
jc.type != "param",
|
||||||
|
# Private at the end.
|
||||||
|
jc.name.startswith("_"),
|
||||||
|
# Then sort by name.
|
||||||
|
jc.name_with_symbols.lower(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for jc in jedi_completions:
|
||||||
|
if jc.type == "function":
|
||||||
|
suffix = "()"
|
||||||
|
else:
|
||||||
|
suffix = ""
|
||||||
|
|
||||||
|
if jc.type == "param":
|
||||||
|
suffix = "..."
|
||||||
|
|
||||||
|
yield Completion(
|
||||||
|
jc.name_with_symbols,
|
||||||
|
len(jc.complete) - len(jc.name_with_symbols),
|
||||||
|
display=jc.name_with_symbols + suffix,
|
||||||
|
display_meta=jc.type,
|
||||||
|
style=_get_style_for_jedi_completion(jc),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DictionaryCompleter(Completer):
|
||||||
|
"""
|
||||||
|
Experimental completer for Python dictionary keys.
|
||||||
|
|
||||||
|
Warning: This does an `eval` and `repr` on some Python expressions before
|
||||||
|
the cursor, which is potentially dangerous. It doesn't match on
|
||||||
|
function calls, so it only triggers attribute access.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_globals, get_locals):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.get_globals = get_globals
|
||||||
|
self.get_locals = get_locals
|
||||||
|
|
||||||
|
# Pattern for expressions that are "safe" to eval for auto-completion.
|
||||||
|
# These are expressions that contain only attribute and index lookups.
|
||||||
|
varname = r"[a-zA-Z_][a-zA-Z0-9_]*"
|
||||||
|
|
||||||
|
expression = rf"""
|
||||||
|
# Any expression safe enough to eval while typing.
|
||||||
|
# No operators, except dot, and only other dict lookups.
|
||||||
|
# Technically, this can be unsafe of course, if bad code runs
|
||||||
|
# in `__getattr__` or ``__getitem__``.
|
||||||
|
(
|
||||||
|
# Variable name
|
||||||
|
{varname}
|
||||||
|
|
||||||
|
\s*
|
||||||
|
|
||||||
|
(?:
|
||||||
|
# Attribute access.
|
||||||
|
\s* \. \s* {varname} \s*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
# Item lookup.
|
||||||
|
# (We match the square brackets. The key can be anything.
|
||||||
|
# We don't care about matching quotes here in the regex.
|
||||||
|
# Nested square brackets are not supported.)
|
||||||
|
\s* \[ [^\[\]]+ \] \s*
|
||||||
|
)*
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pattern for recognizing for-loops, so that we can provide
|
||||||
|
# autocompletion on the iterator of the for-loop. (According to the
|
||||||
|
# first item of the collection we're iterating over.)
|
||||||
|
self.for_loop_pattern = re.compile(
|
||||||
|
rf"""
|
||||||
|
for \s+ ([a-zA-Z0-9_]+) \s+ in \s+ {expression} \s* :
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern for matching a simple expression (for completing [ or .
|
||||||
|
# operators).
|
||||||
|
self.expression_pattern = re.compile(
|
||||||
|
rf"""
|
||||||
|
{expression}
|
||||||
|
$
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern for matching item lookups.
|
||||||
|
self.item_lookup_pattern = re.compile(
|
||||||
|
rf"""
|
||||||
|
{expression}
|
||||||
|
|
||||||
|
# Dict loopup to complete (square bracket open + start of
|
||||||
|
# string).
|
||||||
|
\[
|
||||||
|
\s* ([^\[\]]*)$
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern for matching attribute lookups.
|
||||||
|
self.attribute_lookup_pattern = re.compile(
|
||||||
|
rf"""
|
||||||
|
{expression}
|
||||||
|
|
||||||
|
# Attribute loopup to complete (dot + varname).
|
||||||
|
\.
|
||||||
|
\s* ([a-zA-Z0-9_]*)$
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _lookup(self, expression: str, temp_locals: Dict[str, Any]) -> object:
|
||||||
|
"""
|
||||||
|
Do lookup of `object_var` in the context.
|
||||||
|
`temp_locals` is a dictionary, used for the locals.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return eval(expression.strip(), self.get_globals(), temp_locals)
|
||||||
|
except BaseException:
|
||||||
|
return None # Many exception, like NameError can be thrown here.
|
||||||
|
|
||||||
|
def get_completions(
|
||||||
|
self, document: Document, complete_event: CompleteEvent
|
||||||
|
) -> Iterable[Completion]:
|
||||||
|
|
||||||
|
# First, find all for-loops, and assign the first item of the
|
||||||
|
# collections they're iterating to the iterator variable, so that we
|
||||||
|
# can provide code completion on the iterators.
|
||||||
|
temp_locals = self.get_locals().copy()
|
||||||
|
|
||||||
|
for match in self.for_loop_pattern.finditer(document.text_before_cursor):
|
||||||
|
varname, expression = match.groups()
|
||||||
|
expression_val = self._lookup(expression, temp_locals)
|
||||||
|
|
||||||
|
# We do this only for lists and tuples. Calling `next()` on any
|
||||||
|
# collection would create undesired side effects.
|
||||||
|
if isinstance(expression_val, (list, tuple)) and expression_val:
|
||||||
|
temp_locals[varname] = expression_val[0]
|
||||||
|
|
||||||
|
# Get all completions.
|
||||||
|
yield from self._get_expression_completions(
|
||||||
|
document, complete_event, temp_locals
|
||||||
|
)
|
||||||
|
yield from self._get_item_lookup_completions(
|
||||||
|
document, complete_event, temp_locals
|
||||||
|
)
|
||||||
|
yield from self._get_attribute_completions(
|
||||||
|
document, complete_event, temp_locals
|
||||||
|
)
|
||||||
|
|
||||||
|
def _do_repr(self, obj: object) -> str:
|
||||||
|
try:
|
||||||
|
return str(repr(obj))
|
||||||
|
except BaseException:
|
||||||
|
raise ReprFailedError
|
||||||
|
|
||||||
|
def eval_expression(self, document: Document, locals: Dict[str, Any]) -> object:
|
||||||
|
"""
|
||||||
|
Evaluate
|
||||||
|
"""
|
||||||
|
match = self.expression_pattern.search(document.text_before_cursor)
|
||||||
|
if match is not None:
|
||||||
|
object_var = match.groups()[0]
|
||||||
|
return self._lookup(object_var, locals)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_expression_completions(
|
||||||
|
self,
|
||||||
|
document: Document,
|
||||||
|
complete_event: CompleteEvent,
|
||||||
|
temp_locals: Dict[str, Any],
|
||||||
|
) -> Iterable[Completion]:
|
||||||
|
"""
|
||||||
|
Complete the [ or . operator after an object.
|
||||||
|
"""
|
||||||
|
result = self.eval_expression(document, temp_locals)
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
|
||||||
|
if isinstance(
|
||||||
|
result,
|
||||||
|
(list, tuple, dict, collections_abc.Mapping, collections_abc.Sequence),
|
||||||
|
):
|
||||||
|
yield Completion("[", 0)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Note: Don't call `if result` here. That can fail for types
|
||||||
|
# that have custom truthness checks.
|
||||||
|
yield Completion(".", 0)
|
||||||
|
|
||||||
|
def _get_item_lookup_completions(
|
||||||
|
self,
|
||||||
|
document: Document,
|
||||||
|
complete_event: CompleteEvent,
|
||||||
|
temp_locals: Dict[str, Any],
|
||||||
|
) -> Iterable[Completion]:
|
||||||
|
"""
|
||||||
|
Complete dictionary keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def abbr_meta(text: str) -> str:
|
||||||
|
" Abbreviate meta text, make sure it fits on one line. "
|
||||||
|
# Take first line, if multiple lines.
|
||||||
|
if len(text) > 20:
|
||||||
|
text = text[:20] + "..."
|
||||||
|
if "\n" in text:
|
||||||
|
text = text.split("\n", 1)[0] + "..."
|
||||||
|
return text
|
||||||
|
|
||||||
|
match = self.item_lookup_pattern.search(document.text_before_cursor)
|
||||||
|
if match is not None:
|
||||||
|
object_var, key = match.groups()
|
||||||
|
|
||||||
|
# Do lookup of `object_var` in the context.
|
||||||
|
result = self._lookup(object_var, temp_locals)
|
||||||
|
|
||||||
|
# If this object is a dictionary, complete the keys.
|
||||||
|
if isinstance(result, (dict, collections_abc.Mapping)):
|
||||||
|
# Try to evaluate the key.
|
||||||
|
key_obj = key
|
||||||
|
for k in [key, key + '"', key + "'"]:
|
||||||
|
try:
|
||||||
|
key_obj = ast.literal_eval(k)
|
||||||
|
except (SyntaxError, ValueError):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
for k in result:
|
||||||
|
if str(k).startswith(str(key_obj)):
|
||||||
|
try:
|
||||||
|
k_repr = self._do_repr(k)
|
||||||
|
yield Completion(
|
||||||
|
k_repr + "]",
|
||||||
|
-len(key),
|
||||||
|
display=f"[{k_repr}]",
|
||||||
|
display_meta=abbr_meta(self._do_repr(result[k])),
|
||||||
|
)
|
||||||
|
except ReprFailedError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Complete list/tuple index keys.
|
||||||
|
elif isinstance(result, (list, tuple, collections_abc.Sequence)):
|
||||||
|
if not key or key.isdigit():
|
||||||
|
for k in range(min(len(result), 1000)):
|
||||||
|
if str(k).startswith(key):
|
||||||
|
try:
|
||||||
|
k_repr = self._do_repr(k)
|
||||||
|
yield Completion(
|
||||||
|
k_repr + "]",
|
||||||
|
-len(key),
|
||||||
|
display=f"[{k_repr}]",
|
||||||
|
display_meta=abbr_meta(self._do_repr(result[k])),
|
||||||
|
)
|
||||||
|
except ReprFailedError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_attribute_completions(
|
||||||
|
self,
|
||||||
|
document: Document,
|
||||||
|
complete_event: CompleteEvent,
|
||||||
|
temp_locals: Dict[str, Any],
|
||||||
|
) -> Iterable[Completion]:
|
||||||
|
"""
|
||||||
|
Complete attribute names.
|
||||||
|
"""
|
||||||
|
match = self.attribute_lookup_pattern.search(document.text_before_cursor)
|
||||||
|
if match is not None:
|
||||||
|
object_var, attr_name = match.groups()
|
||||||
|
|
||||||
|
# Do lookup of `object_var` in the context.
|
||||||
|
result = self._lookup(object_var, temp_locals)
|
||||||
|
|
||||||
|
names = self._sort_attribute_names(dir(result))
|
||||||
|
|
||||||
|
def get_suffix(name: str) -> str:
|
||||||
|
try:
|
||||||
|
obj = getattr(result, name, None)
|
||||||
|
if inspect.isfunction(obj):
|
||||||
|
return "()"
|
||||||
|
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return "{}"
|
||||||
|
if isinstance(obj, (list, tuple)):
|
||||||
|
return "[]"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
if name.startswith(attr_name):
|
||||||
|
suffix = get_suffix(name)
|
||||||
|
yield Completion(name, -len(attr_name), display=name + suffix)
|
||||||
|
|
||||||
|
def _sort_attribute_names(self, names: List[str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Sort attribute names alphabetically, but move the double underscore and
|
||||||
|
underscore names to the end.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def sort_key(name: str):
|
||||||
|
if name.startswith("__"):
|
||||||
|
return (2, name) # Double underscore comes latest.
|
||||||
|
if name.startswith("_"):
|
||||||
|
return (1, name) # Single underscore before that.
|
||||||
|
return (0, name) # Other names first.
|
||||||
|
|
||||||
|
return sorted(names, key=sort_key)
|
||||||
|
|
||||||
|
|
||||||
|
class HidePrivateCompleter(Completer):
|
||||||
|
"""
|
||||||
|
Wrapper around completer that hides private fields, deponding on whether or
|
||||||
|
not public fields are shown.
|
||||||
|
|
||||||
|
(The reason this is implemented as a `Completer` wrapper is because this
|
||||||
|
way it works also with `FuzzyCompleter`.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
completer: Completer,
|
||||||
|
complete_private_attributes: Callable[[], CompletePrivateAttributes],
|
||||||
|
) -> None:
|
||||||
|
self.completer = completer
|
||||||
|
self.complete_private_attributes = complete_private_attributes
|
||||||
|
|
||||||
|
def get_completions(
|
||||||
|
self, document: Document, complete_event: CompleteEvent
|
||||||
|
) -> Iterable[Completion]:
|
||||||
|
|
||||||
|
completions = list(self.completer.get_completions(document, complete_event))
|
||||||
|
complete_private_attributes = self.complete_private_attributes()
|
||||||
|
hide_private = False
|
||||||
|
|
||||||
|
def is_private(completion: Completion) -> bool:
|
||||||
|
text = fragment_list_to_text(to_formatted_text(completion.display))
|
||||||
|
return text.startswith("_")
|
||||||
|
|
||||||
|
if complete_private_attributes == CompletePrivateAttributes.NEVER:
|
||||||
|
hide_private = True
|
||||||
|
|
||||||
|
elif complete_private_attributes == CompletePrivateAttributes.IF_NO_PUBLIC:
|
||||||
|
hide_private = any(not is_private(completion) for completion in completions)
|
||||||
|
|
||||||
|
if hide_private:
|
||||||
|
completions = [
|
||||||
|
completion for completion in completions if not is_private(completion)
|
||||||
|
]
|
||||||
|
|
||||||
|
return completions
|
||||||
|
|
||||||
|
|
||||||
|
class ReprFailedError(Exception):
|
||||||
|
" Raised when the repr() call in `DictionaryCompleter` fails. "
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
_builtin_names = dir(builtins)
|
||||||
|
except ImportError: # Python 2.
|
||||||
|
_builtin_names = []
|
||||||
|
|
||||||
|
|
||||||
|
def _get_style_for_jedi_completion(jedi_completion) -> str:
|
||||||
|
"""
|
||||||
|
Return completion style to use for this name.
|
||||||
|
"""
|
||||||
|
name = jedi_completion.name_with_symbols
|
||||||
|
|
||||||
|
if jedi_completion.type == "param":
|
||||||
|
return "class:completion.param"
|
||||||
|
|
||||||
|
if name in _builtin_names:
|
||||||
|
return "class:completion.builtin"
|
||||||
|
|
||||||
|
if keyword.iskeyword(name):
|
||||||
|
return "class:completion.keyword"
|
||||||
|
|
||||||
|
return ""
|
0
ptpython/contrib/__init__.py
Normal file
119
ptpython/contrib/asyncssh_repl.py
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
"""
|
||||||
|
Tool for embedding a REPL inside a Python 3 asyncio process.
|
||||||
|
See ./examples/asyncio-ssh-python-embed.py for a demo.
|
||||||
|
|
||||||
|
Note that the code in this file is Python 3 only. However, we
|
||||||
|
should make sure not to use Python 3-only syntax, because this
|
||||||
|
package should be installable in Python 2 as well!
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from typing import Any, Optional, TextIO, cast
|
||||||
|
|
||||||
|
import asyncssh
|
||||||
|
from prompt_toolkit.data_structures import Size
|
||||||
|
from prompt_toolkit.input import create_pipe_input
|
||||||
|
from prompt_toolkit.output.vt100 import Vt100_Output
|
||||||
|
|
||||||
|
from ptpython.python_input import _GetNamespace
|
||||||
|
from ptpython.repl import PythonRepl
|
||||||
|
|
||||||
|
__all__ = ["ReplSSHServerSession"]
|
||||||
|
|
||||||
|
|
||||||
|
class ReplSSHServerSession(asyncssh.SSHServerSession):
|
||||||
|
"""
|
||||||
|
SSH server session that runs a Python REPL.
|
||||||
|
|
||||||
|
:param get_globals: callable that returns the current globals.
|
||||||
|
:param get_locals: (optional) callable that returns the current locals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, get_globals: _GetNamespace, get_locals: Optional[_GetNamespace] = None
|
||||||
|
) -> None:
|
||||||
|
self._chan: Any = None
|
||||||
|
|
||||||
|
def _globals() -> dict:
|
||||||
|
data = get_globals()
|
||||||
|
data.setdefault("print", self._print)
|
||||||
|
return data
|
||||||
|
|
||||||
|
# PipInput object, for sending input in the CLI.
|
||||||
|
# (This is something that we can use in the prompt_toolkit event loop,
|
||||||
|
# but still write date in manually.)
|
||||||
|
self._input_pipe = create_pipe_input()
|
||||||
|
|
||||||
|
# Output object. Don't render to the real stdout, but write everything
|
||||||
|
# in the SSH channel.
|
||||||
|
class Stdout:
|
||||||
|
def write(s, data: str) -> None:
|
||||||
|
if self._chan is not None:
|
||||||
|
data = data.replace("\n", "\r\n")
|
||||||
|
self._chan.write(data)
|
||||||
|
|
||||||
|
def flush(s) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.repl = PythonRepl(
|
||||||
|
get_globals=_globals,
|
||||||
|
get_locals=get_locals or _globals,
|
||||||
|
input=self._input_pipe,
|
||||||
|
output=Vt100_Output(cast(TextIO, Stdout()), self._get_size),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Disable open-in-editor and system prompt. Because it would run and
|
||||||
|
# display these commands on the server side, rather than in the SSH
|
||||||
|
# client.
|
||||||
|
self.repl.enable_open_in_editor = False
|
||||||
|
self.repl.enable_system_bindings = False
|
||||||
|
|
||||||
|
def _get_size(self) -> Size:
|
||||||
|
"""
|
||||||
|
Callable that returns the current `Size`, required by Vt100_Output.
|
||||||
|
"""
|
||||||
|
if self._chan is None:
|
||||||
|
return Size(rows=20, columns=79)
|
||||||
|
else:
|
||||||
|
width, height, pixwidth, pixheight = self._chan.get_terminal_size()
|
||||||
|
return Size(rows=height, columns=width)
|
||||||
|
|
||||||
|
def connection_made(self, chan):
|
||||||
|
"""
|
||||||
|
Client connected, run repl in coroutine.
|
||||||
|
"""
|
||||||
|
self._chan = chan
|
||||||
|
|
||||||
|
# Run REPL interface.
|
||||||
|
f = asyncio.ensure_future(self.repl.run_async())
|
||||||
|
|
||||||
|
# Close channel when done.
|
||||||
|
def done(_) -> None:
|
||||||
|
chan.close()
|
||||||
|
self._chan = None
|
||||||
|
|
||||||
|
f.add_done_callback(done)
|
||||||
|
|
||||||
|
def shell_requested(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def terminal_size_changed(self, width, height, pixwidth, pixheight):
|
||||||
|
"""
|
||||||
|
When the terminal size changes, report back to CLI.
|
||||||
|
"""
|
||||||
|
self.repl.app._on_resize()
|
||||||
|
|
||||||
|
def data_received(self, data, datatype):
|
||||||
|
"""
|
||||||
|
When data is received, send to inputstream of the CLI and repaint.
|
||||||
|
"""
|
||||||
|
self._input_pipe.send(data)
|
||||||
|
|
||||||
|
def _print(self, *data, sep=" ", end="\n", file=None) -> None:
|
||||||
|
"""
|
||||||
|
Alternative 'print' function that prints back into the SSH channel.
|
||||||
|
"""
|
||||||
|
# Pop keyword-only arguments. (We cannot use the syntax from the
|
||||||
|
# signature. Otherwise, Python2 will give a syntax error message when
|
||||||
|
# installing.)
|
||||||
|
data = sep.join(map(str, data))
|
||||||
|
self._chan.write(data + end)
|
0
ptpython/entry_points/__init__.py
Normal file
80
ptpython/entry_points/run_ptipython.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from .run_ptpython import create_parser, get_config_and_history_file
|
||||||
|
|
||||||
|
|
||||||
|
def run(user_ns=None):
|
||||||
|
a = create_parser().parse_args()
|
||||||
|
|
||||||
|
config_file, history_file = get_config_and_history_file(a)
|
||||||
|
|
||||||
|
# If IPython is not available, show message and exit here with error status
|
||||||
|
# code.
|
||||||
|
try:
|
||||||
|
import IPython
|
||||||
|
except ImportError:
|
||||||
|
print("IPython not found. Please install IPython (pip install ipython).")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
from ptpython.ipython import embed
|
||||||
|
from ptpython.repl import enable_deprecation_warnings, run_config
|
||||||
|
|
||||||
|
# Add the current directory to `sys.path`.
|
||||||
|
if sys.path[0] != "":
|
||||||
|
sys.path.insert(0, "")
|
||||||
|
|
||||||
|
# When a file has been given, run that, otherwise start the shell.
|
||||||
|
if a.args and not a.interactive:
|
||||||
|
sys.argv = a.args
|
||||||
|
path = a.args[0]
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
code = compile(f.read(), path, "exec")
|
||||||
|
exec(code, {})
|
||||||
|
else:
|
||||||
|
enable_deprecation_warnings()
|
||||||
|
|
||||||
|
# Create an empty namespace for this interactive shell. (If we don't do
|
||||||
|
# that, all the variables from this function will become available in
|
||||||
|
# the IPython shell.)
|
||||||
|
if user_ns is None:
|
||||||
|
user_ns = {}
|
||||||
|
|
||||||
|
# Startup path
|
||||||
|
startup_paths = []
|
||||||
|
if "PYTHONSTARTUP" in os.environ:
|
||||||
|
startup_paths.append(os.environ["PYTHONSTARTUP"])
|
||||||
|
|
||||||
|
# --interactive
|
||||||
|
if a.interactive:
|
||||||
|
startup_paths.append(a.args[0])
|
||||||
|
sys.argv = a.args
|
||||||
|
|
||||||
|
# exec scripts from startup paths
|
||||||
|
for path in startup_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
code = compile(f.read(), path, "exec")
|
||||||
|
exec(code, user_ns, user_ns)
|
||||||
|
else:
|
||||||
|
print("File not found: {}\n\n".format(path))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Apply config file
|
||||||
|
def configure(repl):
|
||||||
|
if os.path.exists(config_file):
|
||||||
|
run_config(repl, config_file)
|
||||||
|
|
||||||
|
# Run interactive shell.
|
||||||
|
embed(
|
||||||
|
vi_mode=a.vi,
|
||||||
|
history_filename=history_file,
|
||||||
|
configure=configure,
|
||||||
|
user_ns=user_ns,
|
||||||
|
title="IPython REPL (ptipython)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
217
ptpython/entry_points/run_ptpython.py
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
ptpython: Interactive Python shell.
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
args Script and arguments
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--vi Enable Vi key bindings
|
||||||
|
-i, --interactive Start interactive shell after executing this file.
|
||||||
|
--light-bg Run on a light background (use dark colors for text).
|
||||||
|
--dark-bg Run on a dark background (use light colors for text).
|
||||||
|
--config-file CONFIG_FILE
|
||||||
|
Location of configuration file.
|
||||||
|
--history-file HISTORY_FILE
|
||||||
|
Location of history file.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
|
||||||
|
environment variables:
|
||||||
|
PTPYTHON_CONFIG_HOME: a configuration directory to use
|
||||||
|
PYTHONSTARTUP: file executed on interactive startup (no default)
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import appdirs
|
||||||
|
from prompt_toolkit.formatted_text import HTML
|
||||||
|
from prompt_toolkit.shortcuts import print_formatted_text
|
||||||
|
|
||||||
|
from ptpython.repl import embed, enable_deprecation_warnings, run_config
|
||||||
|
|
||||||
|
try:
|
||||||
|
from importlib import metadata
|
||||||
|
except ImportError:
|
||||||
|
import importlib_metadata as metadata # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["create_parser", "get_config_and_history_file", "run"]
|
||||||
|
|
||||||
|
|
||||||
|
class _Parser(argparse.ArgumentParser):
|
||||||
|
def print_help(self):
|
||||||
|
super().print_help()
|
||||||
|
print(
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
environment variables:
|
||||||
|
PTPYTHON_CONFIG_HOME: a configuration directory to use
|
||||||
|
PYTHONSTARTUP: file executed on interactive startup (no default)
|
||||||
|
""",
|
||||||
|
).rstrip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_parser() -> _Parser:
|
||||||
|
parser = _Parser(description="ptpython: Interactive Python shell.")
|
||||||
|
parser.add_argument("--vi", action="store_true", help="Enable Vi key bindings")
|
||||||
|
parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--interactive",
|
||||||
|
action="store_true",
|
||||||
|
help="Start interactive shell after executing this file.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--light-bg",
|
||||||
|
action="store_true",
|
||||||
|
help="Run on a light background (use dark colors for text).",
|
||||||
|
),
|
||||||
|
parser.add_argument(
|
||||||
|
"--dark-bg",
|
||||||
|
action="store_true",
|
||||||
|
help="Run on a dark background (use light colors for text).",
|
||||||
|
),
|
||||||
|
parser.add_argument(
|
||||||
|
"--config-file", type=str, help="Location of configuration file."
|
||||||
|
)
|
||||||
|
parser.add_argument("--history-file", type=str, help="Location of history file.")
|
||||||
|
parser.add_argument(
|
||||||
|
"-V",
|
||||||
|
"--version",
|
||||||
|
action="version",
|
||||||
|
version=metadata.version("ptpython"), # type: ignore
|
||||||
|
)
|
||||||
|
parser.add_argument("args", nargs="*", help="Script and arguments")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Check which config/history files to use, ensure that the directories for
|
||||||
|
these files exist, and return the config and history path.
|
||||||
|
"""
|
||||||
|
config_dir = os.environ.get(
|
||||||
|
"PTPYTHON_CONFIG_HOME",
|
||||||
|
appdirs.user_config_dir("ptpython", "prompt_toolkit"),
|
||||||
|
)
|
||||||
|
data_dir = appdirs.user_data_dir("ptpython", "prompt_toolkit")
|
||||||
|
|
||||||
|
# Create directories.
|
||||||
|
for d in (config_dir, data_dir):
|
||||||
|
pathlib.Path(d).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Determine config file to be used.
|
||||||
|
config_file = os.path.join(config_dir, "config.py")
|
||||||
|
legacy_config_file = os.path.join(os.path.expanduser("~/.ptpython"), "config.py")
|
||||||
|
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# Config file
|
||||||
|
if namespace.config_file:
|
||||||
|
# Override config_file.
|
||||||
|
config_file = os.path.expanduser(namespace.config_file)
|
||||||
|
|
||||||
|
elif os.path.isfile(legacy_config_file):
|
||||||
|
# Warn about the legacy configuration file.
|
||||||
|
warnings.append(
|
||||||
|
HTML(
|
||||||
|
" <i>~/.ptpython/config.py</i> is deprecated, move your configuration to <i>%s</i>\n"
|
||||||
|
)
|
||||||
|
% config_file
|
||||||
|
)
|
||||||
|
config_file = legacy_config_file
|
||||||
|
|
||||||
|
# Determine history file to be used.
|
||||||
|
history_file = os.path.join(data_dir, "history")
|
||||||
|
legacy_history_file = os.path.join(os.path.expanduser("~/.ptpython"), "history")
|
||||||
|
|
||||||
|
if namespace.history_file:
|
||||||
|
# Override history_file.
|
||||||
|
history_file = os.path.expanduser(namespace.history_file)
|
||||||
|
|
||||||
|
elif os.path.isfile(legacy_history_file):
|
||||||
|
# Warn about the legacy history file.
|
||||||
|
warnings.append(
|
||||||
|
HTML(
|
||||||
|
" <i>~/.ptpython/history</i> is deprecated, move your history to <i>%s</i>\n"
|
||||||
|
)
|
||||||
|
% history_file
|
||||||
|
)
|
||||||
|
history_file = legacy_history_file
|
||||||
|
|
||||||
|
# Print warnings.
|
||||||
|
if warnings:
|
||||||
|
print_formatted_text(HTML("<u>Warning:</u>"))
|
||||||
|
for w in warnings:
|
||||||
|
print_formatted_text(w)
|
||||||
|
|
||||||
|
return config_file, history_file
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
a = create_parser().parse_args()
|
||||||
|
|
||||||
|
config_file, history_file = get_config_and_history_file(a)
|
||||||
|
|
||||||
|
# Startup path
|
||||||
|
startup_paths = []
|
||||||
|
if "PYTHONSTARTUP" in os.environ:
|
||||||
|
startup_paths.append(os.environ["PYTHONSTARTUP"])
|
||||||
|
|
||||||
|
# --interactive
|
||||||
|
if a.interactive and a.args:
|
||||||
|
# Note that we shouldn't run PYTHONSTARTUP when -i is given.
|
||||||
|
startup_paths = [a.args[0]]
|
||||||
|
sys.argv = a.args
|
||||||
|
|
||||||
|
# Add the current directory to `sys.path`.
|
||||||
|
if sys.path[0] != "":
|
||||||
|
sys.path.insert(0, "")
|
||||||
|
|
||||||
|
# When a file has been given, run that, otherwise start the shell.
|
||||||
|
if a.args and not a.interactive:
|
||||||
|
sys.argv = a.args
|
||||||
|
path = a.args[0]
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
code = compile(f.read(), path, "exec")
|
||||||
|
# NOTE: We have to pass an empty dictionary as namespace. Omitting
|
||||||
|
# this argument causes imports to not be found. See issue #326.
|
||||||
|
exec(code, {})
|
||||||
|
|
||||||
|
# Run interactive shell.
|
||||||
|
else:
|
||||||
|
enable_deprecation_warnings()
|
||||||
|
|
||||||
|
# Apply config file
|
||||||
|
def configure(repl) -> None:
|
||||||
|
if os.path.exists(config_file):
|
||||||
|
run_config(repl, config_file)
|
||||||
|
|
||||||
|
# Adjust colors if dark/light background flag has been given.
|
||||||
|
if a.light_bg:
|
||||||
|
repl.min_brightness = 0.0
|
||||||
|
repl.max_brightness = 0.60
|
||||||
|
elif a.dark_bg:
|
||||||
|
repl.min_brightness = 0.60
|
||||||
|
repl.max_brightness = 1.0
|
||||||
|
|
||||||
|
import __main__
|
||||||
|
|
||||||
|
embed(
|
||||||
|
vi_mode=a.vi,
|
||||||
|
history_filename=history_file,
|
||||||
|
configure=configure,
|
||||||
|
locals=__main__.__dict__,
|
||||||
|
globals=__main__.__dict__,
|
||||||
|
startup_paths=startup_paths,
|
||||||
|
title="Python REPL (ptpython)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
71
ptpython/eventloop.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
"""
|
||||||
|
Wrapper around the eventloop that gives some time to the Tkinter GUI to process
|
||||||
|
events when it's loaded and while we are waiting for input at the REPL. This
|
||||||
|
way we don't block the UI of for instance ``turtle`` and other Tk libraries.
|
||||||
|
|
||||||
|
(Normally Tkinter registers it's callbacks in ``PyOS_InputHook`` to integrate
|
||||||
|
in readline. ``prompt-toolkit`` doesn't understand that input hook, but this
|
||||||
|
will fix it for Tk.)
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
__all__ = ["inputhook"]
|
||||||
|
|
||||||
|
|
||||||
|
def _inputhook_tk(inputhook_context):
|
||||||
|
"""
|
||||||
|
Inputhook for Tk.
|
||||||
|
Run the Tk eventloop until prompt-toolkit needs to process the next input.
|
||||||
|
"""
|
||||||
|
# Get the current TK application.
|
||||||
|
import tkinter
|
||||||
|
|
||||||
|
import _tkinter # Keep this imports inline!
|
||||||
|
|
||||||
|
root = tkinter._default_root
|
||||||
|
|
||||||
|
def wait_using_filehandler():
|
||||||
|
"""
|
||||||
|
Run the TK eventloop until the file handler that we got from the
|
||||||
|
inputhook becomes readable.
|
||||||
|
"""
|
||||||
|
# Add a handler that sets the stop flag when `prompt-toolkit` has input
|
||||||
|
# to process.
|
||||||
|
stop = [False]
|
||||||
|
|
||||||
|
def done(*a):
|
||||||
|
stop[0] = True
|
||||||
|
|
||||||
|
root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done)
|
||||||
|
|
||||||
|
# Run the TK event loop as long as we don't receive input.
|
||||||
|
while root.dooneevent(_tkinter.ALL_EVENTS):
|
||||||
|
if stop[0]:
|
||||||
|
break
|
||||||
|
|
||||||
|
root.deletefilehandler(inputhook_context.fileno())
|
||||||
|
|
||||||
|
def wait_using_polling():
|
||||||
|
"""
|
||||||
|
Windows TK doesn't support 'createfilehandler'.
|
||||||
|
So, run the TK eventloop and poll until input is ready.
|
||||||
|
"""
|
||||||
|
while not inputhook_context.input_is_ready():
|
||||||
|
while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT):
|
||||||
|
pass
|
||||||
|
# Sleep to make the CPU idle, but not too long, so that the UI
|
||||||
|
# stays responsive.
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
if root is not None:
|
||||||
|
if hasattr(root, "createfilehandler"):
|
||||||
|
wait_using_filehandler()
|
||||||
|
else:
|
||||||
|
wait_using_polling()
|
||||||
|
|
||||||
|
|
||||||
|
def inputhook(inputhook_context):
|
||||||
|
# Only call the real input hook when the 'Tkinter' library was loaded.
|
||||||
|
if "Tkinter" in sys.modules or "tkinter" in sys.modules:
|
||||||
|
_inputhook_tk(inputhook_context)
|
36
ptpython/filters.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from prompt_toolkit.filters import Filter
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .python_input import PythonInput
|
||||||
|
|
||||||
|
__all__ = ["HasSignature", "ShowSidebar", "ShowSignature", "ShowDocstring"]
|
||||||
|
|
||||||
|
|
||||||
|
class PythonInputFilter(Filter):
|
||||||
|
def __init__(self, python_input: "PythonInput") -> None:
|
||||||
|
self.python_input = python_input
|
||||||
|
|
||||||
|
def __call__(self) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class HasSignature(PythonInputFilter):
|
||||||
|
def __call__(self) -> bool:
|
||||||
|
return bool(self.python_input.signatures)
|
||||||
|
|
||||||
|
|
||||||
|
class ShowSidebar(PythonInputFilter):
|
||||||
|
def __call__(self) -> bool:
|
||||||
|
return self.python_input.show_sidebar
|
||||||
|
|
||||||
|
|
||||||
|
class ShowSignature(PythonInputFilter):
|
||||||
|
def __call__(self) -> bool:
|
||||||
|
return self.python_input.show_signature
|
||||||
|
|
||||||
|
|
||||||
|
class ShowDocstring(PythonInputFilter):
|
||||||
|
def __call__(self) -> bool:
|
||||||
|
return self.python_input.show_docstring
|
648
ptpython/history_browser.py
Normal file
|
@ -0,0 +1,648 @@
|
||||||
|
"""
|
||||||
|
Utility to easily select lines from the history and execute them again.
|
||||||
|
|
||||||
|
`create_history_application` creates an `Application` instance that runs will
|
||||||
|
run as a sub application of the Repl/PythonInput.
|
||||||
|
"""
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from prompt_toolkit.application import Application
|
||||||
|
from prompt_toolkit.application.current import get_app
|
||||||
|
from prompt_toolkit.buffer import Buffer
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
from prompt_toolkit.enums import DEFAULT_BUFFER
|
||||||
|
from prompt_toolkit.filters import Condition, has_focus
|
||||||
|
from prompt_toolkit.formatted_text.utils import fragment_list_to_text
|
||||||
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
|
from prompt_toolkit.layout.containers import (
|
||||||
|
ConditionalContainer,
|
||||||
|
Container,
|
||||||
|
Float,
|
||||||
|
FloatContainer,
|
||||||
|
HSplit,
|
||||||
|
ScrollOffsets,
|
||||||
|
VSplit,
|
||||||
|
Window,
|
||||||
|
WindowAlign,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
||||||
|
from prompt_toolkit.layout.dimension import Dimension as D
|
||||||
|
from prompt_toolkit.layout.layout import Layout
|
||||||
|
from prompt_toolkit.layout.margins import Margin, ScrollbarMargin
|
||||||
|
from prompt_toolkit.layout.processors import Processor, Transformation
|
||||||
|
from prompt_toolkit.lexers import PygmentsLexer
|
||||||
|
from prompt_toolkit.widgets import Frame
|
||||||
|
from prompt_toolkit.widgets.toolbars import ArgToolbar, SearchToolbar
|
||||||
|
from pygments.lexers import Python3Lexer as PythonLexer
|
||||||
|
from pygments.lexers import RstLexer
|
||||||
|
|
||||||
|
from ptpython.layout import get_inputmode_fragments
|
||||||
|
|
||||||
|
from .utils import if_mousedown
|
||||||
|
|
||||||
|
HISTORY_COUNT = 2000
|
||||||
|
|
||||||
|
__all__ = ["HistoryLayout", "PythonHistory"]
|
||||||
|
|
||||||
|
HELP_TEXT = """
|
||||||
|
This interface is meant to select multiple lines from the
|
||||||
|
history and execute them together.
|
||||||
|
|
||||||
|
Typical usage
|
||||||
|
-------------
|
||||||
|
|
||||||
|
1. Move the ``cursor up`` in the history pane, until the
|
||||||
|
cursor is on the first desired line.
|
||||||
|
2. Hold down the ``space bar``, or press it multiple
|
||||||
|
times. Each time it will select one line and move to
|
||||||
|
the next one. Each selected line will appear on the
|
||||||
|
right side.
|
||||||
|
3. When all the required lines are displayed on the right
|
||||||
|
side, press ``Enter``. This will go back to the Python
|
||||||
|
REPL and show these lines as the current input. They
|
||||||
|
can still be edited from there.
|
||||||
|
|
||||||
|
Key bindings
|
||||||
|
------------
|
||||||
|
|
||||||
|
Many Emacs and Vi navigation key bindings should work.
|
||||||
|
Press ``F4`` to switch between Emacs and Vi mode.
|
||||||
|
|
||||||
|
Additional bindings:
|
||||||
|
|
||||||
|
- ``Space``: Select or delect a line.
|
||||||
|
- ``Tab``: Move the focus between the history and input
|
||||||
|
pane. (Alternative: ``Ctrl-W``)
|
||||||
|
- ``Ctrl-C``: Cancel. Ignore the result and go back to
|
||||||
|
the REPL. (Alternatives: ``q`` and ``Control-G``.)
|
||||||
|
- ``Enter``: Accept the result and go back to the REPL.
|
||||||
|
- ``F1``: Show/hide help. Press ``Enter`` to quit this
|
||||||
|
help message.
|
||||||
|
|
||||||
|
Further, remember that searching works like in Emacs
|
||||||
|
(using ``Ctrl-R``) or Vi (using ``/``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BORDER:
|
||||||
|
" Box drawing characters. "
|
||||||
|
HORIZONTAL = "\u2501"
|
||||||
|
VERTICAL = "\u2503"
|
||||||
|
TOP_LEFT = "\u250f"
|
||||||
|
TOP_RIGHT = "\u2513"
|
||||||
|
BOTTOM_LEFT = "\u2517"
|
||||||
|
BOTTOM_RIGHT = "\u251b"
|
||||||
|
LIGHT_VERTICAL = "\u2502"
|
||||||
|
|
||||||
|
|
||||||
|
def _create_popup_window(title: str, body: Container) -> Frame:
|
||||||
|
"""
|
||||||
|
Return the layout for a pop-up window. It consists of a title bar showing
|
||||||
|
the `title` text, and a body layout. The window is surrounded by borders.
|
||||||
|
"""
|
||||||
|
return Frame(body=body, title=title)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryLayout:
|
||||||
|
"""
|
||||||
|
Create and return a `Container` instance for the history
|
||||||
|
application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, history):
|
||||||
|
search_toolbar = SearchToolbar()
|
||||||
|
|
||||||
|
self.help_buffer_control = BufferControl(
|
||||||
|
buffer=history.help_buffer, lexer=PygmentsLexer(RstLexer)
|
||||||
|
)
|
||||||
|
|
||||||
|
help_window = _create_popup_window(
|
||||||
|
title="History Help",
|
||||||
|
body=Window(
|
||||||
|
content=self.help_buffer_control,
|
||||||
|
right_margins=[ScrollbarMargin(display_arrows=True)],
|
||||||
|
scroll_offsets=ScrollOffsets(top=2, bottom=2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.default_buffer_control = BufferControl(
|
||||||
|
buffer=history.default_buffer,
|
||||||
|
input_processors=[GrayExistingText(history.history_mapping)],
|
||||||
|
lexer=PygmentsLexer(PythonLexer),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.history_buffer_control = BufferControl(
|
||||||
|
buffer=history.history_buffer,
|
||||||
|
lexer=PygmentsLexer(PythonLexer),
|
||||||
|
search_buffer_control=search_toolbar.control,
|
||||||
|
preview_search=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
history_window = Window(
|
||||||
|
content=self.history_buffer_control,
|
||||||
|
wrap_lines=False,
|
||||||
|
left_margins=[HistoryMargin(history)],
|
||||||
|
scroll_offsets=ScrollOffsets(top=2, bottom=2),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.root_container = HSplit(
|
||||||
|
[
|
||||||
|
# Top title bar.
|
||||||
|
Window(
|
||||||
|
content=FormattedTextControl(_get_top_toolbar_fragments),
|
||||||
|
align=WindowAlign.CENTER,
|
||||||
|
style="class:status-toolbar",
|
||||||
|
),
|
||||||
|
FloatContainer(
|
||||||
|
content=VSplit(
|
||||||
|
[
|
||||||
|
# Left side: history.
|
||||||
|
history_window,
|
||||||
|
# Separator.
|
||||||
|
Window(
|
||||||
|
width=D.exact(1),
|
||||||
|
char=BORDER.LIGHT_VERTICAL,
|
||||||
|
style="class:separator",
|
||||||
|
),
|
||||||
|
# Right side: result.
|
||||||
|
Window(
|
||||||
|
content=self.default_buffer_control,
|
||||||
|
wrap_lines=False,
|
||||||
|
left_margins=[ResultMargin(history)],
|
||||||
|
scroll_offsets=ScrollOffsets(top=2, bottom=2),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
floats=[
|
||||||
|
# Help text as a float.
|
||||||
|
Float(
|
||||||
|
width=60,
|
||||||
|
top=3,
|
||||||
|
bottom=2,
|
||||||
|
content=ConditionalContainer(
|
||||||
|
content=help_window,
|
||||||
|
filter=has_focus(history.help_buffer),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Bottom toolbars.
|
||||||
|
ArgToolbar(),
|
||||||
|
search_toolbar,
|
||||||
|
Window(
|
||||||
|
content=FormattedTextControl(
|
||||||
|
partial(_get_bottom_toolbar_fragments, history=history)
|
||||||
|
),
|
||||||
|
style="class:status-toolbar",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.layout = Layout(self.root_container, history_window)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_top_toolbar_fragments():
|
||||||
|
return [("class:status-bar.title", "History browser - Insert from history")]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bottom_toolbar_fragments(history):
|
||||||
|
python_input = history.python_input
|
||||||
|
|
||||||
|
@if_mousedown
|
||||||
|
def f1(mouse_event):
|
||||||
|
_toggle_help(history)
|
||||||
|
|
||||||
|
@if_mousedown
|
||||||
|
def tab(mouse_event):
|
||||||
|
_select_other_window(history)
|
||||||
|
|
||||||
|
return (
|
||||||
|
[("class:status-toolbar", " ")]
|
||||||
|
+ get_inputmode_fragments(python_input)
|
||||||
|
+ [
|
||||||
|
("class:status-toolbar", " "),
|
||||||
|
("class:status-toolbar.key", "[Space]"),
|
||||||
|
("class:status-toolbar", " Toggle "),
|
||||||
|
("class:status-toolbar.key", "[Tab]", tab),
|
||||||
|
("class:status-toolbar", " Focus ", tab),
|
||||||
|
("class:status-toolbar.key", "[Enter]"),
|
||||||
|
("class:status-toolbar", " Accept "),
|
||||||
|
("class:status-toolbar.key", "[F1]", f1),
|
||||||
|
("class:status-toolbar", " Help ", f1),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryMargin(Margin):
|
||||||
|
"""
|
||||||
|
Margin for the history buffer.
|
||||||
|
This displays a green bar for the selected entries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, history):
|
||||||
|
self.history_buffer = history.history_buffer
|
||||||
|
self.history_mapping = history.history_mapping
|
||||||
|
|
||||||
|
def get_width(self, ui_content):
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def create_margin(self, window_render_info, width, height):
|
||||||
|
document = self.history_buffer.document
|
||||||
|
|
||||||
|
lines_starting_new_entries = self.history_mapping.lines_starting_new_entries
|
||||||
|
selected_lines = self.history_mapping.selected_lines
|
||||||
|
|
||||||
|
current_lineno = document.cursor_position_row
|
||||||
|
|
||||||
|
visible_line_to_input_line = window_render_info.visible_line_to_input_line
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for y in range(height):
|
||||||
|
line_number = visible_line_to_input_line.get(y)
|
||||||
|
|
||||||
|
# Show stars at the start of each entry.
|
||||||
|
# (Visualises multiline entries.)
|
||||||
|
if line_number in lines_starting_new_entries:
|
||||||
|
char = "*"
|
||||||
|
else:
|
||||||
|
char = " "
|
||||||
|
|
||||||
|
if line_number in selected_lines:
|
||||||
|
t = "class:history-line,selected"
|
||||||
|
else:
|
||||||
|
t = "class:history-line"
|
||||||
|
|
||||||
|
if line_number == current_lineno:
|
||||||
|
t = t + ",current"
|
||||||
|
|
||||||
|
result.append((t, char))
|
||||||
|
result.append(("", "\n"))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ResultMargin(Margin):
|
||||||
|
"""
|
||||||
|
The margin to be shown in the result pane.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, history):
|
||||||
|
self.history_mapping = history.history_mapping
|
||||||
|
self.history_buffer = history.history_buffer
|
||||||
|
|
||||||
|
def get_width(self, ui_content):
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def create_margin(self, window_render_info, width, height):
|
||||||
|
document = self.history_buffer.document
|
||||||
|
|
||||||
|
current_lineno = document.cursor_position_row
|
||||||
|
offset = (
|
||||||
|
self.history_mapping.result_line_offset
|
||||||
|
) # original_document.cursor_position_row
|
||||||
|
|
||||||
|
visible_line_to_input_line = window_render_info.visible_line_to_input_line
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for y in range(height):
|
||||||
|
line_number = visible_line_to_input_line.get(y)
|
||||||
|
|
||||||
|
if (
|
||||||
|
line_number is None
|
||||||
|
or line_number < offset
|
||||||
|
or line_number >= offset + len(self.history_mapping.selected_lines)
|
||||||
|
):
|
||||||
|
t = ""
|
||||||
|
elif line_number == current_lineno:
|
||||||
|
t = "class:history-line,selected,current"
|
||||||
|
else:
|
||||||
|
t = "class:history-line,selected"
|
||||||
|
|
||||||
|
result.append((t, " "))
|
||||||
|
result.append(("", "\n"))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def invalidation_hash(self, document):
|
||||||
|
return document.cursor_position_row
|
||||||
|
|
||||||
|
|
||||||
|
class GrayExistingText(Processor):
|
||||||
|
"""
|
||||||
|
Turn the existing input, before and after the inserted code gray.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, history_mapping):
|
||||||
|
self.history_mapping = history_mapping
|
||||||
|
self._lines_before = len(
|
||||||
|
history_mapping.original_document.text_before_cursor.splitlines()
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_transformation(self, transformation_input):
|
||||||
|
lineno = transformation_input.lineno
|
||||||
|
fragments = transformation_input.fragments
|
||||||
|
|
||||||
|
if lineno < self._lines_before or lineno >= self._lines_before + len(
|
||||||
|
self.history_mapping.selected_lines
|
||||||
|
):
|
||||||
|
text = fragment_list_to_text(fragments)
|
||||||
|
return Transformation(fragments=[("class:history.existing-input", text)])
|
||||||
|
else:
|
||||||
|
return Transformation(fragments=fragments)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryMapping:
|
||||||
|
"""
|
||||||
|
Keep a list of all the lines from the history and the selected lines.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, history, python_history, original_document):
|
||||||
|
self.history = history
|
||||||
|
self.python_history = python_history
|
||||||
|
self.original_document = original_document
|
||||||
|
|
||||||
|
self.lines_starting_new_entries = set()
|
||||||
|
self.selected_lines = set()
|
||||||
|
|
||||||
|
# Process history.
|
||||||
|
history_strings = python_history.get_strings()
|
||||||
|
history_lines = []
|
||||||
|
|
||||||
|
for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]:
|
||||||
|
self.lines_starting_new_entries.add(len(history_lines))
|
||||||
|
|
||||||
|
for line in entry.splitlines():
|
||||||
|
history_lines.append(line)
|
||||||
|
|
||||||
|
if len(history_strings) > HISTORY_COUNT:
|
||||||
|
history_lines[0] = (
|
||||||
|
"# *** History has been truncated to %s lines ***" % HISTORY_COUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
self.history_lines = history_lines
|
||||||
|
self.concatenated_history = "\n".join(history_lines)
|
||||||
|
|
||||||
|
# Line offset.
|
||||||
|
if self.original_document.text_before_cursor:
|
||||||
|
self.result_line_offset = self.original_document.cursor_position_row + 1
|
||||||
|
else:
|
||||||
|
self.result_line_offset = 0
|
||||||
|
|
||||||
|
def get_new_document(self, cursor_pos=None):
|
||||||
|
"""
|
||||||
|
Create a `Document` instance that contains the resulting text.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
# Original text, before cursor.
|
||||||
|
if self.original_document.text_before_cursor:
|
||||||
|
lines.append(self.original_document.text_before_cursor)
|
||||||
|
|
||||||
|
# Selected entries from the history.
|
||||||
|
for line_no in sorted(self.selected_lines):
|
||||||
|
lines.append(self.history_lines[line_no])
|
||||||
|
|
||||||
|
# Original text, after cursor.
|
||||||
|
if self.original_document.text_after_cursor:
|
||||||
|
lines.append(self.original_document.text_after_cursor)
|
||||||
|
|
||||||
|
# Create `Document` with cursor at the right position.
|
||||||
|
text = "\n".join(lines)
|
||||||
|
if cursor_pos is not None and cursor_pos > len(text):
|
||||||
|
cursor_pos = len(text)
|
||||||
|
return Document(text, cursor_pos)
|
||||||
|
|
||||||
|
def update_default_buffer(self):
|
||||||
|
b = self.history.default_buffer
|
||||||
|
|
||||||
|
b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggle_help(history):
|
||||||
|
" Display/hide help. "
|
||||||
|
help_buffer_control = history.history_layout.help_buffer_control
|
||||||
|
|
||||||
|
if history.app.layout.current_control == help_buffer_control:
|
||||||
|
history.app.layout.focus_previous()
|
||||||
|
else:
|
||||||
|
history.app.layout.current_control = help_buffer_control
|
||||||
|
|
||||||
|
|
||||||
|
def _select_other_window(history):
|
||||||
|
" Toggle focus between left/right window. "
|
||||||
|
current_buffer = history.app.current_buffer
|
||||||
|
layout = history.history_layout.layout
|
||||||
|
|
||||||
|
if current_buffer == history.history_buffer:
|
||||||
|
layout.current_control = history.history_layout.default_buffer_control
|
||||||
|
|
||||||
|
elif current_buffer == history.default_buffer:
|
||||||
|
layout.current_control = history.history_layout.history_buffer_control
|
||||||
|
|
||||||
|
|
||||||
|
def create_key_bindings(history, python_input, history_mapping):
|
||||||
|
"""
|
||||||
|
Key bindings.
|
||||||
|
"""
|
||||||
|
bindings = KeyBindings()
|
||||||
|
handle = bindings.add
|
||||||
|
|
||||||
|
@handle(" ", filter=has_focus(history.history_buffer))
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Space: select/deselect line from history pane.
|
||||||
|
"""
|
||||||
|
b = event.current_buffer
|
||||||
|
line_no = b.document.cursor_position_row
|
||||||
|
|
||||||
|
if not history_mapping.history_lines:
|
||||||
|
# If we've no history, then nothing to do
|
||||||
|
return
|
||||||
|
|
||||||
|
if line_no in history_mapping.selected_lines:
|
||||||
|
# Remove line.
|
||||||
|
history_mapping.selected_lines.remove(line_no)
|
||||||
|
history_mapping.update_default_buffer()
|
||||||
|
else:
|
||||||
|
# Add line.
|
||||||
|
history_mapping.selected_lines.add(line_no)
|
||||||
|
history_mapping.update_default_buffer()
|
||||||
|
|
||||||
|
# Update cursor position
|
||||||
|
default_buffer = history.default_buffer
|
||||||
|
default_lineno = (
|
||||||
|
sorted(history_mapping.selected_lines).index(line_no)
|
||||||
|
+ history_mapping.result_line_offset
|
||||||
|
)
|
||||||
|
default_buffer.cursor_position = (
|
||||||
|
default_buffer.document.translate_row_col_to_index(default_lineno, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Also move the cursor to the next line. (This way they can hold
|
||||||
|
# space to select a region.)
|
||||||
|
b.cursor_position = b.document.translate_row_col_to_index(line_no + 1, 0)
|
||||||
|
|
||||||
|
@handle(" ", filter=has_focus(DEFAULT_BUFFER))
|
||||||
|
@handle("delete", filter=has_focus(DEFAULT_BUFFER))
|
||||||
|
@handle("c-h", filter=has_focus(DEFAULT_BUFFER))
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Space: remove line from default pane.
|
||||||
|
"""
|
||||||
|
b = event.current_buffer
|
||||||
|
line_no = b.document.cursor_position_row - history_mapping.result_line_offset
|
||||||
|
|
||||||
|
if line_no >= 0:
|
||||||
|
try:
|
||||||
|
history_lineno = sorted(history_mapping.selected_lines)[line_no]
|
||||||
|
except IndexError:
|
||||||
|
pass # When `selected_lines` is an empty set.
|
||||||
|
else:
|
||||||
|
history_mapping.selected_lines.remove(history_lineno)
|
||||||
|
|
||||||
|
history_mapping.update_default_buffer()
|
||||||
|
|
||||||
|
help_focussed = has_focus(history.help_buffer)
|
||||||
|
main_buffer_focussed = has_focus(history.history_buffer) | has_focus(
|
||||||
|
history.default_buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
@handle("tab", filter=main_buffer_focussed)
|
||||||
|
@handle("c-x", filter=main_buffer_focussed, eager=True)
|
||||||
|
# Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding.
|
||||||
|
@handle("c-w", filter=main_buffer_focussed)
|
||||||
|
def _(event):
|
||||||
|
" Select other window. "
|
||||||
|
_select_other_window(history)
|
||||||
|
|
||||||
|
@handle("f4")
|
||||||
|
def _(event):
|
||||||
|
" Switch between Emacs/Vi mode. "
|
||||||
|
python_input.vi_mode = not python_input.vi_mode
|
||||||
|
|
||||||
|
@handle("f1")
|
||||||
|
def _(event):
|
||||||
|
" Display/hide help. "
|
||||||
|
_toggle_help(history)
|
||||||
|
|
||||||
|
@handle("enter", filter=help_focussed)
|
||||||
|
@handle("c-c", filter=help_focussed)
|
||||||
|
@handle("c-g", filter=help_focussed)
|
||||||
|
@handle("escape", filter=help_focussed)
|
||||||
|
def _(event):
|
||||||
|
" Leave help. "
|
||||||
|
event.app.layout.focus_previous()
|
||||||
|
|
||||||
|
@handle("q", filter=main_buffer_focussed)
|
||||||
|
@handle("f3", filter=main_buffer_focussed)
|
||||||
|
@handle("c-c", filter=main_buffer_focussed)
|
||||||
|
@handle("c-g", filter=main_buffer_focussed)
|
||||||
|
def _(event):
|
||||||
|
" Cancel and go back. "
|
||||||
|
event.app.exit(result=None)
|
||||||
|
|
||||||
|
@handle("enter", filter=main_buffer_focussed)
|
||||||
|
def _(event):
|
||||||
|
" Accept input. "
|
||||||
|
event.app.exit(result=history.default_buffer.text)
|
||||||
|
|
||||||
|
enable_system_bindings = Condition(lambda: python_input.enable_system_bindings)
|
||||||
|
|
||||||
|
@handle("c-z", filter=enable_system_bindings)
|
||||||
|
def _(event):
|
||||||
|
" Suspend to background. "
|
||||||
|
event.app.suspend_to_background()
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
|
class PythonHistory:
|
||||||
|
def __init__(self, python_input, original_document):
|
||||||
|
"""
|
||||||
|
Create an `Application` for the history screen.
|
||||||
|
This has to be run as a sub application of `python_input`.
|
||||||
|
|
||||||
|
When this application runs and returns, it retuns the selected lines.
|
||||||
|
"""
|
||||||
|
self.python_input = python_input
|
||||||
|
|
||||||
|
history_mapping = HistoryMapping(self, python_input.history, original_document)
|
||||||
|
self.history_mapping = history_mapping
|
||||||
|
|
||||||
|
document = Document(history_mapping.concatenated_history)
|
||||||
|
document = Document(
|
||||||
|
document.text,
|
||||||
|
cursor_position=document.cursor_position
|
||||||
|
+ document.get_start_of_line_position(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.history_buffer = Buffer(
|
||||||
|
document=document,
|
||||||
|
on_cursor_position_changed=self._history_buffer_pos_changed,
|
||||||
|
accept_handler=(
|
||||||
|
lambda buff: get_app().exit(result=self.default_buffer.text)
|
||||||
|
),
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.default_buffer = Buffer(
|
||||||
|
name=DEFAULT_BUFFER,
|
||||||
|
document=history_mapping.get_new_document(),
|
||||||
|
on_cursor_position_changed=self._default_buffer_pos_changed,
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.help_buffer = Buffer(document=Document(HELP_TEXT, 0), read_only=True)
|
||||||
|
|
||||||
|
self.history_layout = HistoryLayout(self)
|
||||||
|
|
||||||
|
self.app = Application(
|
||||||
|
layout=self.history_layout.layout,
|
||||||
|
full_screen=True,
|
||||||
|
style=python_input._current_style,
|
||||||
|
mouse_support=Condition(lambda: python_input.enable_mouse_support),
|
||||||
|
key_bindings=create_key_bindings(self, python_input, history_mapping),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _default_buffer_pos_changed(self, _):
|
||||||
|
"""When the cursor changes in the default buffer. Synchronize with
|
||||||
|
history buffer."""
|
||||||
|
# Only when this buffer has the focus.
|
||||||
|
if self.app.current_buffer == self.default_buffer:
|
||||||
|
try:
|
||||||
|
line_no = (
|
||||||
|
self.default_buffer.document.cursor_position_row
|
||||||
|
- self.history_mapping.result_line_offset
|
||||||
|
)
|
||||||
|
|
||||||
|
if line_no < 0: # When the cursor is above the inserted region.
|
||||||
|
raise IndexError
|
||||||
|
|
||||||
|
history_lineno = sorted(self.history_mapping.selected_lines)[line_no]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.history_buffer.cursor_position = (
|
||||||
|
self.history_buffer.document.translate_row_col_to_index(
|
||||||
|
history_lineno, 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _history_buffer_pos_changed(self, _):
|
||||||
|
""" When the cursor changes in the history buffer. Synchronize. """
|
||||||
|
# Only when this buffer has the focus.
|
||||||
|
if self.app.current_buffer == self.history_buffer:
|
||||||
|
line_no = self.history_buffer.document.cursor_position_row
|
||||||
|
|
||||||
|
if line_no in self.history_mapping.selected_lines:
|
||||||
|
default_lineno = (
|
||||||
|
sorted(self.history_mapping.selected_lines).index(line_no)
|
||||||
|
+ self.history_mapping.result_line_offset
|
||||||
|
)
|
||||||
|
|
||||||
|
self.default_buffer.cursor_position = (
|
||||||
|
self.default_buffer.document.translate_row_col_to_index(
|
||||||
|
default_lineno, 0
|
||||||
|
)
|
||||||
|
)
|
285
ptpython/ipython.py
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
Adaptor for using the input system of `prompt_toolkit` with the IPython
|
||||||
|
backend.
|
||||||
|
|
||||||
|
This gives a powerful interactive shell that has a nice user interface, but
|
||||||
|
also the power of for instance all the %-magic functions that IPython has to
|
||||||
|
offer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from IPython import utils as ipy_utils
|
||||||
|
from IPython.core.inputsplitter import IPythonInputSplitter
|
||||||
|
from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed
|
||||||
|
from IPython.terminal.ipapp import load_default_config
|
||||||
|
from prompt_toolkit.completion import (
|
||||||
|
Completer,
|
||||||
|
Completion,
|
||||||
|
PathCompleter,
|
||||||
|
WordCompleter,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.contrib.completers import SystemCompleter
|
||||||
|
from prompt_toolkit.contrib.regular_languages.compiler import compile
|
||||||
|
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
|
||||||
|
from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
from prompt_toolkit.formatted_text import PygmentsTokens
|
||||||
|
from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer
|
||||||
|
from prompt_toolkit.styles import Style
|
||||||
|
from pygments.lexers import BashLexer, PythonLexer
|
||||||
|
|
||||||
|
from ptpython.prompt_style import PromptStyle
|
||||||
|
|
||||||
|
from .python_input import PythonCompleter, PythonInput, PythonValidator
|
||||||
|
from .style import default_ui_style
|
||||||
|
|
||||||
|
__all__ = ["embed"]
|
||||||
|
|
||||||
|
|
||||||
|
class IPythonPrompt(PromptStyle):
|
||||||
|
"""
|
||||||
|
Style for IPython >5.0, use the prompt_toolkit tokens directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, prompts):
|
||||||
|
self.prompts = prompts
|
||||||
|
|
||||||
|
def in_prompt(self):
|
||||||
|
return PygmentsTokens(self.prompts.in_prompt_tokens())
|
||||||
|
|
||||||
|
def in2_prompt(self, width):
|
||||||
|
return PygmentsTokens(self.prompts.continuation_prompt_tokens())
|
||||||
|
|
||||||
|
def out_prompt(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class IPythonValidator(PythonValidator):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(IPythonValidator, self).__init__(*args, **kwargs)
|
||||||
|
self.isp = IPythonInputSplitter()
|
||||||
|
|
||||||
|
def validate(self, document):
|
||||||
|
document = Document(text=self.isp.transform_cell(document.text))
|
||||||
|
super(IPythonValidator, self).validate(document)
|
||||||
|
|
||||||
|
|
||||||
|
def create_ipython_grammar():
|
||||||
|
"""
|
||||||
|
Return compiled IPython grammar.
|
||||||
|
"""
|
||||||
|
return compile(
|
||||||
|
r"""
|
||||||
|
\s*
|
||||||
|
(
|
||||||
|
(?P<percent>%)(
|
||||||
|
(?P<magic>pycat|run|loadpy|load) \s+ (?P<py_filename>[^\s]+) |
|
||||||
|
(?P<magic>cat) \s+ (?P<filename>[^\s]+) |
|
||||||
|
(?P<magic>pushd|cd|ls) \s+ (?P<directory>[^\s]+) |
|
||||||
|
(?P<magic>pdb) \s+ (?P<pdb_arg>[^\s]+) |
|
||||||
|
(?P<magic>autocall) \s+ (?P<autocall_arg>[^\s]+) |
|
||||||
|
(?P<magic>time|timeit|prun) \s+ (?P<python>.+) |
|
||||||
|
(?P<magic>psource|pfile|pinfo|pinfo2) \s+ (?P<python>.+) |
|
||||||
|
(?P<magic>system) \s+ (?P<system>.+) |
|
||||||
|
(?P<magic>unalias) \s+ (?P<alias_name>.+) |
|
||||||
|
(?P<magic>[^\s]+) .* |
|
||||||
|
) .* |
|
||||||
|
!(?P<system>.+) |
|
||||||
|
(?![%!]) (?P<python>.+)
|
||||||
|
)
|
||||||
|
\s*
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_completer(
|
||||||
|
get_globals,
|
||||||
|
get_locals,
|
||||||
|
magics_manager,
|
||||||
|
alias_manager,
|
||||||
|
get_enable_dictionary_completion,
|
||||||
|
):
|
||||||
|
g = create_ipython_grammar()
|
||||||
|
|
||||||
|
return GrammarCompleter(
|
||||||
|
g,
|
||||||
|
{
|
||||||
|
"python": PythonCompleter(
|
||||||
|
get_globals, get_locals, get_enable_dictionary_completion
|
||||||
|
),
|
||||||
|
"magic": MagicsCompleter(magics_manager),
|
||||||
|
"alias_name": AliasCompleter(alias_manager),
|
||||||
|
"pdb_arg": WordCompleter(["on", "off"], ignore_case=True),
|
||||||
|
"autocall_arg": WordCompleter(["0", "1", "2"], ignore_case=True),
|
||||||
|
"py_filename": PathCompleter(
|
||||||
|
only_directories=False, file_filter=lambda name: name.endswith(".py")
|
||||||
|
),
|
||||||
|
"filename": PathCompleter(only_directories=False),
|
||||||
|
"directory": PathCompleter(only_directories=True),
|
||||||
|
"system": SystemCompleter(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_lexer():
|
||||||
|
g = create_ipython_grammar()
|
||||||
|
|
||||||
|
return GrammarLexer(
|
||||||
|
g,
|
||||||
|
lexers={
|
||||||
|
"percent": SimpleLexer("class:pygments.operator"),
|
||||||
|
"magic": SimpleLexer("class:pygments.keyword"),
|
||||||
|
"filename": SimpleLexer("class:pygments.name"),
|
||||||
|
"python": PygmentsLexer(PythonLexer),
|
||||||
|
"system": PygmentsLexer(BashLexer),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MagicsCompleter(Completer):
|
||||||
|
def __init__(self, magics_manager):
|
||||||
|
self.magics_manager = magics_manager
|
||||||
|
|
||||||
|
def get_completions(self, document, complete_event):
|
||||||
|
text = document.text_before_cursor.lstrip()
|
||||||
|
|
||||||
|
for m in sorted(self.magics_manager.magics["line"]):
|
||||||
|
if m.startswith(text):
|
||||||
|
yield Completion("%s" % m, -len(text))
|
||||||
|
|
||||||
|
|
||||||
|
class AliasCompleter(Completer):
|
||||||
|
def __init__(self, alias_manager):
|
||||||
|
self.alias_manager = alias_manager
|
||||||
|
|
||||||
|
def get_completions(self, document, complete_event):
|
||||||
|
text = document.text_before_cursor.lstrip()
|
||||||
|
# aliases = [a for a, _ in self.alias_manager.aliases]
|
||||||
|
aliases = self.alias_manager.aliases
|
||||||
|
|
||||||
|
for a, cmd in sorted(aliases, key=lambda a: a[0]):
|
||||||
|
if a.startswith(text):
|
||||||
|
yield Completion("%s" % a, -len(text), display_meta=cmd)
|
||||||
|
|
||||||
|
|
||||||
|
class IPythonInput(PythonInput):
|
||||||
|
"""
|
||||||
|
Override our `PythonCommandLineInterface` to add IPython specific stuff.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ipython_shell, *a, **kw):
|
||||||
|
kw["_completer"] = create_completer(
|
||||||
|
kw["get_globals"],
|
||||||
|
kw["get_globals"],
|
||||||
|
ipython_shell.magics_manager,
|
||||||
|
ipython_shell.alias_manager,
|
||||||
|
lambda: self.enable_dictionary_completion,
|
||||||
|
)
|
||||||
|
kw["_lexer"] = create_lexer()
|
||||||
|
kw["_validator"] = IPythonValidator(get_compiler_flags=self.get_compiler_flags)
|
||||||
|
|
||||||
|
super().__init__(*a, **kw)
|
||||||
|
self.ipython_shell = ipython_shell
|
||||||
|
|
||||||
|
self.all_prompt_styles["ipython"] = IPythonPrompt(ipython_shell.prompts)
|
||||||
|
self.prompt_style = "ipython"
|
||||||
|
|
||||||
|
# UI style for IPython. Add tokens that are used by IPython>5.0
|
||||||
|
style_dict = {}
|
||||||
|
style_dict.update(default_ui_style)
|
||||||
|
style_dict.update(
|
||||||
|
{
|
||||||
|
"pygments.prompt": "#009900",
|
||||||
|
"pygments.prompt-num": "#00ff00 bold",
|
||||||
|
"pygments.out-prompt": "#990000",
|
||||||
|
"pygments.out-prompt-num": "#ff0000 bold",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ui_styles = {"default": Style.from_dict(style_dict)}
|
||||||
|
self.use_ui_colorscheme("default")
|
||||||
|
|
||||||
|
|
||||||
|
class InteractiveShellEmbed(_InteractiveShellEmbed):
|
||||||
|
"""
|
||||||
|
Override the `InteractiveShellEmbed` from IPython, to replace the front-end
|
||||||
|
with our input shell.
|
||||||
|
|
||||||
|
:param configure: Callable for configuring the repl.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *a, **kw):
|
||||||
|
vi_mode = kw.pop("vi_mode", False)
|
||||||
|
history_filename = kw.pop("history_filename", None)
|
||||||
|
configure = kw.pop("configure", None)
|
||||||
|
title = kw.pop("title", None)
|
||||||
|
|
||||||
|
# Don't ask IPython to confirm for exit. We have our own exit prompt.
|
||||||
|
self.confirm_exit = False
|
||||||
|
|
||||||
|
super().__init__(*a, **kw)
|
||||||
|
|
||||||
|
def get_globals():
|
||||||
|
return self.user_ns
|
||||||
|
|
||||||
|
python_input = IPythonInput(
|
||||||
|
self,
|
||||||
|
get_globals=get_globals,
|
||||||
|
vi_mode=vi_mode,
|
||||||
|
history_filename=history_filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
if title:
|
||||||
|
python_input.terminal_title = title
|
||||||
|
|
||||||
|
if configure:
|
||||||
|
configure(python_input)
|
||||||
|
python_input.prompt_style = "ipython" # Don't take from config.
|
||||||
|
|
||||||
|
self.python_input = python_input
|
||||||
|
|
||||||
|
def prompt_for_code(self):
|
||||||
|
try:
|
||||||
|
return self.python_input.app.run()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.python_input.default_buffer.document = Document()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_extensions(shell, extensions):
|
||||||
|
"""
|
||||||
|
Partial copy of `InteractiveShellApp.init_extensions` from IPython.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
iter(extensions)
|
||||||
|
except TypeError:
|
||||||
|
pass # no extensions found
|
||||||
|
else:
|
||||||
|
for ext in extensions:
|
||||||
|
try:
|
||||||
|
shell.extension_manager.load_extension(ext)
|
||||||
|
except:
|
||||||
|
warn(
|
||||||
|
"Error in loading extension: %s" % ext
|
||||||
|
+ "\nCheck your config files in %s"
|
||||||
|
% ipy_utils.path.get_ipython_dir()
|
||||||
|
)
|
||||||
|
shell.showtraceback()
|
||||||
|
|
||||||
|
|
||||||
|
def embed(**kwargs):
|
||||||
|
"""
|
||||||
|
Copied from `IPython/terminal/embed.py`, but using our `InteractiveShellEmbed` instead.
|
||||||
|
"""
|
||||||
|
config = kwargs.get("config")
|
||||||
|
header = kwargs.pop("header", "")
|
||||||
|
compile_flags = kwargs.pop("compile_flags", None)
|
||||||
|
if config is None:
|
||||||
|
config = load_default_config()
|
||||||
|
config.InteractiveShellEmbed = config.TerminalInteractiveShell
|
||||||
|
kwargs["config"] = config
|
||||||
|
shell = InteractiveShellEmbed.instance(**kwargs)
|
||||||
|
initialize_extensions(shell, config["InteractiveShellApp"]["extensions"])
|
||||||
|
shell(header=header, stack_depth=2, compile_flags=compile_flags)
|
326
ptpython/key_bindings.py
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
from prompt_toolkit.application import get_app
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
from prompt_toolkit.enums import DEFAULT_BUFFER
|
||||||
|
from prompt_toolkit.filters import (
|
||||||
|
Condition,
|
||||||
|
emacs_insert_mode,
|
||||||
|
emacs_mode,
|
||||||
|
has_focus,
|
||||||
|
has_selection,
|
||||||
|
vi_insert_mode,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
|
from prompt_toolkit.key_binding.bindings.named_commands import get_by_name
|
||||||
|
from prompt_toolkit.keys import Keys
|
||||||
|
|
||||||
|
from .utils import document_is_multiline_python
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"load_python_bindings",
|
||||||
|
"load_sidebar_bindings",
|
||||||
|
"load_confirm_exit_bindings",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Condition
|
||||||
|
def tab_should_insert_whitespace():
|
||||||
|
"""
|
||||||
|
When the 'tab' key is pressed with only whitespace character before the
|
||||||
|
cursor, do autocompletion. Otherwise, insert indentation.
|
||||||
|
|
||||||
|
Except for the first character at the first line. Then always do a
|
||||||
|
completion. It doesn't make sense to start the first line with
|
||||||
|
indentation.
|
||||||
|
"""
|
||||||
|
b = get_app().current_buffer
|
||||||
|
before_cursor = b.document.current_line_before_cursor
|
||||||
|
|
||||||
|
return bool(b.text and (not before_cursor or before_cursor.isspace()))
|
||||||
|
|
||||||
|
|
||||||
|
def load_python_bindings(python_input):
|
||||||
|
"""
|
||||||
|
Custom key bindings.
|
||||||
|
"""
|
||||||
|
bindings = KeyBindings()
|
||||||
|
|
||||||
|
sidebar_visible = Condition(lambda: python_input.show_sidebar)
|
||||||
|
handle = bindings.add
|
||||||
|
|
||||||
|
@handle("c-l")
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Clear whole screen and render again -- also when the sidebar is visible.
|
||||||
|
"""
|
||||||
|
event.app.renderer.clear()
|
||||||
|
|
||||||
|
@handle("c-z")
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Suspend.
|
||||||
|
"""
|
||||||
|
if python_input.enable_system_bindings:
|
||||||
|
event.app.suspend_to_background()
|
||||||
|
|
||||||
|
# Delete word before cursor, but use all Python symbols as separators
|
||||||
|
# (WORD=False).
|
||||||
|
handle("c-w")(get_by_name("backward-kill-word"))
|
||||||
|
|
||||||
|
@handle("f2")
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Show/hide sidebar.
|
||||||
|
"""
|
||||||
|
python_input.show_sidebar = not python_input.show_sidebar
|
||||||
|
if python_input.show_sidebar:
|
||||||
|
event.app.layout.focus(python_input.ptpython_layout.sidebar)
|
||||||
|
else:
|
||||||
|
event.app.layout.focus_last()
|
||||||
|
|
||||||
|
@handle("f3")
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Select from the history.
|
||||||
|
"""
|
||||||
|
python_input.enter_history()
|
||||||
|
|
||||||
|
@handle("f4")
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Toggle between Vi and Emacs mode.
|
||||||
|
"""
|
||||||
|
python_input.vi_mode = not python_input.vi_mode
|
||||||
|
|
||||||
|
@handle("f6")
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Enable/Disable paste mode.
|
||||||
|
"""
|
||||||
|
python_input.paste_mode = not python_input.paste_mode
|
||||||
|
|
||||||
|
@handle(
|
||||||
|
"tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace
|
||||||
|
)
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
When tab should insert whitespace, do that instead of completion.
|
||||||
|
"""
|
||||||
|
event.app.current_buffer.insert_text(" ")
|
||||||
|
|
||||||
|
@Condition
|
||||||
|
def is_multiline():
|
||||||
|
return document_is_multiline_python(python_input.default_buffer.document)
|
||||||
|
|
||||||
|
@handle(
|
||||||
|
"enter",
|
||||||
|
filter=~sidebar_visible
|
||||||
|
& ~has_selection
|
||||||
|
& (vi_insert_mode | emacs_insert_mode)
|
||||||
|
& has_focus(DEFAULT_BUFFER)
|
||||||
|
& ~is_multiline,
|
||||||
|
)
|
||||||
|
@handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode)
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Accept input (for single line input).
|
||||||
|
"""
|
||||||
|
b = event.current_buffer
|
||||||
|
|
||||||
|
if b.validate():
|
||||||
|
# When the cursor is at the end, and we have an empty line:
|
||||||
|
# drop the empty lines, but return the value.
|
||||||
|
b.document = Document(
|
||||||
|
text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
|
||||||
|
)
|
||||||
|
|
||||||
|
b.validate_and_handle()
|
||||||
|
|
||||||
|
@handle(
|
||||||
|
"enter",
|
||||||
|
filter=~sidebar_visible
|
||||||
|
& ~has_selection
|
||||||
|
& (vi_insert_mode | emacs_insert_mode)
|
||||||
|
& has_focus(DEFAULT_BUFFER)
|
||||||
|
& is_multiline,
|
||||||
|
)
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Behaviour of the Enter key.
|
||||||
|
|
||||||
|
Auto indent after newline/Enter.
|
||||||
|
(When not in Vi navigaton mode, and when multiline is enabled.)
|
||||||
|
"""
|
||||||
|
b = event.current_buffer
|
||||||
|
empty_lines_required = python_input.accept_input_on_enter or 10000
|
||||||
|
|
||||||
|
def at_the_end(b):
|
||||||
|
"""we consider the cursor at the end when there is no text after
|
||||||
|
the cursor, or only whitespace."""
|
||||||
|
text = b.document.text_after_cursor
|
||||||
|
return text == "" or (text.isspace() and not "\n" in text)
|
||||||
|
|
||||||
|
if python_input.paste_mode:
|
||||||
|
# In paste mode, always insert text.
|
||||||
|
b.insert_text("\n")
|
||||||
|
|
||||||
|
elif at_the_end(b) and b.document.text.replace(" ", "").endswith(
|
||||||
|
"\n" * (empty_lines_required - 1)
|
||||||
|
):
|
||||||
|
# When the cursor is at the end, and we have an empty line:
|
||||||
|
# drop the empty lines, but return the value.
|
||||||
|
if b.validate():
|
||||||
|
b.document = Document(
|
||||||
|
text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
|
||||||
|
)
|
||||||
|
|
||||||
|
b.validate_and_handle()
|
||||||
|
else:
|
||||||
|
auto_newline(b)
|
||||||
|
|
||||||
|
@handle(
|
||||||
|
"c-d",
|
||||||
|
filter=~sidebar_visible
|
||||||
|
& has_focus(python_input.default_buffer)
|
||||||
|
& Condition(
|
||||||
|
lambda:
|
||||||
|
# The current buffer is empty.
|
||||||
|
not get_app().current_buffer.text
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Override Control-D exit, to ask for confirmation.
|
||||||
|
"""
|
||||||
|
if python_input.confirm_exit:
|
||||||
|
# Show exit confirmation and focus it (focusing is important for
|
||||||
|
# making sure the default buffer key bindings are not active).
|
||||||
|
python_input.show_exit_confirmation = True
|
||||||
|
python_input.app.layout.focus(
|
||||||
|
python_input.ptpython_layout.exit_confirmation
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
event.app.exit(exception=EOFError)
|
||||||
|
|
||||||
|
@handle("c-c", filter=has_focus(python_input.default_buffer))
|
||||||
|
def _(event):
|
||||||
|
" Abort when Control-C has been pressed. "
|
||||||
|
event.app.exit(exception=KeyboardInterrupt, style="class:aborting")
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
|
def load_sidebar_bindings(python_input):
|
||||||
|
"""
|
||||||
|
Load bindings for the navigation in the sidebar.
|
||||||
|
"""
|
||||||
|
bindings = KeyBindings()
|
||||||
|
|
||||||
|
handle = bindings.add
|
||||||
|
sidebar_visible = Condition(lambda: python_input.show_sidebar)
|
||||||
|
|
||||||
|
@handle("up", filter=sidebar_visible)
|
||||||
|
@handle("c-p", filter=sidebar_visible)
|
||||||
|
@handle("k", filter=sidebar_visible)
|
||||||
|
def _(event):
|
||||||
|
" Go to previous option. "
|
||||||
|
python_input.selected_option_index = (
|
||||||
|
python_input.selected_option_index - 1
|
||||||
|
) % python_input.option_count
|
||||||
|
|
||||||
|
@handle("down", filter=sidebar_visible)
|
||||||
|
@handle("c-n", filter=sidebar_visible)
|
||||||
|
@handle("j", filter=sidebar_visible)
|
||||||
|
def _(event):
|
||||||
|
" Go to next option. "
|
||||||
|
python_input.selected_option_index = (
|
||||||
|
python_input.selected_option_index + 1
|
||||||
|
) % python_input.option_count
|
||||||
|
|
||||||
|
@handle("right", filter=sidebar_visible)
|
||||||
|
@handle("l", filter=sidebar_visible)
|
||||||
|
@handle(" ", filter=sidebar_visible)
|
||||||
|
def _(event):
|
||||||
|
" Select next value for current option. "
|
||||||
|
option = python_input.selected_option
|
||||||
|
option.activate_next()
|
||||||
|
|
||||||
|
@handle("left", filter=sidebar_visible)
|
||||||
|
@handle("h", filter=sidebar_visible)
|
||||||
|
def _(event):
|
||||||
|
" Select previous value for current option. "
|
||||||
|
option = python_input.selected_option
|
||||||
|
option.activate_previous()
|
||||||
|
|
||||||
|
@handle("c-c", filter=sidebar_visible)
|
||||||
|
@handle("c-d", filter=sidebar_visible)
|
||||||
|
@handle("c-d", filter=sidebar_visible)
|
||||||
|
@handle("enter", filter=sidebar_visible)
|
||||||
|
@handle("escape", filter=sidebar_visible)
|
||||||
|
def _(event):
|
||||||
|
" Hide sidebar. "
|
||||||
|
python_input.show_sidebar = False
|
||||||
|
event.app.layout.focus_last()
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
|
def load_confirm_exit_bindings(python_input):
|
||||||
|
"""
|
||||||
|
Handle yes/no key presses when the exit confirmation is shown.
|
||||||
|
"""
|
||||||
|
bindings = KeyBindings()
|
||||||
|
|
||||||
|
handle = bindings.add
|
||||||
|
confirmation_visible = Condition(lambda: python_input.show_exit_confirmation)
|
||||||
|
|
||||||
|
@handle("y", filter=confirmation_visible)
|
||||||
|
@handle("Y", filter=confirmation_visible)
|
||||||
|
@handle("enter", filter=confirmation_visible)
|
||||||
|
@handle("c-d", filter=confirmation_visible)
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Really quit.
|
||||||
|
"""
|
||||||
|
event.app.exit(exception=EOFError, style="class:exiting")
|
||||||
|
|
||||||
|
@handle(Keys.Any, filter=confirmation_visible)
|
||||||
|
def _(event):
|
||||||
|
"""
|
||||||
|
Cancel exit.
|
||||||
|
"""
|
||||||
|
python_input.show_exit_confirmation = False
|
||||||
|
python_input.app.layout.focus_previous()
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
|
def auto_newline(buffer):
|
||||||
|
r"""
|
||||||
|
Insert \n at the cursor position. Also add necessary padding.
|
||||||
|
"""
|
||||||
|
insert_text = buffer.insert_text
|
||||||
|
|
||||||
|
if buffer.document.current_line_after_cursor:
|
||||||
|
# When we are in the middle of a line. Always insert a newline.
|
||||||
|
insert_text("\n")
|
||||||
|
else:
|
||||||
|
# Go to new line, but also add indentation.
|
||||||
|
current_line = buffer.document.current_line_before_cursor.rstrip()
|
||||||
|
insert_text("\n")
|
||||||
|
|
||||||
|
# Unident if the last line ends with 'pass', remove four spaces.
|
||||||
|
unindent = current_line.rstrip().endswith(" pass")
|
||||||
|
|
||||||
|
# Copy whitespace from current line
|
||||||
|
current_line2 = current_line[4:] if unindent else current_line
|
||||||
|
|
||||||
|
for c in current_line2:
|
||||||
|
if c.isspace():
|
||||||
|
insert_text(c)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# If the last line ends with a colon, add four extra spaces.
|
||||||
|
if current_line[-1:] == ":":
|
||||||
|
for x in range(4):
|
||||||
|
insert_text(" ")
|
763
ptpython/layout.py
Normal file
|
@ -0,0 +1,763 @@
|
||||||
|
"""
|
||||||
|
Creation of the `Layout` instance for the Python input/REPL.
|
||||||
|
"""
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
from enum import Enum
|
||||||
|
from inspect import _ParameterKind as ParameterKind
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from prompt_toolkit.application import get_app
|
||||||
|
from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
|
||||||
|
from prompt_toolkit.filters import (
|
||||||
|
Condition,
|
||||||
|
has_focus,
|
||||||
|
is_done,
|
||||||
|
renderer_height_is_known,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text
|
||||||
|
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
||||||
|
from prompt_toolkit.key_binding.vi_state import InputMode
|
||||||
|
from prompt_toolkit.layout.containers import (
|
||||||
|
ConditionalContainer,
|
||||||
|
Container,
|
||||||
|
Float,
|
||||||
|
FloatContainer,
|
||||||
|
HSplit,
|
||||||
|
ScrollOffsets,
|
||||||
|
VSplit,
|
||||||
|
Window,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
||||||
|
from prompt_toolkit.layout.dimension import AnyDimension, Dimension
|
||||||
|
from prompt_toolkit.layout.layout import Layout
|
||||||
|
from prompt_toolkit.layout.margins import PromptMargin
|
||||||
|
from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu
|
||||||
|
from prompt_toolkit.layout.processors import (
|
||||||
|
AppendAutoSuggestion,
|
||||||
|
ConditionalProcessor,
|
||||||
|
DisplayMultipleCursors,
|
||||||
|
HighlightIncrementalSearchProcessor,
|
||||||
|
HighlightMatchingBracketProcessor,
|
||||||
|
HighlightSelectionProcessor,
|
||||||
|
TabsProcessor,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.lexers import SimpleLexer
|
||||||
|
from prompt_toolkit.mouse_events import MouseEvent
|
||||||
|
from prompt_toolkit.selection import SelectionType
|
||||||
|
from prompt_toolkit.widgets.toolbars import (
|
||||||
|
ArgToolbar,
|
||||||
|
CompletionsToolbar,
|
||||||
|
SearchToolbar,
|
||||||
|
SystemToolbar,
|
||||||
|
ValidationToolbar,
|
||||||
|
)
|
||||||
|
from pygments.lexers import PythonLexer
|
||||||
|
|
||||||
|
from .filters import HasSignature, ShowDocstring, ShowSidebar, ShowSignature
|
||||||
|
from .utils import if_mousedown
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .python_input import OptionCategory, PythonInput
|
||||||
|
|
||||||
|
__all__ = ["PtPythonLayout", "CompletionVisualisation"]
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionVisualisation(Enum):
|
||||||
|
" Visualisation method for the completions. "
|
||||||
|
NONE = "none"
|
||||||
|
POP_UP = "pop-up"
|
||||||
|
MULTI_COLUMN = "multi-column"
|
||||||
|
TOOLBAR = "toolbar"
|
||||||
|
|
||||||
|
|
||||||
|
def show_completions_toolbar(python_input: "PythonInput") -> Condition:
|
||||||
|
return Condition(
|
||||||
|
lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def show_completions_menu(python_input: "PythonInput") -> Condition:
|
||||||
|
return Condition(
|
||||||
|
lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def show_multi_column_completions_menu(python_input: "PythonInput") -> Condition:
|
||||||
|
return Condition(
|
||||||
|
lambda: python_input.completion_visualisation
|
||||||
|
== CompletionVisualisation.MULTI_COLUMN
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def python_sidebar(python_input: "PythonInput") -> Window:
|
||||||
|
"""
|
||||||
|
Create the `Layout` for the sidebar with the configurable options.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_text_fragments() -> StyleAndTextTuples:
|
||||||
|
tokens: StyleAndTextTuples = []
|
||||||
|
|
||||||
|
def append_category(category: "OptionCategory") -> None:
|
||||||
|
tokens.extend(
|
||||||
|
[
|
||||||
|
("class:sidebar", " "),
|
||||||
|
("class:sidebar.title", " %-36s" % category.title),
|
||||||
|
("class:sidebar", "\n"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def append(index: int, label: str, status: str) -> None:
|
||||||
|
selected = index == python_input.selected_option_index
|
||||||
|
|
||||||
|
@if_mousedown
|
||||||
|
def select_item(mouse_event: MouseEvent) -> None:
|
||||||
|
python_input.selected_option_index = index
|
||||||
|
|
||||||
|
@if_mousedown
|
||||||
|
def goto_next(mouse_event: MouseEvent) -> None:
|
||||||
|
" Select item and go to next value. "
|
||||||
|
python_input.selected_option_index = index
|
||||||
|
option = python_input.selected_option
|
||||||
|
option.activate_next()
|
||||||
|
|
||||||
|
sel = ",selected" if selected else ""
|
||||||
|
|
||||||
|
tokens.append(("class:sidebar" + sel, " >" if selected else " "))
|
||||||
|
tokens.append(("class:sidebar.label" + sel, "%-24s" % label, select_item))
|
||||||
|
tokens.append(("class:sidebar.status" + sel, " ", select_item))
|
||||||
|
tokens.append(("class:sidebar.status" + sel, "%s" % status, goto_next))
|
||||||
|
|
||||||
|
if selected:
|
||||||
|
tokens.append(("[SetCursorPosition]", ""))
|
||||||
|
|
||||||
|
tokens.append(
|
||||||
|
("class:sidebar.status" + sel, " " * (13 - len(status)), goto_next)
|
||||||
|
)
|
||||||
|
tokens.append(("class:sidebar", "<" if selected else ""))
|
||||||
|
tokens.append(("class:sidebar", "\n"))
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for category in python_input.options:
|
||||||
|
append_category(category)
|
||||||
|
|
||||||
|
for option in category.options:
|
||||||
|
append(i, option.title, "%s" % option.get_current_value())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
tokens.pop() # Remove last newline.
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
class Control(FormattedTextControl):
|
||||||
|
def move_cursor_down(self):
|
||||||
|
python_input.selected_option_index += 1
|
||||||
|
|
||||||
|
def move_cursor_up(self):
|
||||||
|
python_input.selected_option_index -= 1
|
||||||
|
|
||||||
|
return Window(
|
||||||
|
Control(get_text_fragments),
|
||||||
|
style="class:sidebar",
|
||||||
|
width=Dimension.exact(43),
|
||||||
|
height=Dimension(min=3),
|
||||||
|
scroll_offsets=ScrollOffsets(top=1, bottom=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def python_sidebar_navigation(python_input):
|
||||||
|
"""
|
||||||
|
Create the `Layout` showing the navigation information for the sidebar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_text_fragments():
|
||||||
|
# Show navigation info.
|
||||||
|
return [
|
||||||
|
("class:sidebar", " "),
|
||||||
|
("class:sidebar.key", "[Arrows]"),
|
||||||
|
("class:sidebar", " "),
|
||||||
|
("class:sidebar.description", "Navigate"),
|
||||||
|
("class:sidebar", " "),
|
||||||
|
("class:sidebar.key", "[Enter]"),
|
||||||
|
("class:sidebar", " "),
|
||||||
|
("class:sidebar.description", "Hide menu"),
|
||||||
|
]
|
||||||
|
|
||||||
|
return Window(
|
||||||
|
FormattedTextControl(get_text_fragments),
|
||||||
|
style="class:sidebar",
|
||||||
|
width=Dimension.exact(43),
|
||||||
|
height=Dimension.exact(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def python_sidebar_help(python_input):
|
||||||
|
"""
|
||||||
|
Create the `Layout` for the help text for the current item in the sidebar.
|
||||||
|
"""
|
||||||
|
token = "class:sidebar.helptext"
|
||||||
|
|
||||||
|
def get_current_description():
|
||||||
|
"""
|
||||||
|
Return the description of the selected option.
|
||||||
|
"""
|
||||||
|
i = 0
|
||||||
|
for category in python_input.options:
|
||||||
|
for option in category.options:
|
||||||
|
if i == python_input.selected_option_index:
|
||||||
|
return option.description
|
||||||
|
i += 1
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_help_text():
|
||||||
|
return [(token, get_current_description())]
|
||||||
|
|
||||||
|
return ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
FormattedTextControl(get_help_text),
|
||||||
|
style=token,
|
||||||
|
height=Dimension(min=3),
|
||||||
|
wrap_lines=True,
|
||||||
|
),
|
||||||
|
filter=ShowSidebar(python_input)
|
||||||
|
& Condition(lambda: python_input.show_sidebar_help)
|
||||||
|
& ~is_done,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def signature_toolbar(python_input):
|
||||||
|
"""
|
||||||
|
Return the `Layout` for the signature.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_text_fragments() -> StyleAndTextTuples:
|
||||||
|
result: StyleAndTextTuples = []
|
||||||
|
append = result.append
|
||||||
|
Signature = "class:signature-toolbar"
|
||||||
|
|
||||||
|
if python_input.signatures:
|
||||||
|
sig = python_input.signatures[0] # Always take the first one.
|
||||||
|
|
||||||
|
append((Signature, " "))
|
||||||
|
try:
|
||||||
|
append((Signature, sig.name))
|
||||||
|
except IndexError:
|
||||||
|
# Workaround for #37: https://github.com/jonathanslenders/python-prompt-toolkit/issues/37
|
||||||
|
# See also: https://github.com/davidhalter/jedi/issues/490
|
||||||
|
return []
|
||||||
|
|
||||||
|
append((Signature + ",operator", "("))
|
||||||
|
|
||||||
|
got_positional_only = False
|
||||||
|
got_keyword_only = False
|
||||||
|
|
||||||
|
for i, p in enumerate(sig.parameters):
|
||||||
|
# Detect transition between positional-only and not positional-only.
|
||||||
|
if p.kind == ParameterKind.POSITIONAL_ONLY:
|
||||||
|
got_positional_only = True
|
||||||
|
if got_positional_only and p.kind != ParameterKind.POSITIONAL_ONLY:
|
||||||
|
got_positional_only = False
|
||||||
|
append((Signature, "/"))
|
||||||
|
append((Signature + ",operator", ", "))
|
||||||
|
|
||||||
|
if not got_keyword_only and p.kind == ParameterKind.KEYWORD_ONLY:
|
||||||
|
got_keyword_only = True
|
||||||
|
append((Signature, "*"))
|
||||||
|
append((Signature + ",operator", ", "))
|
||||||
|
|
||||||
|
sig_index = getattr(sig, "index", 0)
|
||||||
|
|
||||||
|
if i == sig_index:
|
||||||
|
# Note: we use `_Param.description` instead of
|
||||||
|
# `_Param.name`, that way we also get the '*' before args.
|
||||||
|
append((Signature + ",current-name", p.description))
|
||||||
|
else:
|
||||||
|
append((Signature, p.description))
|
||||||
|
|
||||||
|
if p.default:
|
||||||
|
# NOTE: For the jedi-based completion, the default is
|
||||||
|
# currently still part of the name.
|
||||||
|
append((Signature, f"={p.default}"))
|
||||||
|
|
||||||
|
append((Signature + ",operator", ", "))
|
||||||
|
|
||||||
|
if sig.parameters:
|
||||||
|
# Pop last comma
|
||||||
|
result.pop()
|
||||||
|
|
||||||
|
append((Signature + ",operator", ")"))
|
||||||
|
append((Signature, " "))
|
||||||
|
return result
|
||||||
|
|
||||||
|
return ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
FormattedTextControl(get_text_fragments), height=Dimension.exact(1)
|
||||||
|
),
|
||||||
|
filter=
|
||||||
|
# Show only when there is a signature
|
||||||
|
HasSignature(python_input) &
|
||||||
|
# Signature needs to be shown.
|
||||||
|
ShowSignature(python_input) &
|
||||||
|
# And no sidebar is visible.
|
||||||
|
~ShowSidebar(python_input) &
|
||||||
|
# Not done yet.
|
||||||
|
~is_done,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PythonPromptMargin(PromptMargin):
|
||||||
|
"""
|
||||||
|
Create margin that displays the prompt.
|
||||||
|
It shows something like "In [1]:".
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, python_input) -> None:
|
||||||
|
self.python_input = python_input
|
||||||
|
|
||||||
|
def get_prompt_style():
|
||||||
|
return python_input.all_prompt_styles[python_input.prompt_style]
|
||||||
|
|
||||||
|
def get_prompt() -> StyleAndTextTuples:
|
||||||
|
return to_formatted_text(get_prompt_style().in_prompt())
|
||||||
|
|
||||||
|
def get_continuation(width, line_number, is_soft_wrap):
|
||||||
|
if python_input.show_line_numbers and not is_soft_wrap:
|
||||||
|
text = ("%i " % (line_number + 1)).rjust(width)
|
||||||
|
return [("class:line-number", text)]
|
||||||
|
else:
|
||||||
|
return get_prompt_style().in2_prompt(width)
|
||||||
|
|
||||||
|
super().__init__(get_prompt, get_continuation)
|
||||||
|
|
||||||
|
|
||||||
|
def status_bar(python_input: "PythonInput") -> Container:
|
||||||
|
"""
|
||||||
|
Create the `Layout` for the status bar.
|
||||||
|
"""
|
||||||
|
TB = "class:status-toolbar"
|
||||||
|
|
||||||
|
@if_mousedown
|
||||||
|
def toggle_paste_mode(mouse_event: MouseEvent) -> None:
|
||||||
|
python_input.paste_mode = not python_input.paste_mode
|
||||||
|
|
||||||
|
@if_mousedown
|
||||||
|
def enter_history(mouse_event: MouseEvent) -> None:
|
||||||
|
python_input.enter_history()
|
||||||
|
|
||||||
|
def get_text_fragments() -> StyleAndTextTuples:
|
||||||
|
python_buffer = python_input.default_buffer
|
||||||
|
|
||||||
|
result: StyleAndTextTuples = []
|
||||||
|
append = result.append
|
||||||
|
|
||||||
|
append((TB, " "))
|
||||||
|
result.extend(get_inputmode_fragments(python_input))
|
||||||
|
append((TB, " "))
|
||||||
|
|
||||||
|
# Position in history.
|
||||||
|
append(
|
||||||
|
(
|
||||||
|
TB,
|
||||||
|
"%i/%i "
|
||||||
|
% (python_buffer.working_index + 1, len(python_buffer._working_lines)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Shortcuts.
|
||||||
|
app = get_app()
|
||||||
|
if (
|
||||||
|
not python_input.vi_mode
|
||||||
|
and app.current_buffer == python_input.search_buffer
|
||||||
|
):
|
||||||
|
append((TB, "[Ctrl-G] Cancel search [Enter] Go to this position."))
|
||||||
|
elif bool(app.current_buffer.selection_state) and not python_input.vi_mode:
|
||||||
|
# Emacs cut/copy keys.
|
||||||
|
append((TB, "[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel"))
|
||||||
|
else:
|
||||||
|
result.extend(
|
||||||
|
[
|
||||||
|
(TB + " class:status-toolbar.key", "[F3]", enter_history),
|
||||||
|
(TB, " History ", enter_history),
|
||||||
|
(TB + " class:status-toolbar.key", "[F6]", toggle_paste_mode),
|
||||||
|
(TB, " ", toggle_paste_mode),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if python_input.paste_mode:
|
||||||
|
append(
|
||||||
|
(TB + " class:paste-mode-on", "Paste mode (on)", toggle_paste_mode)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
append((TB, "Paste mode", toggle_paste_mode))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
return ConditionalContainer(
|
||||||
|
content=Window(content=FormattedTextControl(get_text_fragments), style=TB),
|
||||||
|
filter=~is_done
|
||||||
|
& renderer_height_is_known
|
||||||
|
& Condition(
|
||||||
|
lambda: python_input.show_status_bar
|
||||||
|
and not python_input.show_exit_confirmation
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples:
|
||||||
|
"""
|
||||||
|
Return current input mode as a list of (token, text) tuples for use in a
|
||||||
|
toolbar.
|
||||||
|
"""
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
|
@if_mousedown
|
||||||
|
def toggle_vi_mode(mouse_event: MouseEvent) -> None:
|
||||||
|
python_input.vi_mode = not python_input.vi_mode
|
||||||
|
|
||||||
|
token = "class:status-toolbar"
|
||||||
|
input_mode_t = "class:status-toolbar.input-mode"
|
||||||
|
|
||||||
|
mode = app.vi_state.input_mode
|
||||||
|
result: StyleAndTextTuples = []
|
||||||
|
append = result.append
|
||||||
|
|
||||||
|
if python_input.title:
|
||||||
|
result.extend(to_formatted_text(python_input.title))
|
||||||
|
|
||||||
|
append((input_mode_t, "[F4] ", toggle_vi_mode))
|
||||||
|
|
||||||
|
# InputMode
|
||||||
|
if python_input.vi_mode:
|
||||||
|
recording_register = app.vi_state.recording_register
|
||||||
|
if recording_register:
|
||||||
|
append((token, " "))
|
||||||
|
append((token + " class:record", "RECORD({})".format(recording_register)))
|
||||||
|
append((token, " - "))
|
||||||
|
|
||||||
|
if app.current_buffer.selection_state is not None:
|
||||||
|
if app.current_buffer.selection_state.type == SelectionType.LINES:
|
||||||
|
append((input_mode_t, "Vi (VISUAL LINE)", toggle_vi_mode))
|
||||||
|
elif app.current_buffer.selection_state.type == SelectionType.CHARACTERS:
|
||||||
|
append((input_mode_t, "Vi (VISUAL)", toggle_vi_mode))
|
||||||
|
append((token, " "))
|
||||||
|
elif app.current_buffer.selection_state.type == SelectionType.BLOCK:
|
||||||
|
append((input_mode_t, "Vi (VISUAL BLOCK)", toggle_vi_mode))
|
||||||
|
append((token, " "))
|
||||||
|
elif mode in (InputMode.INSERT, "vi-insert-multiple"):
|
||||||
|
append((input_mode_t, "Vi (INSERT)", toggle_vi_mode))
|
||||||
|
append((token, " "))
|
||||||
|
elif mode == InputMode.NAVIGATION:
|
||||||
|
append((input_mode_t, "Vi (NAV)", toggle_vi_mode))
|
||||||
|
append((token, " "))
|
||||||
|
elif mode == InputMode.REPLACE:
|
||||||
|
append((input_mode_t, "Vi (REPLACE)", toggle_vi_mode))
|
||||||
|
append((token, " "))
|
||||||
|
else:
|
||||||
|
if app.emacs_state.is_recording:
|
||||||
|
append((token, " "))
|
||||||
|
append((token + " class:record", "RECORD"))
|
||||||
|
append((token, " - "))
|
||||||
|
|
||||||
|
append((input_mode_t, "Emacs", toggle_vi_mode))
|
||||||
|
append((token, " "))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def show_sidebar_button_info(python_input: "PythonInput") -> Container:
|
||||||
|
"""
|
||||||
|
Create `Layout` for the information in the right-bottom corner.
|
||||||
|
(The right part of the status bar.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@if_mousedown
|
||||||
|
def toggle_sidebar(mouse_event: MouseEvent) -> None:
|
||||||
|
" Click handler for the menu. "
|
||||||
|
python_input.show_sidebar = not python_input.show_sidebar
|
||||||
|
|
||||||
|
version = sys.version_info
|
||||||
|
tokens: StyleAndTextTuples = [
|
||||||
|
("class:status-toolbar.key", "[F2]", toggle_sidebar),
|
||||||
|
("class:status-toolbar", " Menu", toggle_sidebar),
|
||||||
|
("class:status-toolbar", " - "),
|
||||||
|
(
|
||||||
|
"class:status-toolbar.python-version",
|
||||||
|
"%s %i.%i.%i"
|
||||||
|
% (platform.python_implementation(), version[0], version[1], version[2]),
|
||||||
|
),
|
||||||
|
("class:status-toolbar", " "),
|
||||||
|
]
|
||||||
|
width = fragment_list_width(tokens)
|
||||||
|
|
||||||
|
def get_text_fragments() -> StyleAndTextTuples:
|
||||||
|
# Python version
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
return ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
FormattedTextControl(get_text_fragments),
|
||||||
|
style="class:status-toolbar",
|
||||||
|
height=Dimension.exact(1),
|
||||||
|
width=Dimension.exact(width),
|
||||||
|
),
|
||||||
|
filter=~is_done
|
||||||
|
& renderer_height_is_known
|
||||||
|
& Condition(
|
||||||
|
lambda: python_input.show_status_bar
|
||||||
|
and not python_input.show_exit_confirmation
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_exit_confirmation(
|
||||||
|
python_input: "PythonInput", style="class:exit-confirmation"
|
||||||
|
) -> Container:
|
||||||
|
"""
|
||||||
|
Create `Layout` for the exit message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_text_fragments() -> StyleAndTextTuples:
|
||||||
|
# Show "Do you really want to exit?"
|
||||||
|
return [
|
||||||
|
(style, "\n %s ([y]/n) " % python_input.exit_message),
|
||||||
|
("[SetCursorPosition]", ""),
|
||||||
|
(style, " \n"),
|
||||||
|
]
|
||||||
|
|
||||||
|
visible = ~is_done & Condition(lambda: python_input.show_exit_confirmation)
|
||||||
|
|
||||||
|
return ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
FormattedTextControl(get_text_fragments, focusable=True), style=style
|
||||||
|
),
|
||||||
|
filter=visible,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def meta_enter_message(python_input: "PythonInput") -> Container:
|
||||||
|
"""
|
||||||
|
Create the `Layout` for the 'Meta+Enter` message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_text_fragments() -> StyleAndTextTuples:
|
||||||
|
return [("class:accept-message", " [Meta+Enter] Execute ")]
|
||||||
|
|
||||||
|
@Condition
|
||||||
|
def extra_condition() -> bool:
|
||||||
|
" Only show when... "
|
||||||
|
b = python_input.default_buffer
|
||||||
|
|
||||||
|
return (
|
||||||
|
python_input.show_meta_enter_message
|
||||||
|
and (
|
||||||
|
not b.document.is_cursor_at_the_end
|
||||||
|
or python_input.accept_input_on_enter is None
|
||||||
|
)
|
||||||
|
and "\n" in b.text
|
||||||
|
)
|
||||||
|
|
||||||
|
visible = ~is_done & has_focus(DEFAULT_BUFFER) & extra_condition
|
||||||
|
|
||||||
|
return ConditionalContainer(
|
||||||
|
content=Window(FormattedTextControl(get_text_fragments)), filter=visible
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PtPythonLayout:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
python_input: "PythonInput",
|
||||||
|
lexer=PythonLexer,
|
||||||
|
extra_body=None,
|
||||||
|
extra_toolbars=None,
|
||||||
|
extra_buffer_processors=None,
|
||||||
|
input_buffer_height: Optional[AnyDimension] = None,
|
||||||
|
) -> None:
|
||||||
|
D = Dimension
|
||||||
|
extra_body = [extra_body] if extra_body else []
|
||||||
|
extra_toolbars = extra_toolbars or []
|
||||||
|
extra_buffer_processors = extra_buffer_processors or []
|
||||||
|
input_buffer_height = input_buffer_height or D(min=6)
|
||||||
|
|
||||||
|
search_toolbar = SearchToolbar(python_input.search_buffer)
|
||||||
|
|
||||||
|
def create_python_input_window():
|
||||||
|
def menu_position():
|
||||||
|
"""
|
||||||
|
When there is no autocompletion menu to be shown, and we have a
|
||||||
|
signature, set the pop-up position at `bracket_start`.
|
||||||
|
"""
|
||||||
|
b = python_input.default_buffer
|
||||||
|
|
||||||
|
if python_input.signatures:
|
||||||
|
row, col = python_input.signatures[0].bracket_start
|
||||||
|
index = b.document.translate_row_col_to_index(row - 1, col)
|
||||||
|
return index
|
||||||
|
|
||||||
|
return Window(
|
||||||
|
BufferControl(
|
||||||
|
buffer=python_input.default_buffer,
|
||||||
|
search_buffer_control=search_toolbar.control,
|
||||||
|
lexer=lexer,
|
||||||
|
include_default_input_processors=False,
|
||||||
|
input_processors=[
|
||||||
|
ConditionalProcessor(
|
||||||
|
processor=HighlightIncrementalSearchProcessor(),
|
||||||
|
filter=has_focus(SEARCH_BUFFER)
|
||||||
|
| has_focus(search_toolbar.control),
|
||||||
|
),
|
||||||
|
HighlightSelectionProcessor(),
|
||||||
|
DisplayMultipleCursors(),
|
||||||
|
TabsProcessor(),
|
||||||
|
# Show matching parentheses, but only while editing.
|
||||||
|
ConditionalProcessor(
|
||||||
|
processor=HighlightMatchingBracketProcessor(chars="[](){}"),
|
||||||
|
filter=has_focus(DEFAULT_BUFFER)
|
||||||
|
& ~is_done
|
||||||
|
& Condition(
|
||||||
|
lambda: python_input.highlight_matching_parenthesis
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ConditionalProcessor(
|
||||||
|
processor=AppendAutoSuggestion(), filter=~is_done
|
||||||
|
),
|
||||||
|
]
|
||||||
|
+ extra_buffer_processors,
|
||||||
|
menu_position=menu_position,
|
||||||
|
# Make sure that we always see the result of an reverse-i-search:
|
||||||
|
preview_search=True,
|
||||||
|
),
|
||||||
|
left_margins=[PythonPromptMargin(python_input)],
|
||||||
|
# Scroll offsets. The 1 at the bottom is important to make sure
|
||||||
|
# the cursor is never below the "Press [Meta+Enter]" message
|
||||||
|
# which is a float.
|
||||||
|
scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4),
|
||||||
|
# As long as we're editing, prefer a minimal height of 6.
|
||||||
|
height=(
|
||||||
|
lambda: (
|
||||||
|
None
|
||||||
|
if get_app().is_done or python_input.show_exit_confirmation
|
||||||
|
else input_buffer_height
|
||||||
|
)
|
||||||
|
),
|
||||||
|
wrap_lines=Condition(lambda: python_input.wrap_lines),
|
||||||
|
)
|
||||||
|
|
||||||
|
sidebar = python_sidebar(python_input)
|
||||||
|
self.exit_confirmation = create_exit_confirmation(python_input)
|
||||||
|
|
||||||
|
root_container = HSplit(
|
||||||
|
[
|
||||||
|
VSplit(
|
||||||
|
[
|
||||||
|
HSplit(
|
||||||
|
[
|
||||||
|
FloatContainer(
|
||||||
|
content=HSplit(
|
||||||
|
[create_python_input_window()] + extra_body
|
||||||
|
),
|
||||||
|
floats=[
|
||||||
|
Float(
|
||||||
|
xcursor=True,
|
||||||
|
ycursor=True,
|
||||||
|
content=HSplit(
|
||||||
|
[
|
||||||
|
signature_toolbar(python_input),
|
||||||
|
ConditionalContainer(
|
||||||
|
content=CompletionsMenu(
|
||||||
|
scroll_offset=(
|
||||||
|
lambda: python_input.completion_menu_scroll_offset
|
||||||
|
),
|
||||||
|
max_height=12,
|
||||||
|
),
|
||||||
|
filter=show_completions_menu(
|
||||||
|
python_input
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ConditionalContainer(
|
||||||
|
content=MultiColumnCompletionsMenu(),
|
||||||
|
filter=show_multi_column_completions_menu(
|
||||||
|
python_input
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Float(
|
||||||
|
left=2,
|
||||||
|
bottom=1,
|
||||||
|
content=self.exit_confirmation,
|
||||||
|
),
|
||||||
|
Float(
|
||||||
|
bottom=0,
|
||||||
|
right=0,
|
||||||
|
height=1,
|
||||||
|
content=meta_enter_message(python_input),
|
||||||
|
hide_when_covering_content=True,
|
||||||
|
),
|
||||||
|
Float(
|
||||||
|
bottom=1,
|
||||||
|
left=1,
|
||||||
|
right=0,
|
||||||
|
content=python_sidebar_help(python_input),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ArgToolbar(),
|
||||||
|
search_toolbar,
|
||||||
|
SystemToolbar(),
|
||||||
|
ValidationToolbar(),
|
||||||
|
ConditionalContainer(
|
||||||
|
content=CompletionsToolbar(),
|
||||||
|
filter=show_completions_toolbar(python_input)
|
||||||
|
& ~is_done,
|
||||||
|
),
|
||||||
|
# Docstring region.
|
||||||
|
ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
height=D.exact(1),
|
||||||
|
char="\u2500",
|
||||||
|
style="class:separator",
|
||||||
|
),
|
||||||
|
filter=HasSignature(python_input)
|
||||||
|
& ShowDocstring(python_input)
|
||||||
|
& ~is_done,
|
||||||
|
),
|
||||||
|
ConditionalContainer(
|
||||||
|
content=Window(
|
||||||
|
BufferControl(
|
||||||
|
buffer=python_input.docstring_buffer,
|
||||||
|
lexer=SimpleLexer(style="class:docstring"),
|
||||||
|
# lexer=PythonLexer,
|
||||||
|
),
|
||||||
|
height=D(max=12),
|
||||||
|
),
|
||||||
|
filter=HasSignature(python_input)
|
||||||
|
& ShowDocstring(python_input)
|
||||||
|
& ~is_done,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
ConditionalContainer(
|
||||||
|
content=HSplit(
|
||||||
|
[
|
||||||
|
sidebar,
|
||||||
|
Window(style="class:sidebar,separator", height=1),
|
||||||
|
python_sidebar_navigation(python_input),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
filter=ShowSidebar(python_input) & ~is_done,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
+ extra_toolbars
|
||||||
|
+ [
|
||||||
|
VSplit(
|
||||||
|
[status_bar(python_input), show_sidebar_button_info(python_input)]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.layout = Layout(root_container)
|
||||||
|
self.sidebar = sidebar
|
28
ptpython/lexer.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
||||||
|
from prompt_toolkit.lexers import Lexer, PygmentsLexer
|
||||||
|
from pygments.lexers import BashLexer
|
||||||
|
from pygments.lexers import Python3Lexer as PythonLexer
|
||||||
|
|
||||||
|
__all__ = ["PtpythonLexer"]
|
||||||
|
|
||||||
|
|
||||||
|
class PtpythonLexer(Lexer):
|
||||||
|
"""
|
||||||
|
Lexer for ptpython input.
|
||||||
|
|
||||||
|
If the input starts with an exclamation mark, use a Bash lexer, otherwise,
|
||||||
|
use a Python 3 lexer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, python_lexer: Optional[Lexer] = None) -> None:
|
||||||
|
self.python_lexer = python_lexer or PygmentsLexer(PythonLexer)
|
||||||
|
self.system_lexer = PygmentsLexer(BashLexer)
|
||||||
|
|
||||||
|
def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]:
|
||||||
|
if document.text.startswith("!"):
|
||||||
|
return self.system_lexer.lex_document(document)
|
||||||
|
|
||||||
|
return self.python_lexer.lex_document(document)
|
77
ptpython/prompt_style.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from prompt_toolkit.formatted_text import AnyFormattedText
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .python_input import PythonInput
|
||||||
|
|
||||||
|
__all__ = ["PromptStyle", "IPythonPrompt", "ClassicPrompt"]
|
||||||
|
|
||||||
|
|
||||||
|
class PromptStyle(metaclass=ABCMeta):
|
||||||
|
"""
|
||||||
|
Base class for all prompts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def in_prompt(self) -> AnyFormattedText:
|
||||||
|
" Return the input tokens. "
|
||||||
|
return []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def in2_prompt(self, width: int) -> AnyFormattedText:
|
||||||
|
"""
|
||||||
|
Tokens for every following input line.
|
||||||
|
|
||||||
|
:param width: The available width. This is coming from the width taken
|
||||||
|
by `in_prompt`.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def out_prompt(self) -> AnyFormattedText:
|
||||||
|
" Return the output tokens. "
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class IPythonPrompt(PromptStyle):
|
||||||
|
"""
|
||||||
|
A prompt resembling the IPython prompt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, python_input: "PythonInput") -> None:
|
||||||
|
self.python_input = python_input
|
||||||
|
|
||||||
|
def in_prompt(self) -> AnyFormattedText:
|
||||||
|
return [
|
||||||
|
("class:in", "In ["),
|
||||||
|
("class:in.number", "%s" % self.python_input.current_statement_index),
|
||||||
|
("class:in", "]: "),
|
||||||
|
]
|
||||||
|
|
||||||
|
def in2_prompt(self, width: int) -> AnyFormattedText:
|
||||||
|
return [("class:in", "...: ".rjust(width))]
|
||||||
|
|
||||||
|
def out_prompt(self) -> AnyFormattedText:
|
||||||
|
return [
|
||||||
|
("class:out", "Out["),
|
||||||
|
("class:out.number", "%s" % self.python_input.current_statement_index),
|
||||||
|
("class:out", "]:"),
|
||||||
|
("", " "),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClassicPrompt(PromptStyle):
|
||||||
|
"""
|
||||||
|
The classic Python prompt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def in_prompt(self) -> AnyFormattedText:
|
||||||
|
return [("class:prompt", ">>> ")]
|
||||||
|
|
||||||
|
def in2_prompt(self, width: int) -> AnyFormattedText:
|
||||||
|
return [("class:prompt.dots", "...")]
|
||||||
|
|
||||||
|
def out_prompt(self) -> AnyFormattedText:
|
||||||
|
return []
|
0
ptpython/py.typed
Normal file
1054
ptpython/python_input.py
Normal file
765
ptpython/repl.py
Normal file
|
@ -0,0 +1,765 @@
|
||||||
|
"""
|
||||||
|
Utility for creating a Python repl.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from ptpython.repl import embed
|
||||||
|
embed(globals(), locals(), vi_mode=False)
|
||||||
|
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import builtins
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import types
|
||||||
|
import warnings
|
||||||
|
from dis import COMPILER_FLAG_NAMES
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Callable, ContextManager, Dict, Optional
|
||||||
|
|
||||||
|
from prompt_toolkit.formatted_text import (
|
||||||
|
HTML,
|
||||||
|
AnyFormattedText,
|
||||||
|
FormattedText,
|
||||||
|
PygmentsTokens,
|
||||||
|
StyleAndTextTuples,
|
||||||
|
fragment_list_width,
|
||||||
|
merge_formatted_text,
|
||||||
|
to_formatted_text,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.formatted_text.utils import fragment_list_to_text, split_lines
|
||||||
|
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
|
||||||
|
from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context
|
||||||
|
from prompt_toolkit.shortcuts import (
|
||||||
|
PromptSession,
|
||||||
|
clear_title,
|
||||||
|
print_formatted_text,
|
||||||
|
set_title,
|
||||||
|
)
|
||||||
|
from prompt_toolkit.styles import BaseStyle
|
||||||
|
from prompt_toolkit.utils import DummyContext, get_cwidth
|
||||||
|
from pygments.lexers import PythonLexer, PythonTracebackLexer
|
||||||
|
from pygments.token import Token
|
||||||
|
|
||||||
|
from .python_input import PythonInput
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
PyCF_ALLOW_TOP_LEVEL_AWAIT = 0
|
||||||
|
|
||||||
|
__all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_coroutine_flag() -> Optional[int]:
|
||||||
|
for k, v in COMPILER_FLAG_NAMES.items():
|
||||||
|
if v == "COROUTINE":
|
||||||
|
return k
|
||||||
|
|
||||||
|
# Flag not found.
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
COROUTINE_FLAG: Optional[int] = _get_coroutine_flag()
|
||||||
|
|
||||||
|
|
||||||
|
def _has_coroutine_flag(code: types.CodeType) -> bool:
|
||||||
|
if COROUTINE_FLAG is None:
|
||||||
|
# Not supported on this Python version.
|
||||||
|
return False
|
||||||
|
|
||||||
|
return bool(code.co_flags & COROUTINE_FLAG)
|
||||||
|
|
||||||
|
|
||||||
|
class PythonRepl(PythonInput):
|
||||||
|
def __init__(self, *a, **kw) -> None:
|
||||||
|
self._startup_paths = kw.pop("startup_paths", None)
|
||||||
|
super().__init__(*a, **kw)
|
||||||
|
self._load_start_paths()
|
||||||
|
|
||||||
|
def _load_start_paths(self) -> None:
|
||||||
|
" Start the Read-Eval-Print Loop. "
|
||||||
|
if self._startup_paths:
|
||||||
|
for path in self._startup_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
code = compile(f.read(), path, "exec")
|
||||||
|
exec(code, self.get_globals(), self.get_locals())
|
||||||
|
else:
|
||||||
|
output = self.app.output
|
||||||
|
output.write("WARNING | File not found: {}\n\n".format(path))
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""
|
||||||
|
Run the REPL loop.
|
||||||
|
"""
|
||||||
|
if self.terminal_title:
|
||||||
|
set_title(self.terminal_title)
|
||||||
|
|
||||||
|
self._add_to_namespace()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Read.
|
||||||
|
try:
|
||||||
|
text = self.read()
|
||||||
|
except EOFError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Eval.
|
||||||
|
try:
|
||||||
|
result = self.eval(text)
|
||||||
|
except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception.
|
||||||
|
raise
|
||||||
|
except SystemExit:
|
||||||
|
return
|
||||||
|
except BaseException as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
else:
|
||||||
|
# Print.
|
||||||
|
if result is not None:
|
||||||
|
self.show_result(result)
|
||||||
|
|
||||||
|
# Loop.
|
||||||
|
self.current_statement_index += 1
|
||||||
|
self.signatures = []
|
||||||
|
|
||||||
|
except KeyboardInterrupt as e:
|
||||||
|
# Handle all possible `KeyboardInterrupt` errors. This can
|
||||||
|
# happen during the `eval`, but also during the
|
||||||
|
# `show_result` if something takes too long.
|
||||||
|
# (Try/catch is around the whole block, because we want to
|
||||||
|
# prevent that a Control-C keypress terminates the REPL in
|
||||||
|
# any case.)
|
||||||
|
self._handle_keyboard_interrupt(e)
|
||||||
|
finally:
|
||||||
|
if self.terminal_title:
|
||||||
|
clear_title()
|
||||||
|
self._remove_from_namespace()
|
||||||
|
|
||||||
|
async def run_async(self) -> None:
|
||||||
|
"""
|
||||||
|
Run the REPL loop, but run the blocking parts in an executor, so that
|
||||||
|
we don't block the event loop. Both the input and output (which can
|
||||||
|
display a pager) will run in a separate thread with their own event
|
||||||
|
loop, this way ptpython's own event loop won't interfere with the
|
||||||
|
asyncio event loop from where this is called.
|
||||||
|
|
||||||
|
The "eval" however happens in the current thread, which is important.
|
||||||
|
(Both for control-C to work, as well as for the code to see the right
|
||||||
|
thread in which it was embedded).
|
||||||
|
"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
if self.terminal_title:
|
||||||
|
set_title(self.terminal_title)
|
||||||
|
|
||||||
|
self._add_to_namespace()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Read.
|
||||||
|
try:
|
||||||
|
text = await loop.run_in_executor(None, self.read)
|
||||||
|
except EOFError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Eval.
|
||||||
|
try:
|
||||||
|
result = await self.eval_async(text)
|
||||||
|
except KeyboardInterrupt as e: # KeyboardInterrupt doesn't inherit from Exception.
|
||||||
|
raise
|
||||||
|
except SystemExit:
|
||||||
|
return
|
||||||
|
except BaseException as e:
|
||||||
|
self._handle_exception(e)
|
||||||
|
else:
|
||||||
|
# Print.
|
||||||
|
if result is not None:
|
||||||
|
await loop.run_in_executor(
|
||||||
|
None, lambda: self.show_result(result)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Loop.
|
||||||
|
self.current_statement_index += 1
|
||||||
|
self.signatures = []
|
||||||
|
|
||||||
|
except KeyboardInterrupt as e:
|
||||||
|
# XXX: This does not yet work properly. In some situations,
|
||||||
|
# `KeyboardInterrupt` exceptions can end up in the event
|
||||||
|
# loop selector.
|
||||||
|
self._handle_keyboard_interrupt(e)
|
||||||
|
finally:
|
||||||
|
if self.terminal_title:
|
||||||
|
clear_title()
|
||||||
|
self._remove_from_namespace()
|
||||||
|
|
||||||
|
def eval(self, line: str) -> object:
|
||||||
|
"""
|
||||||
|
Evaluate the line and print the result.
|
||||||
|
"""
|
||||||
|
# WORKAROUND: Due to a bug in Jedi, the current directory is removed
|
||||||
|
# from sys.path. See: https://github.com/davidhalter/jedi/issues/1148
|
||||||
|
if "" not in sys.path:
|
||||||
|
sys.path.insert(0, "")
|
||||||
|
|
||||||
|
if line.lstrip().startswith("!"):
|
||||||
|
# Run as shell command
|
||||||
|
os.system(line[1:])
|
||||||
|
else:
|
||||||
|
# Try eval first
|
||||||
|
try:
|
||||||
|
code = self._compile_with_flags(line, "eval")
|
||||||
|
except SyntaxError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# No syntax errors for eval. Do eval.
|
||||||
|
result = eval(code, self.get_globals(), self.get_locals())
|
||||||
|
|
||||||
|
if _has_coroutine_flag(code):
|
||||||
|
result = asyncio.get_event_loop().run_until_complete(result)
|
||||||
|
|
||||||
|
self._store_eval_result(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# If not a valid `eval` expression, run using `exec` instead.
|
||||||
|
# Note that we shouldn't run this in the `except SyntaxError` block
|
||||||
|
# above, then `sys.exc_info()` would not report the right error.
|
||||||
|
# See issue: https://github.com/prompt-toolkit/ptpython/issues/435
|
||||||
|
code = self._compile_with_flags(line, "exec")
|
||||||
|
exec(code, self.get_globals(), self.get_locals())
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def eval_async(self, line: str) -> object:
|
||||||
|
"""
|
||||||
|
Evaluate the line and print the result.
|
||||||
|
"""
|
||||||
|
# WORKAROUND: Due to a bug in Jedi, the current directory is removed
|
||||||
|
# from sys.path. See: https://github.com/davidhalter/jedi/issues/1148
|
||||||
|
if "" not in sys.path:
|
||||||
|
sys.path.insert(0, "")
|
||||||
|
|
||||||
|
if line.lstrip().startswith("!"):
|
||||||
|
# Run as shell command
|
||||||
|
os.system(line[1:])
|
||||||
|
else:
|
||||||
|
# Try eval first
|
||||||
|
try:
|
||||||
|
code = self._compile_with_flags(line, "eval")
|
||||||
|
except SyntaxError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# No syntax errors for eval. Do eval.
|
||||||
|
result = eval(code, self.get_globals(), self.get_locals())
|
||||||
|
|
||||||
|
if _has_coroutine_flag(code):
|
||||||
|
result = await result
|
||||||
|
|
||||||
|
self._store_eval_result(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# If not a valid `eval` expression, run using `exec` instead.
|
||||||
|
code = self._compile_with_flags(line, "exec")
|
||||||
|
exec(code, self.get_globals(), self.get_locals())
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _store_eval_result(self, result: object) -> None:
|
||||||
|
locals: Dict[str, Any] = self.get_locals()
|
||||||
|
locals["_"] = locals["_%i" % self.current_statement_index] = result
|
||||||
|
|
||||||
|
def get_compiler_flags(self) -> int:
|
||||||
|
return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT
|
||||||
|
|
||||||
|
def _compile_with_flags(self, code: str, mode: str):
|
||||||
|
" Compile code with the right compiler flags. "
|
||||||
|
return compile(
|
||||||
|
code,
|
||||||
|
"<stdin>",
|
||||||
|
mode,
|
||||||
|
flags=self.get_compiler_flags(),
|
||||||
|
dont_inherit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def show_result(self, result: object) -> None:
|
||||||
|
"""
|
||||||
|
Show __repr__ for an `eval` result.
|
||||||
|
|
||||||
|
Note: this can raise `KeyboardInterrupt` if either calling `__repr__`,
|
||||||
|
`__pt_repr__` or formatting the output with "Black" takes to long
|
||||||
|
and the user presses Control-C.
|
||||||
|
"""
|
||||||
|
out_prompt = to_formatted_text(self.get_output_prompt())
|
||||||
|
|
||||||
|
# If the repr is valid Python code, use the Pygments lexer.
|
||||||
|
try:
|
||||||
|
result_repr = repr(result)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
raise # Don't catch here.
|
||||||
|
except BaseException as e:
|
||||||
|
# Calling repr failed.
|
||||||
|
self._handle_exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
compile(result_repr, "", "eval")
|
||||||
|
except SyntaxError:
|
||||||
|
formatted_result_repr = to_formatted_text(result_repr)
|
||||||
|
else:
|
||||||
|
# Syntactically correct. Format with black and syntax highlight.
|
||||||
|
if self.enable_output_formatting:
|
||||||
|
# Inline import. Slightly speed up start-up time if black is
|
||||||
|
# not used.
|
||||||
|
import black
|
||||||
|
|
||||||
|
result_repr = black.format_str(
|
||||||
|
result_repr,
|
||||||
|
mode=black.FileMode(line_length=self.app.output.get_size().columns),
|
||||||
|
)
|
||||||
|
|
||||||
|
formatted_result_repr = to_formatted_text(
|
||||||
|
PygmentsTokens(list(_lex_python_result(result_repr)))
|
||||||
|
)
|
||||||
|
|
||||||
|
# If __pt_repr__ is present, take this. This can return prompt_toolkit
|
||||||
|
# formatted text.
|
||||||
|
try:
|
||||||
|
if hasattr(result, "__pt_repr__"):
|
||||||
|
formatted_result_repr = to_formatted_text(
|
||||||
|
getattr(result, "__pt_repr__")()
|
||||||
|
)
|
||||||
|
if isinstance(formatted_result_repr, list):
|
||||||
|
formatted_result_repr = FormattedText(formatted_result_repr)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
raise # Don't catch here.
|
||||||
|
except:
|
||||||
|
# For bad code, `__getattr__` can raise something that's not an
|
||||||
|
# `AttributeError`. This happens already when calling `hasattr()`.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Align every line to the prompt.
|
||||||
|
line_sep = "\n" + " " * fragment_list_width(out_prompt)
|
||||||
|
indented_repr: StyleAndTextTuples = []
|
||||||
|
|
||||||
|
lines = list(split_lines(formatted_result_repr))
|
||||||
|
|
||||||
|
for i, fragment in enumerate(lines):
|
||||||
|
indented_repr.extend(fragment)
|
||||||
|
|
||||||
|
# Add indentation separator between lines, not after the last line.
|
||||||
|
if i != len(lines) - 1:
|
||||||
|
indented_repr.append(("", line_sep))
|
||||||
|
|
||||||
|
# Write output tokens.
|
||||||
|
if self.enable_syntax_highlighting:
|
||||||
|
formatted_output = merge_formatted_text([out_prompt, indented_repr])
|
||||||
|
else:
|
||||||
|
formatted_output = FormattedText(
|
||||||
|
out_prompt + [("", fragment_list_to_text(formatted_result_repr))]
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.enable_pager:
|
||||||
|
self.print_paginated_formatted_text(to_formatted_text(formatted_output))
|
||||||
|
else:
|
||||||
|
self.print_formatted_text(to_formatted_text(formatted_output))
|
||||||
|
|
||||||
|
self.app.output.flush()
|
||||||
|
|
||||||
|
if self.insert_blank_line_after_output:
|
||||||
|
self.app.output.write("\n")
|
||||||
|
|
||||||
|
def print_formatted_text(
|
||||||
|
self, formatted_text: StyleAndTextTuples, end: str = "\n"
|
||||||
|
) -> None:
|
||||||
|
print_formatted_text(
|
||||||
|
FormattedText(formatted_text),
|
||||||
|
style=self._current_style,
|
||||||
|
style_transformation=self.style_transformation,
|
||||||
|
include_default_pygments_style=False,
|
||||||
|
output=self.app.output,
|
||||||
|
end=end,
|
||||||
|
)
|
||||||
|
|
||||||
|
def print_paginated_formatted_text(
|
||||||
|
self,
|
||||||
|
formatted_text: StyleAndTextTuples,
|
||||||
|
end: str = "\n",
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Print formatted text, using --MORE-- style pagination.
|
||||||
|
(Avoid filling up the terminal's scrollback buffer.)
|
||||||
|
"""
|
||||||
|
pager_prompt = self.create_pager_prompt()
|
||||||
|
size = self.app.output.get_size()
|
||||||
|
|
||||||
|
abort = False
|
||||||
|
print_all = False
|
||||||
|
|
||||||
|
# Max number of lines allowed in the buffer before painting.
|
||||||
|
max_rows = size.rows - 1
|
||||||
|
|
||||||
|
# Page buffer.
|
||||||
|
rows_in_buffer = 0
|
||||||
|
columns_in_buffer = 0
|
||||||
|
page: StyleAndTextTuples = []
|
||||||
|
|
||||||
|
def flush_page() -> None:
|
||||||
|
nonlocal page, columns_in_buffer, rows_in_buffer
|
||||||
|
self.print_formatted_text(page, end="")
|
||||||
|
page = []
|
||||||
|
columns_in_buffer = 0
|
||||||
|
rows_in_buffer = 0
|
||||||
|
|
||||||
|
def show_pager() -> None:
|
||||||
|
nonlocal abort, max_rows, print_all
|
||||||
|
|
||||||
|
# Run pager prompt in another thread.
|
||||||
|
# Same as for the input. This prevents issues with nested event
|
||||||
|
# loops.
|
||||||
|
pager_result = None
|
||||||
|
|
||||||
|
def in_thread() -> None:
|
||||||
|
nonlocal pager_result
|
||||||
|
pager_result = pager_prompt.prompt()
|
||||||
|
|
||||||
|
th = threading.Thread(target=in_thread)
|
||||||
|
th.start()
|
||||||
|
th.join()
|
||||||
|
|
||||||
|
if pager_result == PagerResult.ABORT:
|
||||||
|
print("...")
|
||||||
|
abort = True
|
||||||
|
|
||||||
|
elif pager_result == PagerResult.NEXT_LINE:
|
||||||
|
max_rows = 1
|
||||||
|
|
||||||
|
elif pager_result == PagerResult.NEXT_PAGE:
|
||||||
|
max_rows = size.rows - 1
|
||||||
|
|
||||||
|
elif pager_result == PagerResult.PRINT_ALL:
|
||||||
|
print_all = True
|
||||||
|
|
||||||
|
# Loop over lines. Show --MORE-- prompt when page is filled.
|
||||||
|
|
||||||
|
formatted_text = formatted_text + [("", end)]
|
||||||
|
lines = list(split_lines(formatted_text))
|
||||||
|
|
||||||
|
for lineno, line in enumerate(lines):
|
||||||
|
for style, text, *_ in line:
|
||||||
|
for c in text:
|
||||||
|
width = get_cwidth(c)
|
||||||
|
|
||||||
|
# (Soft) wrap line if it doesn't fit.
|
||||||
|
if columns_in_buffer + width > size.columns:
|
||||||
|
# Show pager first if we get too many lines after
|
||||||
|
# wrapping.
|
||||||
|
if rows_in_buffer + 1 >= max_rows and not print_all:
|
||||||
|
page.append(("", "\n"))
|
||||||
|
flush_page()
|
||||||
|
show_pager()
|
||||||
|
if abort:
|
||||||
|
return
|
||||||
|
|
||||||
|
rows_in_buffer += 1
|
||||||
|
columns_in_buffer = 0
|
||||||
|
|
||||||
|
columns_in_buffer += width
|
||||||
|
page.append((style, c))
|
||||||
|
|
||||||
|
if rows_in_buffer + 1 >= max_rows and not print_all:
|
||||||
|
page.append(("", "\n"))
|
||||||
|
flush_page()
|
||||||
|
show_pager()
|
||||||
|
if abort:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Add line ending between lines (if `end="\n"` was given, one
|
||||||
|
# more empty line is added in `split_lines` automatically to
|
||||||
|
# take care of the final line ending).
|
||||||
|
if lineno != len(lines) - 1:
|
||||||
|
page.append(("", "\n"))
|
||||||
|
rows_in_buffer += 1
|
||||||
|
columns_in_buffer = 0
|
||||||
|
|
||||||
|
flush_page()
|
||||||
|
|
||||||
|
def create_pager_prompt(self) -> PromptSession["PagerResult"]:
|
||||||
|
"""
|
||||||
|
Create pager --MORE-- prompt.
|
||||||
|
"""
|
||||||
|
return create_pager_prompt(self._current_style, self.title)
|
||||||
|
|
||||||
|
def _handle_exception(self, e: BaseException) -> None:
|
||||||
|
output = self.app.output
|
||||||
|
|
||||||
|
# Instead of just calling ``traceback.format_exc``, we take the
|
||||||
|
# traceback and skip the bottom calls of this framework.
|
||||||
|
t, v, tb = sys.exc_info()
|
||||||
|
|
||||||
|
# Required for pdb.post_mortem() to work.
|
||||||
|
sys.last_type, sys.last_value, sys.last_traceback = t, v, tb
|
||||||
|
|
||||||
|
tblist = list(traceback.extract_tb(tb))
|
||||||
|
|
||||||
|
for line_nr, tb_tuple in enumerate(tblist):
|
||||||
|
if tb_tuple[0] == "<stdin>":
|
||||||
|
tblist = tblist[line_nr:]
|
||||||
|
break
|
||||||
|
|
||||||
|
l = traceback.format_list(tblist)
|
||||||
|
if l:
|
||||||
|
l.insert(0, "Traceback (most recent call last):\n")
|
||||||
|
l.extend(traceback.format_exception_only(t, v))
|
||||||
|
|
||||||
|
tb_str = "".join(l)
|
||||||
|
|
||||||
|
# Format exception and write to output.
|
||||||
|
# (We use the default style. Most other styles result
|
||||||
|
# in unreadable colors for the traceback.)
|
||||||
|
if self.enable_syntax_highlighting:
|
||||||
|
tokens = list(_lex_python_traceback(tb_str))
|
||||||
|
else:
|
||||||
|
tokens = [(Token, tb_str)]
|
||||||
|
|
||||||
|
print_formatted_text(
|
||||||
|
PygmentsTokens(tokens),
|
||||||
|
style=self._current_style,
|
||||||
|
style_transformation=self.style_transformation,
|
||||||
|
include_default_pygments_style=False,
|
||||||
|
output=output,
|
||||||
|
)
|
||||||
|
|
||||||
|
output.write("%s\n" % e)
|
||||||
|
output.flush()
|
||||||
|
|
||||||
|
def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None:
|
||||||
|
output = self.app.output
|
||||||
|
|
||||||
|
output.write("\rKeyboardInterrupt\n\n")
|
||||||
|
output.flush()
|
||||||
|
|
||||||
|
def _add_to_namespace(self) -> None:
|
||||||
|
"""
|
||||||
|
Add ptpython built-ins to global namespace.
|
||||||
|
"""
|
||||||
|
globals = self.get_globals()
|
||||||
|
|
||||||
|
# Add a 'get_ptpython', similar to 'get_ipython'
|
||||||
|
def get_ptpython() -> PythonInput:
|
||||||
|
return self
|
||||||
|
|
||||||
|
globals["get_ptpython"] = get_ptpython
|
||||||
|
|
||||||
|
def _remove_from_namespace(self) -> None:
|
||||||
|
"""
|
||||||
|
Remove added symbols from the globals.
|
||||||
|
"""
|
||||||
|
globals = self.get_globals()
|
||||||
|
del globals["get_ptpython"]
|
||||||
|
|
||||||
|
|
||||||
|
def _lex_python_traceback(tb):
|
||||||
|
" Return token list for traceback string. "
|
||||||
|
lexer = PythonTracebackLexer()
|
||||||
|
return lexer.get_tokens(tb)
|
||||||
|
|
||||||
|
|
||||||
|
def _lex_python_result(tb):
|
||||||
|
" Return token list for Python string. "
|
||||||
|
lexer = PythonLexer()
|
||||||
|
# Use `get_tokens_unprocessed`, so that we get exactly the same string,
|
||||||
|
# without line endings appended. `print_formatted_text` already appends a
|
||||||
|
# line ending, and otherwise we'll have two line endings.
|
||||||
|
tokens = lexer.get_tokens_unprocessed(tb)
|
||||||
|
return [(tokentype, value) for index, tokentype, value in tokens]
|
||||||
|
|
||||||
|
|
||||||
|
def enable_deprecation_warnings() -> None:
|
||||||
|
"""
|
||||||
|
Show deprecation warnings, when they are triggered directly by actions in
|
||||||
|
the REPL. This is recommended to call, before calling `embed`.
|
||||||
|
|
||||||
|
e.g. This will show an error message when the user imports the 'sha'
|
||||||
|
library on Python 2.7.
|
||||||
|
"""
|
||||||
|
warnings.filterwarnings("default", category=DeprecationWarning, module="__main__")
|
||||||
|
|
||||||
|
|
||||||
|
def run_config(repl: PythonInput, config_file: str = "~/.ptpython/config.py") -> None:
|
||||||
|
"""
|
||||||
|
Execute REPL config file.
|
||||||
|
|
||||||
|
:param repl: `PythonInput` instance.
|
||||||
|
:param config_file: Path of the configuration file.
|
||||||
|
"""
|
||||||
|
# Expand tildes.
|
||||||
|
config_file = os.path.expanduser(config_file)
|
||||||
|
|
||||||
|
def enter_to_continue() -> None:
|
||||||
|
input("\nPress ENTER to continue...")
|
||||||
|
|
||||||
|
# Check whether this file exists.
|
||||||
|
if not os.path.exists(config_file):
|
||||||
|
print("Impossible to read %r" % config_file)
|
||||||
|
enter_to_continue()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run the config file in an empty namespace.
|
||||||
|
try:
|
||||||
|
namespace: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
with open(config_file, "rb") as f:
|
||||||
|
code = compile(f.read(), config_file, "exec")
|
||||||
|
exec(code, namespace, namespace)
|
||||||
|
|
||||||
|
# Now we should have a 'configure' method in this namespace. We call this
|
||||||
|
# method with the repl as an argument.
|
||||||
|
if "configure" in namespace:
|
||||||
|
namespace["configure"](repl)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
enter_to_continue()
|
||||||
|
|
||||||
|
|
||||||
|
def embed(
|
||||||
|
globals=None,
|
||||||
|
locals=None,
|
||||||
|
configure: Optional[Callable[[PythonRepl], None]] = None,
|
||||||
|
vi_mode: bool = False,
|
||||||
|
history_filename: Optional[str] = None,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
startup_paths=None,
|
||||||
|
patch_stdout: bool = False,
|
||||||
|
return_asyncio_coroutine: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Call this to embed Python shell at the current point in your program.
|
||||||
|
It's similar to `IPython.embed` and `bpython.embed`. ::
|
||||||
|
|
||||||
|
from prompt_toolkit.contrib.repl import embed
|
||||||
|
embed(globals(), locals())
|
||||||
|
|
||||||
|
:param vi_mode: Boolean. Use Vi instead of Emacs key bindings.
|
||||||
|
:param configure: Callable that will be called with the `PythonRepl` as a first
|
||||||
|
argument, to trigger configuration.
|
||||||
|
:param title: Title to be displayed in the terminal titlebar. (None or string.)
|
||||||
|
:param patch_stdout: When true, patch `sys.stdout` so that background
|
||||||
|
threads that are printing will print nicely above the prompt.
|
||||||
|
"""
|
||||||
|
# Default globals/locals
|
||||||
|
if globals is None:
|
||||||
|
globals = {
|
||||||
|
"__name__": "__main__",
|
||||||
|
"__package__": None,
|
||||||
|
"__doc__": None,
|
||||||
|
"__builtins__": builtins,
|
||||||
|
}
|
||||||
|
|
||||||
|
locals = locals or globals
|
||||||
|
|
||||||
|
def get_globals():
|
||||||
|
return globals
|
||||||
|
|
||||||
|
def get_locals():
|
||||||
|
return locals
|
||||||
|
|
||||||
|
# Create REPL.
|
||||||
|
repl = PythonRepl(
|
||||||
|
get_globals=get_globals,
|
||||||
|
get_locals=get_locals,
|
||||||
|
vi_mode=vi_mode,
|
||||||
|
history_filename=history_filename,
|
||||||
|
startup_paths=startup_paths,
|
||||||
|
)
|
||||||
|
|
||||||
|
if title:
|
||||||
|
repl.terminal_title = title
|
||||||
|
|
||||||
|
if configure:
|
||||||
|
configure(repl)
|
||||||
|
|
||||||
|
# Start repl.
|
||||||
|
patch_context: ContextManager = (
|
||||||
|
patch_stdout_context() if patch_stdout else DummyContext()
|
||||||
|
)
|
||||||
|
|
||||||
|
if return_asyncio_coroutine:
|
||||||
|
|
||||||
|
async def coroutine():
|
||||||
|
with patch_context:
|
||||||
|
await repl.run_async()
|
||||||
|
|
||||||
|
return coroutine()
|
||||||
|
else:
|
||||||
|
with patch_context:
|
||||||
|
repl.run()
|
||||||
|
|
||||||
|
|
||||||
|
class PagerResult(Enum):
|
||||||
|
ABORT = "ABORT"
|
||||||
|
NEXT_LINE = "NEXT_LINE"
|
||||||
|
NEXT_PAGE = "NEXT_PAGE"
|
||||||
|
PRINT_ALL = "PRINT_ALL"
|
||||||
|
|
||||||
|
|
||||||
|
def create_pager_prompt(
|
||||||
|
style: BaseStyle, title: AnyFormattedText = ""
|
||||||
|
) -> PromptSession[PagerResult]:
|
||||||
|
"""
|
||||||
|
Create a "continue" prompt for paginated output.
|
||||||
|
"""
|
||||||
|
bindings = KeyBindings()
|
||||||
|
|
||||||
|
@bindings.add("enter")
|
||||||
|
@bindings.add("down")
|
||||||
|
def next_line(event: KeyPressEvent) -> None:
|
||||||
|
event.app.exit(result=PagerResult.NEXT_LINE)
|
||||||
|
|
||||||
|
@bindings.add("space")
|
||||||
|
def next_page(event: KeyPressEvent) -> None:
|
||||||
|
event.app.exit(result=PagerResult.NEXT_PAGE)
|
||||||
|
|
||||||
|
@bindings.add("a")
|
||||||
|
def print_all(event: KeyPressEvent) -> None:
|
||||||
|
event.app.exit(result=PagerResult.PRINT_ALL)
|
||||||
|
|
||||||
|
@bindings.add("q")
|
||||||
|
@bindings.add("c-c")
|
||||||
|
@bindings.add("c-d")
|
||||||
|
@bindings.add("escape", eager=True)
|
||||||
|
def no(event: KeyPressEvent) -> None:
|
||||||
|
event.app.exit(result=PagerResult.ABORT)
|
||||||
|
|
||||||
|
@bindings.add("<any>")
|
||||||
|
def _(event: KeyPressEvent) -> None:
|
||||||
|
" Disallow inserting other text. "
|
||||||
|
pass
|
||||||
|
|
||||||
|
style
|
||||||
|
|
||||||
|
session: PromptSession[PagerResult] = PromptSession(
|
||||||
|
merge_formatted_text(
|
||||||
|
[
|
||||||
|
title,
|
||||||
|
HTML(
|
||||||
|
"<status-toolbar>"
|
||||||
|
"<more> -- MORE -- </more> "
|
||||||
|
"<key>[Enter]</key> Scroll "
|
||||||
|
"<key>[Space]</key> Next page "
|
||||||
|
"<key>[a]</key> Print all "
|
||||||
|
"<key>[q]</key> Quit "
|
||||||
|
"</status-toolbar>: "
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
key_bindings=bindings,
|
||||||
|
erase_when_done=True,
|
||||||
|
style=style,
|
||||||
|
)
|
||||||
|
return session
|
266
ptpython/signatures.py
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
"""
|
||||||
|
Helpers for retrieving the function signature of the function call that we are
|
||||||
|
editing.
|
||||||
|
|
||||||
|
Either with the Jedi library, or using `inspect.signature` if Jedi fails and we
|
||||||
|
can use `eval()` to evaluate the function object.
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
from inspect import Signature as InspectSignature
|
||||||
|
from inspect import _ParameterKind as ParameterKind
|
||||||
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
|
|
||||||
|
from .completer import DictionaryCompleter
|
||||||
|
from .utils import get_jedi_script_from_document
|
||||||
|
|
||||||
|
__all__ = ["Signature", "get_signatures_using_jedi", "get_signatures_using_eval"]
|
||||||
|
|
||||||
|
|
||||||
|
class Parameter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
annotation: Optional[str],
|
||||||
|
default: Optional[str],
|
||||||
|
kind: ParameterKind,
|
||||||
|
) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.kind = kind
|
||||||
|
|
||||||
|
self.annotation = annotation
|
||||||
|
self.default = default
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Parameter(name={self.name!r})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""
|
||||||
|
Name + annotation.
|
||||||
|
"""
|
||||||
|
description = self.name
|
||||||
|
|
||||||
|
if self.annotation is not None:
|
||||||
|
description += f": {self.annotation}"
|
||||||
|
|
||||||
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
class Signature:
|
||||||
|
"""
|
||||||
|
Signature definition used wrap around both Jedi signatures and
|
||||||
|
python-inspect signatures.
|
||||||
|
|
||||||
|
:param index: Parameter index of the current cursor position.
|
||||||
|
:param bracket_start: (line, column) tuple for the open bracket that starts
|
||||||
|
the function call.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
docstring: str,
|
||||||
|
parameters: Sequence[Parameter],
|
||||||
|
index: Optional[int] = None,
|
||||||
|
returns: str = "",
|
||||||
|
bracket_start: Tuple[int, int] = (0, 0),
|
||||||
|
) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.docstring = docstring
|
||||||
|
self.parameters = parameters
|
||||||
|
self.index = index
|
||||||
|
self.returns = returns
|
||||||
|
self.bracket_start = bracket_start
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_inspect_signature(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
docstring: str,
|
||||||
|
signature: InspectSignature,
|
||||||
|
index: int,
|
||||||
|
) -> "Signature":
|
||||||
|
parameters = []
|
||||||
|
|
||||||
|
def get_annotation_name(annotation: object) -> str:
|
||||||
|
"""
|
||||||
|
Get annotation as string from inspect signature.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# In case the annotation is a class like "int", "float", ...
|
||||||
|
return str(annotation.__name__) # type: ignore
|
||||||
|
except AttributeError:
|
||||||
|
pass # No attribute `__name__`, e.g., in case of `List[int]`.
|
||||||
|
|
||||||
|
annotation = str(annotation)
|
||||||
|
if annotation.startswith("typing."):
|
||||||
|
annotation = annotation[len("typing:") :]
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
for p in signature.parameters.values():
|
||||||
|
parameters.append(
|
||||||
|
Parameter(
|
||||||
|
name=p.name,
|
||||||
|
annotation=get_annotation_name(p.annotation),
|
||||||
|
default=repr(p.default)
|
||||||
|
if p.default is not inspect.Parameter.empty
|
||||||
|
else None,
|
||||||
|
kind=p.kind,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
docstring=docstring,
|
||||||
|
parameters=parameters,
|
||||||
|
index=index,
|
||||||
|
returns="",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_jedi_signature(cls, signature) -> "Signature":
|
||||||
|
parameters = []
|
||||||
|
|
||||||
|
for p in signature.params:
|
||||||
|
if p is None:
|
||||||
|
# We just hit the "*".
|
||||||
|
continue
|
||||||
|
|
||||||
|
parameters.append(
|
||||||
|
Parameter(
|
||||||
|
name=p.to_string(), # p.name, (`to_string()` already includes the annotation).
|
||||||
|
annotation=None, # p.infer_annotation()
|
||||||
|
default=None, # p.infer_default()
|
||||||
|
kind=p.kind,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
docstring = signature.docstring()
|
||||||
|
if not isinstance(docstring, str):
|
||||||
|
docstring = docstring.decode("utf-8")
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
name=signature.name,
|
||||||
|
docstring=docstring,
|
||||||
|
parameters=parameters,
|
||||||
|
index=signature.index,
|
||||||
|
returns="",
|
||||||
|
bracket_start=signature.bracket_start,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Signature({self.name!r}, parameters={self.parameters!r})"
|
||||||
|
|
||||||
|
|
||||||
|
def get_signatures_using_jedi(
|
||||||
|
document: Document, locals: Dict[str, Any], globals: Dict[str, Any]
|
||||||
|
) -> List[Signature]:
|
||||||
|
script = get_jedi_script_from_document(document, locals, globals)
|
||||||
|
|
||||||
|
# Show signatures in help text.
|
||||||
|
if not script:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
signatures = script.get_signatures()
|
||||||
|
except ValueError:
|
||||||
|
# e.g. in case of an invalid \\x escape.
|
||||||
|
signatures = []
|
||||||
|
except Exception:
|
||||||
|
# Sometimes we still get an exception (TypeError), because
|
||||||
|
# of probably bugs in jedi. We can silence them.
|
||||||
|
# See: https://github.com/davidhalter/jedi/issues/492
|
||||||
|
signatures = []
|
||||||
|
else:
|
||||||
|
# Try to access the params attribute just once. For Jedi
|
||||||
|
# signatures containing the keyword-only argument star,
|
||||||
|
# this will crash when retrieving it the first time with
|
||||||
|
# AttributeError. Every following time it works.
|
||||||
|
# See: https://github.com/jonathanslenders/ptpython/issues/47
|
||||||
|
# https://github.com/davidhalter/jedi/issues/598
|
||||||
|
try:
|
||||||
|
if signatures:
|
||||||
|
signatures[0].params
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return [Signature.from_jedi_signature(sig) for sig in signatures]
|
||||||
|
|
||||||
|
|
||||||
|
def get_signatures_using_eval(
|
||||||
|
document: Document, locals: Dict[str, Any], globals: Dict[str, Any]
|
||||||
|
) -> List[Signature]:
|
||||||
|
"""
|
||||||
|
Look for the signature of the function before the cursor position without
|
||||||
|
use of Jedi. This uses a similar approach as the `DictionaryCompleter` of
|
||||||
|
running `eval()` over the detected function name.
|
||||||
|
"""
|
||||||
|
# Look for open parenthesis, before cursor position.
|
||||||
|
text = document.text_before_cursor
|
||||||
|
pos = document.cursor_position - 1
|
||||||
|
|
||||||
|
paren_mapping = {")": "(", "}": "{", "]": "["}
|
||||||
|
paren_stack = [
|
||||||
|
")"
|
||||||
|
] # Start stack with closing ')'. We are going to look for the matching open ')'.
|
||||||
|
comma_count = 0 # Number of comma's between start of function call and cursor pos.
|
||||||
|
found_start = False # Found something.
|
||||||
|
|
||||||
|
while pos >= 0:
|
||||||
|
char = document.text[pos]
|
||||||
|
if char in ")]}":
|
||||||
|
paren_stack.append(char)
|
||||||
|
elif char in "([{":
|
||||||
|
if not paren_stack:
|
||||||
|
# Open paren, while no closing paren was found. Mouse cursor is
|
||||||
|
# positioned in nested parentheses. Not at the "top-level" of a
|
||||||
|
# function call.
|
||||||
|
break
|
||||||
|
if paren_mapping[paren_stack[-1]] != char:
|
||||||
|
# Unmatching parentheses: syntax error?
|
||||||
|
break
|
||||||
|
|
||||||
|
paren_stack.pop()
|
||||||
|
|
||||||
|
if len(paren_stack) == 0:
|
||||||
|
found_start = True
|
||||||
|
break
|
||||||
|
|
||||||
|
elif char == "," and len(paren_stack) == 1:
|
||||||
|
comma_count += 1
|
||||||
|
|
||||||
|
pos -= 1
|
||||||
|
|
||||||
|
if not found_start:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# We found the start of the function call. Now look for the object before
|
||||||
|
# this position on which we can do an 'eval' to retrieve the function
|
||||||
|
# object.
|
||||||
|
obj = DictionaryCompleter(lambda: globals, lambda: locals).eval_expression(
|
||||||
|
Document(document.text, cursor_position=pos), locals
|
||||||
|
)
|
||||||
|
if obj is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
name = obj.__name__ # type:ignore
|
||||||
|
except Exception:
|
||||||
|
name = obj.__class__.__name__
|
||||||
|
|
||||||
|
try:
|
||||||
|
signature = inspect.signature(obj) # type: ignore
|
||||||
|
except TypeError:
|
||||||
|
return [] # Not a callable object.
|
||||||
|
except ValueError:
|
||||||
|
return [] # No signature found, like for build-ins like "print".
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = obj.__doc__ or ""
|
||||||
|
except:
|
||||||
|
doc = ""
|
||||||
|
|
||||||
|
# TODO: `index` is not yet correct when dealing with keyword-only arguments.
|
||||||
|
return [Signature.from_inspect_signature(name, doc, signature, index=comma_count)]
|
175
ptpython/style.py
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from prompt_toolkit.styles import BaseStyle, Style, merge_styles
|
||||||
|
from prompt_toolkit.styles.pygments import style_from_pygments_cls
|
||||||
|
from prompt_toolkit.utils import is_conemu_ansi, is_windows, is_windows_vt100_supported
|
||||||
|
from pygments.styles import get_all_styles, get_style_by_name
|
||||||
|
|
||||||
|
__all__ = ["get_all_code_styles", "get_all_ui_styles", "generate_style"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_code_styles() -> Dict[str, BaseStyle]:
|
||||||
|
"""
|
||||||
|
Return a mapping from style names to their classes.
|
||||||
|
"""
|
||||||
|
result: Dict[str, BaseStyle] = {
|
||||||
|
name: style_from_pygments_cls(get_style_by_name(name))
|
||||||
|
for name in get_all_styles()
|
||||||
|
}
|
||||||
|
result["win32"] = Style.from_dict(win32_code_style)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_ui_styles() -> Dict[str, BaseStyle]:
|
||||||
|
"""
|
||||||
|
Return a dict mapping {ui_style_name -> style_dict}.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"default": Style.from_dict(default_ui_style),
|
||||||
|
"blue": Style.from_dict(blue_ui_style),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle:
|
||||||
|
"""
|
||||||
|
Generate Pygments Style class from two dictionaries
|
||||||
|
containing style rules.
|
||||||
|
"""
|
||||||
|
return merge_styles([python_style, ui_style])
|
||||||
|
|
||||||
|
|
||||||
|
# Code style for Windows consoles. They support only 16 colors,
|
||||||
|
# so we choose a combination that displays nicely.
|
||||||
|
win32_code_style = {
|
||||||
|
"pygments.comment": "#00ff00",
|
||||||
|
"pygments.keyword": "#44ff44",
|
||||||
|
"pygments.number": "",
|
||||||
|
"pygments.operator": "",
|
||||||
|
"pygments.string": "#ff44ff",
|
||||||
|
"pygments.name": "",
|
||||||
|
"pygments.name.decorator": "#ff4444",
|
||||||
|
"pygments.name.class": "#ff4444",
|
||||||
|
"pygments.name.function": "#ff4444",
|
||||||
|
"pygments.name.builtin": "#ff4444",
|
||||||
|
"pygments.name.attribute": "",
|
||||||
|
"pygments.name.constant": "",
|
||||||
|
"pygments.name.entity": "",
|
||||||
|
"pygments.name.exception": "",
|
||||||
|
"pygments.name.label": "",
|
||||||
|
"pygments.name.namespace": "",
|
||||||
|
"pygments.name.tag": "",
|
||||||
|
"pygments.name.variable": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
default_ui_style = {
|
||||||
|
"control-character": "ansiblue",
|
||||||
|
# Classic prompt.
|
||||||
|
"prompt": "bold",
|
||||||
|
"prompt.dots": "noinherit",
|
||||||
|
# (IPython <5.0) Prompt: "In [1]:"
|
||||||
|
"in": "bold #008800",
|
||||||
|
"in.number": "",
|
||||||
|
# Return value.
|
||||||
|
"out": "#ff0000",
|
||||||
|
"out.number": "#ff0000",
|
||||||
|
# Completions.
|
||||||
|
"completion.builtin": "",
|
||||||
|
"completion.param": "#006666 italic",
|
||||||
|
"completion.keyword": "fg:#008800",
|
||||||
|
"completion.keyword fuzzymatch.inside": "fg:#008800",
|
||||||
|
"completion.keyword fuzzymatch.outside": "fg:#44aa44",
|
||||||
|
# Separator between windows. (Used above docstring.)
|
||||||
|
"separator": "#bbbbbb",
|
||||||
|
# System toolbar
|
||||||
|
"system-toolbar": "#22aaaa noinherit",
|
||||||
|
# "arg" toolbar.
|
||||||
|
"arg-toolbar": "#22aaaa noinherit",
|
||||||
|
"arg-toolbar.text": "noinherit",
|
||||||
|
# Signature toolbar.
|
||||||
|
"signature-toolbar": "bg:#44bbbb #000000",
|
||||||
|
"signature-toolbar current-name": "bg:#008888 #ffffff bold",
|
||||||
|
"signature-toolbar operator": "#000000 bold",
|
||||||
|
"docstring": "#888888",
|
||||||
|
# Validation toolbar.
|
||||||
|
"validation-toolbar": "bg:#440000 #aaaaaa",
|
||||||
|
# Status toolbar.
|
||||||
|
"status-toolbar": "bg:#222222 #aaaaaa",
|
||||||
|
"status-toolbar.title": "underline",
|
||||||
|
"status-toolbar.inputmode": "bg:#222222 #ffffaa",
|
||||||
|
"status-toolbar.key": "bg:#000000 #888888",
|
||||||
|
"status-toolbar key": "bg:#000000 #888888",
|
||||||
|
"status-toolbar.pastemodeon": "bg:#aa4444 #ffffff",
|
||||||
|
"status-toolbar.pythonversion": "bg:#222222 #ffffff bold",
|
||||||
|
"status-toolbar paste-mode-on": "bg:#aa4444 #ffffff",
|
||||||
|
"record": "bg:#884444 white",
|
||||||
|
"status-toolbar more": "#ffff44",
|
||||||
|
"status-toolbar.input-mode": "#ffff44",
|
||||||
|
# The options sidebar.
|
||||||
|
"sidebar": "bg:#bbbbbb #000000",
|
||||||
|
"sidebar.title": "bg:#668866 #ffffff",
|
||||||
|
"sidebar.label": "bg:#bbbbbb #222222",
|
||||||
|
"sidebar.status": "bg:#dddddd #000011",
|
||||||
|
"sidebar.label selected": "bg:#222222 #eeeeee",
|
||||||
|
"sidebar.status selected": "bg:#444444 #ffffff bold",
|
||||||
|
"sidebar.separator": "underline",
|
||||||
|
"sidebar.key": "bg:#bbddbb #000000 bold",
|
||||||
|
"sidebar.key.description": "bg:#bbbbbb #000000",
|
||||||
|
"sidebar.helptext": "bg:#fdf6e3 #000011",
|
||||||
|
# # Styling for the history layout.
|
||||||
|
# history.line: '',
|
||||||
|
# history.line.selected: 'bg:#008800 #000000',
|
||||||
|
# history.line.current: 'bg:#ffffff #000000',
|
||||||
|
# history.line.selected.current: 'bg:#88ff88 #000000',
|
||||||
|
# history.existinginput: '#888888',
|
||||||
|
# Help Window.
|
||||||
|
"window-border": "#aaaaaa",
|
||||||
|
"window-title": "bg:#bbbbbb #000000",
|
||||||
|
# Meta-enter message.
|
||||||
|
"accept-message": "bg:#ffff88 #444444",
|
||||||
|
# Exit confirmation.
|
||||||
|
"exit-confirmation": "bg:#884444 #ffffff",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Some changes to get a bit more contrast on Windows consoles.
|
||||||
|
# (They only support 16 colors.)
|
||||||
|
if is_windows() and not is_conemu_ansi() and not is_windows_vt100_supported():
|
||||||
|
default_ui_style.update(
|
||||||
|
{
|
||||||
|
"sidebar.title": "bg:#00ff00 #ffffff",
|
||||||
|
"exitconfirmation": "bg:#ff4444 #ffffff",
|
||||||
|
"toolbar.validation": "bg:#ff4444 #ffffff",
|
||||||
|
"menu.completions.completion": "bg:#ffffff #000000",
|
||||||
|
"menu.completions.completion.current": "bg:#aaaaaa #000000",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
blue_ui_style = {}
|
||||||
|
blue_ui_style.update(default_ui_style)
|
||||||
|
# blue_ui_style.update({
|
||||||
|
# # Line numbers.
|
||||||
|
# Token.LineNumber: '#aa6666',
|
||||||
|
#
|
||||||
|
# # Highlighting of search matches in document.
|
||||||
|
# Token.SearchMatch: '#ffffff bg:#4444aa',
|
||||||
|
# Token.SearchMatch.Current: '#ffffff bg:#44aa44',
|
||||||
|
#
|
||||||
|
# # Highlighting of select text in document.
|
||||||
|
# Token.SelectedText: '#ffffff bg:#6666aa',
|
||||||
|
#
|
||||||
|
# # Completer toolbar.
|
||||||
|
# Token.Toolbar.Completions: 'bg:#44bbbb #000000',
|
||||||
|
# Token.Toolbar.Completions.Arrow: 'bg:#44bbbb #000000 bold',
|
||||||
|
# Token.Toolbar.Completions.Completion: 'bg:#44bbbb #000000',
|
||||||
|
# Token.Toolbar.Completions.Completion.Current: 'bg:#008888 #ffffff',
|
||||||
|
#
|
||||||
|
# # Completer menu.
|
||||||
|
# Token.Menu.Completions.Completion: 'bg:#44bbbb #000000',
|
||||||
|
# Token.Menu.Completions.Completion.Current: 'bg:#008888 #ffffff',
|
||||||
|
# Token.Menu.Completions.Meta: 'bg:#449999 #000000',
|
||||||
|
# Token.Menu.Completions.Meta.Current: 'bg:#00aaaa #000000',
|
||||||
|
# Token.Menu.Completions.ProgressBar: 'bg:#aaaaaa',
|
||||||
|
# Token.Menu.Completions.ProgressButton: 'bg:#000000',
|
||||||
|
# })
|
198
ptpython/utils.py
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
"""
|
||||||
|
For internal use only.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from typing import Callable, Iterable, Type, TypeVar, cast
|
||||||
|
|
||||||
|
from prompt_toolkit.formatted_text import to_formatted_text
|
||||||
|
from prompt_toolkit.formatted_text.utils import fragment_list_to_text
|
||||||
|
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"has_unclosed_brackets",
|
||||||
|
"get_jedi_script_from_document",
|
||||||
|
"document_is_multiline_python",
|
||||||
|
"unindent_code",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def has_unclosed_brackets(text: str) -> bool:
|
||||||
|
"""
|
||||||
|
Starting at the end of the string. If we find an opening bracket
|
||||||
|
for which we didn't had a closing one yet, return True.
|
||||||
|
"""
|
||||||
|
stack = []
|
||||||
|
|
||||||
|
# Ignore braces inside strings
|
||||||
|
text = re.sub(r"""('[^']*'|"[^"]*")""", "", text) # XXX: handle escaped quotes.!
|
||||||
|
|
||||||
|
for c in reversed(text):
|
||||||
|
if c in "])}":
|
||||||
|
stack.append(c)
|
||||||
|
|
||||||
|
elif c in "[({":
|
||||||
|
if stack:
|
||||||
|
if (
|
||||||
|
(c == "[" and stack[-1] == "]")
|
||||||
|
or (c == "{" and stack[-1] == "}")
|
||||||
|
or (c == "(" and stack[-1] == ")")
|
||||||
|
):
|
||||||
|
stack.pop()
|
||||||
|
else:
|
||||||
|
# Opening bracket for which we didn't had a closing one.
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_jedi_script_from_document(document, locals, globals):
|
||||||
|
import jedi # We keep this import in-line, to improve start-up time.
|
||||||
|
|
||||||
|
# Importing Jedi is 'slow'.
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jedi.Interpreter(
|
||||||
|
document.text,
|
||||||
|
path="input-text",
|
||||||
|
namespaces=[locals, globals],
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
# Invalid cursor position.
|
||||||
|
# ValueError('`column` parameter is not in a valid range.')
|
||||||
|
return None
|
||||||
|
except AttributeError:
|
||||||
|
# Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65
|
||||||
|
# See also: https://github.com/davidhalter/jedi/issues/508
|
||||||
|
return None
|
||||||
|
except IndexError:
|
||||||
|
# Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514
|
||||||
|
return None
|
||||||
|
except KeyError:
|
||||||
|
# Workaroud for a crash when the input is "u'", the start of a unicode string.
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
# Workaround for: https://github.com/jonathanslenders/ptpython/issues/91
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_multiline_string_delims = re.compile("""[']{3}|["]{3}""")
|
||||||
|
|
||||||
|
|
||||||
|
def document_is_multiline_python(document):
|
||||||
|
"""
|
||||||
|
Determine whether this is a multiline Python document.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def ends_in_multiline_string() -> bool:
|
||||||
|
"""
|
||||||
|
``True`` if we're inside a multiline string at the end of the text.
|
||||||
|
"""
|
||||||
|
delims = _multiline_string_delims.findall(document.text)
|
||||||
|
opening = None
|
||||||
|
for delim in delims:
|
||||||
|
if opening is None:
|
||||||
|
opening = delim
|
||||||
|
elif delim == opening:
|
||||||
|
opening = None
|
||||||
|
return bool(opening)
|
||||||
|
|
||||||
|
if "\n" in document.text or ends_in_multiline_string():
|
||||||
|
return True
|
||||||
|
|
||||||
|
def line_ends_with_colon() -> bool:
|
||||||
|
return document.current_line.rstrip()[-1:] == ":"
|
||||||
|
|
||||||
|
# If we just typed a colon, or still have open brackets, always insert a real newline.
|
||||||
|
if (
|
||||||
|
line_ends_with_colon()
|
||||||
|
or (
|
||||||
|
document.is_cursor_at_the_end
|
||||||
|
and has_unclosed_brackets(document.text_before_cursor)
|
||||||
|
)
|
||||||
|
or document.text.startswith("@")
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If the character before the cursor is a backslash (line continuation
|
||||||
|
# char), insert a new line.
|
||||||
|
elif document.text_before_cursor[-1:] == "\\":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T", bound=Callable[[MouseEvent], None])
|
||||||
|
|
||||||
|
|
||||||
|
def if_mousedown(handler: _T) -> _T:
|
||||||
|
"""
|
||||||
|
Decorator for mouse handlers.
|
||||||
|
Only handle event when the user pressed mouse down.
|
||||||
|
|
||||||
|
(When applied to a token list. Scroll events will bubble up and are handled
|
||||||
|
by the Window.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle_if_mouse_down(mouse_event: MouseEvent):
|
||||||
|
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
||||||
|
return handler(mouse_event)
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return cast(_T, handle_if_mouse_down)
|
||||||
|
|
||||||
|
|
||||||
|
_T_type = TypeVar("_T_type", bound=Type)
|
||||||
|
|
||||||
|
|
||||||
|
def ptrepr_to_repr(cls: _T_type) -> _T_type:
|
||||||
|
"""
|
||||||
|
Generate a normal `__repr__` method for classes that have a `__pt_repr__`.
|
||||||
|
"""
|
||||||
|
if not hasattr(cls, "__pt_repr__"):
|
||||||
|
raise TypeError(
|
||||||
|
"@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self)))
|
||||||
|
|
||||||
|
cls.__repr__ = __repr__ # type:ignore
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
def unindent_code(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Remove common leading whitespace when all lines are indented.
|
||||||
|
"""
|
||||||
|
lines = text.splitlines(keepends=True)
|
||||||
|
|
||||||
|
# Look for common prefix.
|
||||||
|
common_prefix = _common_whitespace_prefix(lines)
|
||||||
|
|
||||||
|
# Remove indentation.
|
||||||
|
lines = [line[len(common_prefix) :] for line in lines]
|
||||||
|
|
||||||
|
return "".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _common_whitespace_prefix(strings: Iterable[str]) -> str:
|
||||||
|
"""
|
||||||
|
Return common prefix for a list of lines.
|
||||||
|
This will ignore lines that contain whitespace only.
|
||||||
|
"""
|
||||||
|
# Ignore empty lines and lines that have whitespace only.
|
||||||
|
strings = [s for s in strings if not s.isspace() and not len(s) == 0]
|
||||||
|
|
||||||
|
if not strings:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
else:
|
||||||
|
s1 = min(strings)
|
||||||
|
s2 = max(strings)
|
||||||
|
|
||||||
|
for i, c in enumerate(s1):
|
||||||
|
if c != s2[i] or c not in " \t":
|
||||||
|
return s1[:i]
|
||||||
|
|
||||||
|
return s1
|
57
ptpython/validator.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from prompt_toolkit.validation import ValidationError, Validator
|
||||||
|
|
||||||
|
from .utils import unindent_code
|
||||||
|
|
||||||
|
__all__ = ["PythonValidator"]
|
||||||
|
|
||||||
|
|
||||||
|
class PythonValidator(Validator):
|
||||||
|
"""
|
||||||
|
Validation of Python input.
|
||||||
|
|
||||||
|
:param get_compiler_flags: Callable that returns the currently
|
||||||
|
active compiler flags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_compiler_flags=None):
|
||||||
|
self.get_compiler_flags = get_compiler_flags
|
||||||
|
|
||||||
|
def validate(self, document):
|
||||||
|
"""
|
||||||
|
Check input for Python syntax errors.
|
||||||
|
"""
|
||||||
|
text = unindent_code(document.text)
|
||||||
|
|
||||||
|
# When the input starts with Ctrl-Z, always accept. This means EOF in a
|
||||||
|
# Python REPL.
|
||||||
|
if text.startswith("\x1a"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# When the input starts with an exclamation mark. Accept as shell
|
||||||
|
# command.
|
||||||
|
if text.lstrip().startswith("!"):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.get_compiler_flags:
|
||||||
|
flags = self.get_compiler_flags()
|
||||||
|
else:
|
||||||
|
flags = 0
|
||||||
|
|
||||||
|
compile(text, "<input>", "exec", flags=flags, dont_inherit=True)
|
||||||
|
except SyntaxError as e:
|
||||||
|
# Note, the 'or 1' for offset is required because Python 2.7
|
||||||
|
# gives `None` as offset in case of '4=4' as input. (Looks like
|
||||||
|
# fixed in Python 3.)
|
||||||
|
# TODO: This is not correct if indentation was removed.
|
||||||
|
index = document.translate_row_col_to_index(
|
||||||
|
e.lineno - 1, (e.offset or 1) - 1
|
||||||
|
)
|
||||||
|
raise ValidationError(index, f"Syntax Error: {e}")
|
||||||
|
except TypeError as e:
|
||||||
|
# e.g. "compile() expected string without null bytes"
|
||||||
|
raise ValidationError(0, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
# In Python 2, compiling "\x9" (an invalid escape sequence) raises
|
||||||
|
# ValueError instead of SyntaxError.
|
||||||
|
raise ValidationError(0, "Syntax Error: %s" % e)
|
13
pyproject.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[tool.black]
|
||||||
|
target-version = ['py36']
|
||||||
|
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
# isort configuration that is compatible with Black.
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
known_first_party = "ptpython"
|
||||||
|
known_third_party = "prompt_toolkit,pygments,asyncssh"
|
||||||
|
force_grid_wrap = 0
|
||||||
|
use_parentheses = true
|
||||||
|
line_length = 88
|
2
setup.cfg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[bdist_wheel]
|
||||||
|
universal=1
|
51
setup.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="ptpython",
|
||||||
|
author="Jonathan Slenders",
|
||||||
|
version="3.0.16",
|
||||||
|
url="https://github.com/prompt-toolkit/ptpython",
|
||||||
|
description="Python REPL build on top of prompt_toolkit",
|
||||||
|
long_description=long_description,
|
||||||
|
packages=find_packages("."),
|
||||||
|
install_requires=[
|
||||||
|
"appdirs",
|
||||||
|
"importlib_metadata;python_version<'3.8'",
|
||||||
|
"jedi>=0.16.0",
|
||||||
|
# Use prompt_toolkit 3.0.16, because of the `DeduplicateCompleter`.
|
||||||
|
"prompt_toolkit>=3.0.16,<3.1.0",
|
||||||
|
"pygments",
|
||||||
|
"black",
|
||||||
|
],
|
||||||
|
python_requires=">=3.6",
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"ptpython = ptpython.entry_points.run_ptpython:run",
|
||||||
|
"ptipython = ptpython.entry_points.run_ptipython:run",
|
||||||
|
"ptpython%s = ptpython.entry_points.run_ptpython:run" % sys.version_info[0],
|
||||||
|
"ptpython%s.%s = ptpython.entry_points.run_ptpython:run"
|
||||||
|
% sys.version_info[:2],
|
||||||
|
"ptipython%s = ptpython.entry_points.run_ptipython:run"
|
||||||
|
% sys.version_info[0],
|
||||||
|
"ptipython%s.%s = ptpython.entry_points.run_ptipython:run"
|
||||||
|
% sys.version_info[:2],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
extras_require={"ptipython": ["ipython"]}, # For ptipython, we need to have IPython
|
||||||
|
)
|
22
tests/run_tests.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import ptpython.completer
|
||||||
|
import ptpython.eventloop
|
||||||
|
import ptpython.filters
|
||||||
|
import ptpython.history_browser
|
||||||
|
import ptpython.key_bindings
|
||||||
|
import ptpython.layout
|
||||||
|
import ptpython.python_input
|
||||||
|
import ptpython.repl
|
||||||
|
import ptpython.style
|
||||||
|
import ptpython.utils
|
||||||
|
import ptpython.validator
|
||||||
|
|
||||||
|
# For now there are no tests here.
|
||||||
|
# However this is sufficient for Travis to do at least a syntax check.
|
||||||
|
# That way we are at least sure to restrict to the Python 2.6 syntax.
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|