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()
|