diff options
29 files changed, 548 insertions, 773 deletions
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..ef806cf --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,40 @@ +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, "3.10"] + + 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 + python -m pip install . types-dataclasses # Needed for Python 3.6 + pip list + - name: Type Checker + run: | + mypy ptpython + isort -c --profile black ptpython examples setup.py + black --check ptpython examples setup.py + if: matrix.python-version != '3.6' + - 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 @@ -1,6 +1,22 @@ CHANGELOG ========= +3.0.21: 2022-11-25 +------------------ + +New features: +- Make ptipython respect more config changes. + (See: https://github.com/prompt-toolkit/ptpython/pull/110 ) +- Improved performance of `DictionaryCompleter` for slow mappings. + +Fixes: +- Call `super()` in `PythonInputFilter`. This will prevent potentially breakage + with an upcoming prompt_toolkit change. + (See: https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1690 ) +- Improved type annotations. +- Added `py.typed` to the `package_data`. + + 3.0.20: 2021-09-14 ------------------ diff --git a/PKG-INFO b/PKG-INFO deleted file mode 100644 index 9446457..0000000 --- a/PKG-INFO +++ /dev/null @@ -1,263 +0,0 @@ -Metadata-Version: 2.1 -Name: ptpython -Version: 3.0.20 -Summary: Python REPL build on top of prompt_toolkit -Home-page: https://github.com/prompt-toolkit/ptpython -Author: Jonathan Slenders -License: UNKNOWN -Description: 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 - -Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python -Requires-Python: >=3.6 -Provides-Extra: ptipython -Provides-Extra: all @@ -50,6 +50,40 @@ Features [2] If the terminal supports it (most terminals do), this allows pasting without going into paste mode. It will keep the indentation. +Command Line Options +******************** + +The help menu shows basic command-line options. + +:: + + $ ptpython --help + usage: ptpython [-h] [--vi] [-i] [--light-bg] [--dark-bg] [--config-file CONFIG_FILE] + [--history-file HISTORY_FILE] [-V] + [args ...] + + 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) + + __pt_repr__: A nicer repr with colors ************************************* @@ -109,6 +143,8 @@ like this: else: sys.exit(embed(globals(), locals())) +Note config file support currently only works when invoking `ptpython` directly. +That it, the config file will be ignored when embedding ptpython in an application. Multiline editing ***************** @@ -159,6 +195,9 @@ 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>`_ +Note config file support currently only works when invoking `ptpython` directly. +That it, the config file will be ignored when embedding ptpython in an application. + IPython support *************** @@ -211,7 +250,7 @@ FAQ **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 +**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. diff --git a/docs/concurrency-challenges.rst b/docs/concurrency-challenges.rst new file mode 100644 index 0000000..b56d969 --- /dev/null +++ b/docs/concurrency-challenges.rst @@ -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. diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 2427572..bf9d05f 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -106,8 +106,13 @@ def configure(repl): repl.enable_input_validation = True # Use this colorscheme for the code. + # Ptpython uses Pygments for code styling, so you can choose from Pygments' + # color schemes. See: + # https://pygments.org/docs/styles/ + # https://pygments.org/demo/ repl.use_code_colorscheme("default") - # repl.use_code_colorscheme("pastie") + # A colorscheme that looks good on dark backgrounds is 'native': + # repl.use_code_colorscheme("native") # Set color depth (keep in mind that not all terminals support true color). diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..5a7ef2e --- /dev/null +++ b/mypy.ini @@ -0,0 +1,6 @@ +[mypy] +ignore_missing_imports = True +no_implicit_optional = True +platform = win32 +strict_equality = True +strict_optional = True diff --git a/ptpython.egg-info/PKG-INFO b/ptpython.egg-info/PKG-INFO deleted file mode 100644 index 9446457..0000000 --- a/ptpython.egg-info/PKG-INFO +++ /dev/null @@ -1,263 +0,0 @@ -Metadata-Version: 2.1 -Name: ptpython -Version: 3.0.20 -Summary: Python REPL build on top of prompt_toolkit -Home-page: https://github.com/prompt-toolkit/ptpython -Author: Jonathan Slenders -License: UNKNOWN -Description: 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 - -Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python -Requires-Python: >=3.6 -Provides-Extra: ptipython -Provides-Extra: all diff --git a/ptpython.egg-info/SOURCES.txt b/ptpython.egg-info/SOURCES.txt deleted file mode 100644 index 01ce1c4..0000000 --- a/ptpython.egg-info/SOURCES.txt +++ /dev/null @@ -1,54 +0,0 @@ -.gitignore -CHANGELOG -LICENSE -MANIFEST.in -README.rst -pyproject.toml -setup.cfg -setup.py -docs/images/example1.png -docs/images/file-completion.png -docs/images/ipython.png -docs/images/multiline.png -docs/images/ptpython-history-help.png -docs/images/ptpython-menu.png -docs/images/ptpython.png -docs/images/validation.png -docs/images/windows.png -examples/asyncio-python-embed.py -examples/asyncio-ssh-python-embed.py -examples/python-embed-with-custom-prompt.py -examples/python-embed.py -examples/python-input.py -examples/ssh-and-telnet-embed.py -examples/ptpython_config/config.py -examples/test-cases/ptpython-in-other-thread.py -ptpython/__init__.py -ptpython/__main__.py -ptpython/completer.py -ptpython/eventloop.py -ptpython/filters.py -ptpython/history_browser.py -ptpython/ipython.py -ptpython/key_bindings.py -ptpython/layout.py -ptpython/lexer.py -ptpython/prompt_style.py -ptpython/python_input.py -ptpython/repl.py -ptpython/signatures.py -ptpython/style.py -ptpython/utils.py -ptpython/validator.py -ptpython.egg-info/PKG-INFO -ptpython.egg-info/SOURCES.txt -ptpython.egg-info/dependency_links.txt -ptpython.egg-info/entry_points.txt -ptpython.egg-info/requires.txt -ptpython.egg-info/top_level.txt -ptpython/contrib/__init__.py -ptpython/contrib/asyncssh_repl.py -ptpython/entry_points/__init__.py -ptpython/entry_points/run_ptipython.py -ptpython/entry_points/run_ptpython.py -tests/run_tests.py
\ No newline at end of file diff --git a/ptpython.egg-info/dependency_links.txt b/ptpython.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/ptpython.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/ptpython.egg-info/entry_points.txt b/ptpython.egg-info/entry_points.txt deleted file mode 100644 index c8c4061..0000000 --- a/ptpython.egg-info/entry_points.txt +++ /dev/null @@ -1,8 +0,0 @@ -[console_scripts] -ptipython = ptpython.entry_points.run_ptipython:run -ptipython3 = ptpython.entry_points.run_ptipython:run -ptipython3.9 = ptpython.entry_points.run_ptipython:run -ptpython = ptpython.entry_points.run_ptpython:run -ptpython3 = ptpython.entry_points.run_ptpython:run -ptpython3.9 = ptpython.entry_points.run_ptpython:run - diff --git a/ptpython.egg-info/requires.txt b/ptpython.egg-info/requires.txt deleted file mode 100644 index 9cf7d55..0000000 --- a/ptpython.egg-info/requires.txt +++ /dev/null @@ -1,13 +0,0 @@ -appdirs -jedi>=0.16.0 -prompt_toolkit<3.1.0,>=3.0.18 -pygments - -[:python_version < "3.8"] -importlib_metadata - -[all] -black - -[ptipython] -ipython diff --git a/ptpython.egg-info/top_level.txt b/ptpython.egg-info/top_level.txt deleted file mode 100644 index 5af80c7..0000000 --- a/ptpython.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -ptpython diff --git a/ptpython/completer.py b/ptpython/completer.py index 51a4086..2b6795d 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -4,7 +4,7 @@ import inspect import keyword import re from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple from prompt_toolkit.completion import ( CompleteEvent, @@ -21,6 +21,7 @@ from prompt_toolkit.formatted_text import fragment_list_to_text, to_formatted_te from ptpython.utils import get_jedi_script_from_document if TYPE_CHECKING: + import jedi.api.classes from prompt_toolkit.contrib.regular_languages.compiler import _CompiledGrammar __all__ = ["PythonCompleter", "CompletePrivateAttributes", "HidePrivateCompleter"] @@ -43,8 +44,8 @@ class PythonCompleter(Completer): def __init__( self, - get_globals: Callable[[], dict], - get_locals: Callable[[], dict], + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], enable_dictionary_completion: Callable[[], bool], ) -> None: super().__init__() @@ -200,7 +201,11 @@ class JediCompleter(Completer): Autocompleter that uses the Jedi library. """ - def __init__(self, get_globals, get_locals) -> None: + def __init__( + self, + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], + ) -> None: super().__init__() self.get_globals = get_globals @@ -296,7 +301,11 @@ class DictionaryCompleter(Completer): function calls, so it only triggers attribute access. """ - def __init__(self, get_globals, get_locals): + def __init__( + self, + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], + ) -> None: super().__init__() self.get_globals = get_globals @@ -495,7 +504,7 @@ class DictionaryCompleter(Completer): else: break - for k in result: + for k, v in result.items(): if str(k).startswith(str(key_obj)): try: k_repr = self._do_repr(k) @@ -503,7 +512,7 @@ class DictionaryCompleter(Completer): k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=abbr_meta(self._do_repr(result[k])), + display_meta=abbr_meta(self._do_repr(v)), ) except KeyError: # `result[k]` lookup failed. Trying to complete @@ -574,7 +583,7 @@ class DictionaryCompleter(Completer): underscore names to the end. """ - def sort_key(name: str): + def sort_key(name: str) -> Tuple[int, str]: if name.startswith("__"): return (2, name) # Double underscore comes latest. if name.startswith("_"): @@ -639,7 +648,9 @@ except ImportError: # Python 2. _builtin_names = [] -def _get_style_for_jedi_completion(jedi_completion) -> str: +def _get_style_for_jedi_completion( + jedi_completion: "jedi.api.classes.Completion", +) -> str: """ Return completion style to use for this name. """ diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 5ebe2b9..edffa44 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -26,16 +26,16 @@ import os import pathlib import sys from textwrap import dedent -from typing import Tuple +from typing import IO, Optional, 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 +from ptpython.repl import PythonRepl, embed, enable_deprecation_warnings, run_config try: - from importlib import metadata + from importlib import metadata # type: ignore except ImportError: import importlib_metadata as metadata # type: ignore @@ -44,7 +44,7 @@ __all__ = ["create_parser", "get_config_and_history_file", "run"] class _Parser(argparse.ArgumentParser): - def print_help(self): + def print_help(self, file: Optional[IO[str]] = None) -> None: super().print_help() print( dedent( @@ -84,7 +84,7 @@ def create_parser() -> _Parser: "-V", "--version", action="version", - version=metadata.version("ptpython"), # type: ignore + version=metadata.version("ptpython"), ) parser.add_argument("args", nargs="*", help="Script and arguments") return parser @@ -190,7 +190,7 @@ def run() -> None: enable_deprecation_warnings() # Apply config file - def configure(repl) -> None: + def configure(repl: PythonRepl) -> None: if os.path.exists(config_file): run_config(repl, config_file) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index c841972..63dd740 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -10,10 +10,12 @@ will fix it for Tk.) import sys import time +from prompt_toolkit.eventloop import InputHookContext + __all__ = ["inputhook"] -def _inputhook_tk(inputhook_context): +def _inputhook_tk(inputhook_context: InputHookContext) -> None: """ Inputhook for Tk. Run the Tk eventloop until prompt-toolkit needs to process the next input. @@ -23,9 +25,9 @@ def _inputhook_tk(inputhook_context): import _tkinter # Keep this imports inline! - root = tkinter._default_root + root = tkinter._default_root # type: ignore - def wait_using_filehandler(): + def wait_using_filehandler() -> None: """ Run the TK eventloop until the file handler that we got from the inputhook becomes readable. @@ -34,7 +36,7 @@ def _inputhook_tk(inputhook_context): # to process. stop = [False] - def done(*a): + def done(*a: object) -> None: stop[0] = True root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done) @@ -46,7 +48,7 @@ def _inputhook_tk(inputhook_context): root.deletefilehandler(inputhook_context.fileno()) - def wait_using_polling(): + def wait_using_polling() -> None: """ Windows TK doesn't support 'createfilehandler'. So, run the TK eventloop and poll until input is ready. @@ -65,7 +67,7 @@ def _inputhook_tk(inputhook_context): wait_using_polling() -def inputhook(inputhook_context): +def inputhook(inputhook_context: InputHookContext) -> None: # 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) diff --git a/ptpython/filters.py b/ptpython/filters.py index 1adac13..be85edf 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -10,6 +10,7 @@ __all__ = ["HasSignature", "ShowSidebar", "ShowSignature", "ShowDocstring"] class PythonInputFilter(Filter): def __init__(self, python_input: "PythonInput") -> None: + super().__init__() self.python_input = python_input def __call__(self) -> bool: diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index b7fe086..08725ee 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -5,6 +5,7 @@ Utility to easily select lines from the history and execute them again. run as a sub application of the Repl/PythonInput. """ from functools import partial +from typing import TYPE_CHECKING, Callable, List, Optional, Set from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app @@ -12,8 +13,11 @@ 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.base import StyleAndTextTuples from prompt_toolkit.formatted_text.utils import fragment_list_to_text +from prompt_toolkit.history import History from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.layout.containers import ( ConditionalContainer, Container, @@ -24,13 +28,23 @@ from prompt_toolkit.layout.containers import ( VSplit, Window, WindowAlign, + WindowRenderInfo, +) +from prompt_toolkit.layout.controls import ( + BufferControl, + FormattedTextControl, + UIContent, ) -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.layout.processors import ( + Processor, + Transformation, + TransformationInput, +) from prompt_toolkit.lexers import PygmentsLexer +from prompt_toolkit.mouse_events import MouseEvent from prompt_toolkit.widgets import Frame from prompt_toolkit.widgets.toolbars import ArgToolbar, SearchToolbar from pygments.lexers import Python3Lexer as PythonLexer @@ -40,10 +54,15 @@ from ptpython.layout import get_inputmode_fragments from .utils import if_mousedown +if TYPE_CHECKING: + from .python_input import PythonInput + HISTORY_COUNT = 2000 __all__ = ["HistoryLayout", "PythonHistory"] +E = KeyPressEvent + HELP_TEXT = """ This interface is meant to select multiple lines from the history and execute them together. @@ -109,7 +128,7 @@ class HistoryLayout: application. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: search_toolbar = SearchToolbar() self.help_buffer_control = BufferControl( @@ -201,19 +220,19 @@ class HistoryLayout: self.layout = Layout(self.root_container, history_window) -def _get_top_toolbar_fragments(): +def _get_top_toolbar_fragments() -> StyleAndTextTuples: return [("class:status-bar.title", "History browser - Insert from history")] -def _get_bottom_toolbar_fragments(history): +def _get_bottom_toolbar_fragments(history: "PythonHistory") -> StyleAndTextTuples: python_input = history.python_input @if_mousedown - def f1(mouse_event): + def f1(mouse_event: MouseEvent) -> None: _toggle_help(history) @if_mousedown - def tab(mouse_event): + def tab(mouse_event: MouseEvent) -> None: _select_other_window(history) return ( @@ -239,14 +258,16 @@ class HistoryMargin(Margin): This displays a green bar for the selected entries. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: self.history_buffer = history.history_buffer self.history_mapping = history.history_mapping - def get_width(self, ui_content): + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: return 2 - def create_margin(self, window_render_info, width, height): + def create_margin( + self, window_render_info: WindowRenderInfo, width: int, height: int + ) -> StyleAndTextTuples: document = self.history_buffer.document lines_starting_new_entries = self.history_mapping.lines_starting_new_entries @@ -255,7 +276,7 @@ class HistoryMargin(Margin): current_lineno = document.cursor_position_row visible_line_to_input_line = window_render_info.visible_line_to_input_line - result = [] + result: StyleAndTextTuples = [] for y in range(height): line_number = visible_line_to_input_line.get(y) @@ -286,14 +307,16 @@ class ResultMargin(Margin): The margin to be shown in the result pane. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: self.history_mapping = history.history_mapping self.history_buffer = history.history_buffer - def get_width(self, ui_content): + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: return 2 - def create_margin(self, window_render_info, width, height): + def create_margin( + self, window_render_info: WindowRenderInfo, width: int, height: int + ) -> StyleAndTextTuples: document = self.history_buffer.document current_lineno = document.cursor_position_row @@ -303,7 +326,7 @@ class ResultMargin(Margin): visible_line_to_input_line = window_render_info.visible_line_to_input_line - result = [] + result: StyleAndTextTuples = [] for y in range(height): line_number = visible_line_to_input_line.get(y) @@ -324,7 +347,7 @@ class ResultMargin(Margin): return result - def invalidation_hash(self, document): + def invalidation_hash(self, document: Document) -> int: return document.cursor_position_row @@ -333,13 +356,15 @@ class GrayExistingText(Processor): Turn the existing input, before and after the inserted code gray. """ - def __init__(self, history_mapping): + def __init__(self, history_mapping: "HistoryMapping") -> None: self.history_mapping = history_mapping self._lines_before = len( history_mapping.original_document.text_before_cursor.splitlines() ) - def apply_transformation(self, transformation_input): + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: lineno = transformation_input.lineno fragments = transformation_input.fragments @@ -357,17 +382,22 @@ class HistoryMapping: Keep a list of all the lines from the history and the selected lines. """ - def __init__(self, history, python_history, original_document): + def __init__( + self, + history: "PythonHistory", + python_history: History, + original_document: Document, + ) -> None: self.history = history self.python_history = python_history self.original_document = original_document self.lines_starting_new_entries = set() - self.selected_lines = set() + self.selected_lines: Set[int] = set() # Process history. history_strings = python_history.get_strings() - history_lines = [] + history_lines: List[str] = [] for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]: self.lines_starting_new_entries.add(len(history_lines)) @@ -389,7 +419,7 @@ class HistoryMapping: else: self.result_line_offset = 0 - def get_new_document(self, cursor_pos=None): + def get_new_document(self, cursor_pos: Optional[int] = None) -> Document: """ Create a `Document` instance that contains the resulting text. """ @@ -413,13 +443,13 @@ class HistoryMapping: cursor_pos = len(text) return Document(text, cursor_pos) - def update_default_buffer(self): + def update_default_buffer(self) -> None: b = self.history.default_buffer b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True) -def _toggle_help(history): +def _toggle_help(history: "PythonHistory") -> None: "Display/hide help." help_buffer_control = history.history_layout.help_buffer_control @@ -429,7 +459,7 @@ def _toggle_help(history): history.app.layout.current_control = help_buffer_control -def _select_other_window(history): +def _select_other_window(history: "PythonHistory") -> None: "Toggle focus between left/right window." current_buffer = history.app.current_buffer layout = history.history_layout.layout @@ -441,7 +471,11 @@ def _select_other_window(history): layout.current_control = history.history_layout.history_buffer_control -def create_key_bindings(history, python_input, history_mapping): +def create_key_bindings( + history: "PythonHistory", + python_input: "PythonInput", + history_mapping: HistoryMapping, +) -> KeyBindings: """ Key bindings. """ @@ -449,7 +483,7 @@ def create_key_bindings(history, python_input, history_mapping): handle = bindings.add @handle(" ", filter=has_focus(history.history_buffer)) - def _(event): + def _(event: E) -> None: """ Space: select/deselect line from history pane. """ @@ -486,7 +520,7 @@ def create_key_bindings(history, python_input, history_mapping): @handle(" ", filter=has_focus(DEFAULT_BUFFER)) @handle("delete", filter=has_focus(DEFAULT_BUFFER)) @handle("c-h", filter=has_focus(DEFAULT_BUFFER)) - def _(event): + def _(event: E) -> None: """ Space: remove line from default pane. """ @@ -512,17 +546,17 @@ def create_key_bindings(history, python_input, history_mapping): @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): + def _(event: E) -> None: "Select other window." _select_other_window(history) @handle("f4") - def _(event): + def _(event: E) -> None: "Switch between Emacs/Vi mode." python_input.vi_mode = not python_input.vi_mode @handle("f1") - def _(event): + def _(event: E) -> None: "Display/hide help." _toggle_help(history) @@ -530,7 +564,7 @@ def create_key_bindings(history, python_input, history_mapping): @handle("c-c", filter=help_focussed) @handle("c-g", filter=help_focussed) @handle("escape", filter=help_focussed) - def _(event): + def _(event: E) -> None: "Leave help." event.app.layout.focus_previous() @@ -538,19 +572,19 @@ def create_key_bindings(history, python_input, history_mapping): @handle("f3", filter=main_buffer_focussed) @handle("c-c", filter=main_buffer_focussed) @handle("c-g", filter=main_buffer_focussed) - def _(event): + def _(event: E) -> None: "Cancel and go back." event.app.exit(result=None) @handle("enter", filter=main_buffer_focussed) - def _(event): + def _(event: E) -> None: "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): + def _(event: E) -> None: "Suspend to background." event.app.suspend_to_background() @@ -558,7 +592,9 @@ def create_key_bindings(history, python_input, history_mapping): class PythonHistory: - def __init__(self, python_input, original_document): + def __init__( + self, python_input: "PythonInput", original_document: Document + ) -> None: """ Create an `Application` for the history screen. This has to be run as a sub application of `python_input`. @@ -577,12 +613,14 @@ class PythonHistory: + document.get_start_of_line_position(), ) + def accept_handler(buffer: Buffer) -> bool: + get_app().exit(result=self.default_buffer.text) + return False + 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) - ), + accept_handler=accept_handler, read_only=True, ) @@ -597,7 +635,7 @@ class PythonHistory: self.history_layout = HistoryLayout(self) - self.app = Application( + self.app: Application[str] = Application( layout=self.history_layout.layout, full_screen=True, style=python_input._current_style, @@ -605,7 +643,7 @@ class PythonHistory: key_bindings=create_key_bindings(self, python_input, history_mapping), ) - def _default_buffer_pos_changed(self, _): + def _default_buffer_pos_changed(self, _: Buffer) -> None: """When the cursor changes in the default buffer. Synchronize with history buffer.""" # Only when this buffer has the focus. @@ -629,7 +667,7 @@ class PythonHistory: ) ) - def _history_buffer_pos_changed(self, _): + def _history_buffer_pos_changed(self, _: Buffer) -> None: """When the cursor changes in the history buffer. Synchronize.""" # Only when this buffer has the focus. if self.app.current_buffer == self.history_buffer: diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 9163334..db2a204 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,7 @@ also the power of for instance all the %-magic functions that IPython has to offer. """ +from typing import Iterable from warnings import warn from IPython import utils as ipy_utils @@ -15,6 +16,7 @@ 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 ( + CompleteEvent, Completer, Completion, PathCompleter, @@ -25,15 +27,17 @@ 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.formatted_text import AnyFormattedText, 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 .completer import PythonCompleter +from .python_input import PythonInput from .style import default_ui_style +from .validator import PythonValidator __all__ = ["embed"] @@ -46,13 +50,13 @@ class IPythonPrompt(PromptStyle): def __init__(self, prompts): self.prompts = prompts - def in_prompt(self): + def in_prompt(self) -> AnyFormattedText: return PygmentsTokens(self.prompts.in_prompt_tokens()) - def in2_prompt(self, width): + def in2_prompt(self, width: int) -> AnyFormattedText: return PygmentsTokens(self.prompts.continuation_prompt_tokens()) - def out_prompt(self): + def out_prompt(self) -> AnyFormattedText: return [] @@ -61,7 +65,7 @@ class IPythonValidator(PythonValidator): super(IPythonValidator, self).__init__(*args, **kwargs) self.isp = IPythonInputSplitter() - def validate(self, document): + def validate(self, document: Document) -> None: document = Document(text=self.isp.transform_cell(document.text)) super(IPythonValidator, self).validate(document) @@ -142,7 +146,9 @@ class MagicsCompleter(Completer): def __init__(self, magics_manager): self.magics_manager = magics_manager - def get_completions(self, document, complete_event): + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: text = document.text_before_cursor.lstrip() for m in sorted(self.magics_manager.magics["line"]): @@ -154,7 +160,9 @@ class AliasCompleter(Completer): def __init__(self, alias_manager): self.alias_manager = alias_manager - def get_completions(self, document, complete_event): + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: text = document.text_before_cursor.lstrip() # aliases = [a for a, _ in self.alias_manager.aliases] aliases = self.alias_manager.aliases @@ -240,7 +248,7 @@ class InteractiveShellEmbed(_InteractiveShellEmbed): self.python_input = python_input - def prompt_for_code(self): + def prompt_for_code(self) -> str: try: return self.python_input.app.run() except KeyboardInterrupt: @@ -269,6 +277,25 @@ def initialize_extensions(shell, extensions): shell.showtraceback() +def run_exec_lines(shell, exec_lines): + """ + Partial copy of run_exec_lines code from IPython.core.shellapp . + """ + try: + iter(exec_lines) + except TypeError: + pass + else: + try: + for line in exec_lines: + try: + shell.run_cell(line, store_history=False) + except: + shell.showtraceback() + except: + shell.showtraceback() + + def embed(**kwargs): """ Copied from `IPython/terminal/embed.py`, but using our `InteractiveShellEmbed` instead. @@ -282,6 +309,7 @@ def embed(**kwargs): kwargs["config"] = config shell = InteractiveShellEmbed.instance(**kwargs) initialize_extensions(shell, config["InteractiveShellApp"]["extensions"]) + run_exec_lines(shell, config["InteractiveShellApp"]["exec_lines"]) run_startup_scripts(shell) shell(header=header, stack_depth=2, compile_flags=compile_flags) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index ae23a3d..147a321 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -1,4 +1,7 @@ +from typing import TYPE_CHECKING + from prompt_toolkit.application 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 ( @@ -11,19 +14,25 @@ from prompt_toolkit.filters import ( ) from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.named_commands import get_by_name +from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from .utils import document_is_multiline_python +if TYPE_CHECKING: + from .python_input import PythonInput + __all__ = [ "load_python_bindings", "load_sidebar_bindings", "load_confirm_exit_bindings", ] +E = KeyPressEvent + @Condition -def tab_should_insert_whitespace(): +def tab_should_insert_whitespace() -> bool: """ When the 'tab' key is pressed with only whitespace character before the cursor, do autocompletion. Otherwise, insert indentation. @@ -38,7 +47,7 @@ def tab_should_insert_whitespace(): return bool(b.text and (not before_cursor or before_cursor.isspace())) -def load_python_bindings(python_input): +def load_python_bindings(python_input: "PythonInput") -> KeyBindings: """ Custom key bindings. """ @@ -48,14 +57,14 @@ def load_python_bindings(python_input): handle = bindings.add @handle("c-l") - def _(event): + def _(event: E) -> None: """ Clear whole screen and render again -- also when the sidebar is visible. """ event.app.renderer.clear() @handle("c-z") - def _(event): + def _(event: E) -> None: """ Suspend. """ @@ -67,7 +76,7 @@ def load_python_bindings(python_input): handle("c-w")(get_by_name("backward-kill-word")) @handle("f2") - def _(event): + def _(event: E) -> None: """ Show/hide sidebar. """ @@ -78,21 +87,21 @@ def load_python_bindings(python_input): event.app.layout.focus_last() @handle("f3") - def _(event): + def _(event: E) -> None: """ Select from the history. """ python_input.enter_history() @handle("f4") - def _(event): + def _(event: E) -> None: """ Toggle between Vi and Emacs mode. """ python_input.vi_mode = not python_input.vi_mode @handle("f6") - def _(event): + def _(event: E) -> None: """ Enable/Disable paste mode. """ @@ -101,14 +110,14 @@ def load_python_bindings(python_input): @handle( "tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace ) - def _(event): + def _(event: E) -> None: """ When tab should insert whitespace, do that instead of completion. """ event.app.current_buffer.insert_text(" ") @Condition - def is_multiline(): + def is_multiline() -> bool: return document_is_multiline_python(python_input.default_buffer.document) @handle( @@ -120,7 +129,7 @@ def load_python_bindings(python_input): & ~is_multiline, ) @handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode) - def _(event): + def _(event: E) -> None: """ Accept input (for single line input). """ @@ -143,7 +152,7 @@ def load_python_bindings(python_input): & has_focus(DEFAULT_BUFFER) & is_multiline, ) - def _(event): + def _(event: E) -> None: """ Behaviour of the Enter key. @@ -153,11 +162,11 @@ def load_python_bindings(python_input): b = event.current_buffer empty_lines_required = python_input.accept_input_on_enter or 10000 - def at_the_end(b): + def at_the_end(b: Buffer) -> bool: """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) + return text == "" or (text.isspace() and "\n" not in text) if python_input.paste_mode: # In paste mode, always insert text. @@ -187,7 +196,7 @@ def load_python_bindings(python_input): not get_app().current_buffer.text ), ) - def _(event): + def _(event: E) -> None: """ Override Control-D exit, to ask for confirmation. """ @@ -202,14 +211,14 @@ def load_python_bindings(python_input): event.app.exit(exception=EOFError) @handle("c-c", filter=has_focus(python_input.default_buffer)) - def _(event): + def _(event: E) -> None: "Abort when Control-C has been pressed." event.app.exit(exception=KeyboardInterrupt, style="class:aborting") return bindings -def load_sidebar_bindings(python_input): +def load_sidebar_bindings(python_input: "PythonInput") -> KeyBindings: """ Load bindings for the navigation in the sidebar. """ @@ -221,7 +230,7 @@ def load_sidebar_bindings(python_input): @handle("up", filter=sidebar_visible) @handle("c-p", filter=sidebar_visible) @handle("k", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Go to previous option." python_input.selected_option_index = ( python_input.selected_option_index - 1 @@ -230,7 +239,7 @@ def load_sidebar_bindings(python_input): @handle("down", filter=sidebar_visible) @handle("c-n", filter=sidebar_visible) @handle("j", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Go to next option." python_input.selected_option_index = ( python_input.selected_option_index + 1 @@ -239,14 +248,14 @@ def load_sidebar_bindings(python_input): @handle("right", filter=sidebar_visible) @handle("l", filter=sidebar_visible) @handle(" ", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "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): + def _(event: E) -> None: "Select previous value for current option." option = python_input.selected_option option.activate_previous() @@ -256,7 +265,7 @@ def load_sidebar_bindings(python_input): @handle("c-d", filter=sidebar_visible) @handle("enter", filter=sidebar_visible) @handle("escape", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Hide sidebar." python_input.show_sidebar = False event.app.layout.focus_last() @@ -264,7 +273,7 @@ def load_sidebar_bindings(python_input): return bindings -def load_confirm_exit_bindings(python_input): +def load_confirm_exit_bindings(python_input: "PythonInput") -> KeyBindings: """ Handle yes/no key presses when the exit confirmation is shown. """ @@ -277,14 +286,14 @@ def load_confirm_exit_bindings(python_input): @handle("Y", filter=confirmation_visible) @handle("enter", filter=confirmation_visible) @handle("c-d", filter=confirmation_visible) - def _(event): + def _(event: E) -> None: """ Really quit. """ event.app.exit(exception=EOFError, style="class:exiting") @handle(Keys.Any, filter=confirmation_visible) - def _(event): + def _(event: E) -> None: """ Cancel exit. """ @@ -294,7 +303,7 @@ def load_confirm_exit_bindings(python_input): return bindings -def auto_newline(buffer): +def auto_newline(buffer: Buffer) -> None: r""" Insert \n at the cursor position. Also add necessary padding. """ diff --git a/ptpython/layout.py b/ptpython/layout.py index dc6b19b..365f381 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -5,7 +5,7 @@ import platform import sys from enum import Enum from inspect import _ParameterKind as ParameterKind -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, List, Optional, Type from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER @@ -15,10 +15,15 @@ from prompt_toolkit.filters import ( is_done, renderer_height_is_known, ) -from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text +from prompt_toolkit.formatted_text import ( + AnyFormattedText, + 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 ( + AnyContainer, ConditionalContainer, Container, Float, @@ -40,9 +45,10 @@ from prompt_toolkit.layout.processors import ( HighlightIncrementalSearchProcessor, HighlightMatchingBracketProcessor, HighlightSelectionProcessor, + Processor, TabsProcessor, ) -from prompt_toolkit.lexers import SimpleLexer +from prompt_toolkit.lexers import Lexer, SimpleLexer from prompt_toolkit.mouse_events import MouseEvent from prompt_toolkit.selection import SelectionType from prompt_toolkit.widgets.toolbars import ( @@ -55,6 +61,7 @@ from prompt_toolkit.widgets.toolbars import ( from pygments.lexers import PythonLexer from .filters import HasSignature, ShowDocstring, ShowSidebar, ShowSignature +from .prompt_style import PromptStyle from .utils import if_mousedown if TYPE_CHECKING: @@ -98,7 +105,7 @@ def python_sidebar(python_input: "PythonInput") -> Window: def get_text_fragments() -> StyleAndTextTuples: tokens: StyleAndTextTuples = [] - def append_category(category: "OptionCategory") -> None: + def append_category(category: "OptionCategory[Any]") -> None: tokens.extend( [ ("class:sidebar", " "), @@ -150,10 +157,10 @@ def python_sidebar(python_input: "PythonInput") -> Window: return tokens class Control(FormattedTextControl): - def move_cursor_down(self): + def move_cursor_down(self) -> None: python_input.selected_option_index += 1 - def move_cursor_up(self): + def move_cursor_up(self) -> None: python_input.selected_option_index -= 1 return Window( @@ -165,12 +172,12 @@ def python_sidebar(python_input: "PythonInput") -> Window: ) -def python_sidebar_navigation(python_input): +def python_sidebar_navigation(python_input: "PythonInput") -> Window: """ Create the `Layout` showing the navigation information for the sidebar. """ - def get_text_fragments(): + def get_text_fragments() -> StyleAndTextTuples: # Show navigation info. return [ ("class:sidebar", " "), @@ -191,13 +198,13 @@ def python_sidebar_navigation(python_input): ) -def python_sidebar_help(python_input): +def python_sidebar_help(python_input: "PythonInput") -> Container: """ Create the `Layout` for the help text for the current item in the sidebar. """ token = "class:sidebar.helptext" - def get_current_description(): + def get_current_description() -> str: """ Return the description of the selected option. """ @@ -209,7 +216,7 @@ def python_sidebar_help(python_input): i += 1 return "" - def get_help_text(): + def get_help_text() -> StyleAndTextTuples: return [(token, get_current_description())] return ConditionalContainer( @@ -225,7 +232,7 @@ def python_sidebar_help(python_input): ) -def signature_toolbar(python_input): +def signature_toolbar(python_input: "PythonInput") -> Container: """ Return the `Layout` for the signature. """ @@ -311,21 +318,23 @@ class PythonPromptMargin(PromptMargin): It shows something like "In [1]:". """ - def __init__(self, python_input) -> None: + def __init__(self, python_input: "PythonInput") -> None: self.python_input = python_input - def get_prompt_style(): + def get_prompt_style() -> PromptStyle: 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): + def get_continuation( + width: int, line_number: int, is_soft_wrap: bool + ) -> StyleAndTextTuples: 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) + return to_formatted_text(get_prompt_style().in2_prompt(width)) super().__init__(get_prompt, get_continuation) @@ -510,7 +519,7 @@ def show_sidebar_button_info(python_input: "PythonInput") -> Container: def create_exit_confirmation( - python_input: "PythonInput", style="class:exit-confirmation" + python_input: "PythonInput", style: str = "class:exit-confirmation" ) -> Container: """ Create `Layout` for the exit message. @@ -567,22 +576,22 @@ class PtPythonLayout: def __init__( self, python_input: "PythonInput", - lexer=PythonLexer, - extra_body=None, - extra_toolbars=None, - extra_buffer_processors=None, + lexer: Lexer, + extra_body: Optional[AnyContainer] = None, + extra_toolbars: Optional[List[AnyContainer]] = None, + extra_buffer_processors: Optional[List[Processor]] = None, input_buffer_height: Optional[AnyDimension] = None, ) -> None: D = Dimension - extra_body = [extra_body] if extra_body else [] + extra_body_list: List[AnyContainer] = [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(): + def create_python_input_window() -> Window: + def menu_position() -> Optional[int]: """ When there is no autocompletion menu to be shown, and we have a signature, set the pop-up position at `bracket_start`. @@ -593,6 +602,7 @@ class PtPythonLayout: row, col = python_input.signatures[0].bracket_start index = b.document.translate_row_col_to_index(row - 1, col) return index + return None return Window( BufferControl( @@ -622,7 +632,7 @@ class PtPythonLayout: processor=AppendAutoSuggestion(), filter=~is_done ), ] - + extra_buffer_processors, + + (extra_buffer_processors or []), menu_position=menu_position, # Make sure that we always see the result of an reverse-i-search: preview_search=True, @@ -654,7 +664,7 @@ class PtPythonLayout: [ FloatContainer( content=HSplit( - [create_python_input_window()] + extra_body + [create_python_input_window()] + extra_body_list ), floats=[ Float( diff --git a/ptpython/py.typed b/ptpython/py.typed new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ptpython/py.typed diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1785f52..c561117 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,7 +6,18 @@ import __future__ from asyncio import get_event_loop from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generic, + List, + Mapping, + Optional, + Tuple, + TypeVar, +) from prompt_toolkit.application import Application, get_app from prompt_toolkit.auto_suggest import ( @@ -44,6 +55,7 @@ from prompt_toolkit.key_binding.bindings.open_in_editor import ( load_open_in_editor_bindings, ) from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.layout.containers import AnyContainer from prompt_toolkit.lexers import DynamicLexer, Lexer, SimpleLexer from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.styles import ( @@ -88,8 +100,8 @@ if TYPE_CHECKING: _T = TypeVar("_T", bound="_SupportsLessThan") -class OptionCategory: - def __init__(self, title: str, options: List["Option"]) -> None: +class OptionCategory(Generic[_T]): + def __init__(self, title: str, options: List["Option[_T]"]) -> None: self.title = title self.options = options @@ -113,7 +125,7 @@ class Option(Generic[_T]): get_current_value: Callable[[], _T], # We accept `object` as return type for the select functions, because # often they return an unused boolean. Maybe this can be improved. - get_values: Callable[[], Dict[_T, Callable[[], object]]], + get_values: Callable[[], Mapping[_T, Callable[[], object]]], ) -> None: self.title = title self.description = description @@ -121,7 +133,7 @@ class Option(Generic[_T]): self.get_values = get_values @property - def values(self) -> Dict[_T, Callable[[], object]]: + def values(self) -> Mapping[_T, Callable[[], object]]: return self.get_values() def activate_next(self, _previous: bool = False) -> None: @@ -192,12 +204,12 @@ class PythonInput: output: Optional[Output] = None, # For internal use. extra_key_bindings: Optional[KeyBindings] = None, - create_app=True, + create_app: bool = True, _completer: Optional[Completer] = None, _validator: Optional[Validator] = None, _lexer: Optional[Lexer] = None, _extra_buffer_processors=None, - _extra_layout_body=None, + _extra_layout_body: Optional[AnyContainer] = None, _extra_toolbars=None, _input_buffer_height=None, ) -> None: @@ -239,7 +251,7 @@ class PythonInput: self.history = InMemoryHistory() self._input_buffer_height = _input_buffer_height - self._extra_layout_body = _extra_layout_body or [] + self._extra_layout_body = _extra_layout_body self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] @@ -388,7 +400,9 @@ class PythonInput: # Create an app if requested. If not, the global get_app() is returned # for self.app via property getter. if create_app: - self._app: Optional[Application] = self._create_application(input, output) + self._app: Optional[Application[str]] = self._create_application( + input, output + ) # Setting vi_mode will not work unless the prompt_toolkit # application has been created. if vi_mode: @@ -408,7 +422,7 @@ class PythonInput: return sum(len(category.options) for category in self.options) @property - def selected_option(self) -> Option: + def selected_option(self) -> Option[Any]: "Return the currently selected option." i = 0 for category in self.options: @@ -514,7 +528,7 @@ class PythonInput: self.ui_styles[self._current_ui_style_name], ) - def _create_options(self) -> List[OptionCategory]: + def _create_options(self) -> List[OptionCategory[Any]]: """ Create a list of `Option` instances for the options sidebar. """ @@ -530,15 +544,17 @@ class PythonInput: return True def simple_option( - title: str, description: str, field_name: str, values: Optional[List] = None - ) -> Option: + title: str, + description: str, + field_name: str, + values: Tuple[str, str] = ("off", "on"), + ) -> Option[str]: "Create Simple on/of option." - values = values or ["off", "on"] - def get_current_value(): + def get_current_value() -> str: return values[bool(getattr(self, field_name))] - def get_values(): + def get_values() -> Dict[str, Callable[[], bool]]: return { values[1]: lambda: enable(field_name), values[0]: lambda: disable(field_name), @@ -848,7 +864,7 @@ class PythonInput: def _create_application( self, input: Optional[Input], output: Optional[Output] - ) -> Application: + ) -> Application[str]: """ Create an `Application` instance. """ @@ -926,7 +942,7 @@ class PythonInput: self.editing_mode = EditingMode.EMACS @property - def app(self) -> Application: + def app(self) -> Application[str]: if self._app is None: return get_app() return self._app diff --git a/ptpython/repl.py b/ptpython/repl.py index b55b5d5..3c729c0 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -44,6 +44,7 @@ from pygments.token import Token from .python_input import PythonInput +PyCF_ALLOW_TOP_LEVEL_AWAIT: int try: from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT # type: ignore except ImportError: @@ -90,7 +91,7 @@ class PythonRepl(PythonInput): output = self.app.output output.write("WARNING | File not found: {}\n\n".format(path)) - def run_and_show_expression(self, expression): + def run_and_show_expression(self, expression: str) -> None: try: # Eval. try: @@ -135,7 +136,7 @@ class PythonRepl(PythonInput): text = self.read() except EOFError: return - except BaseException as e: + except BaseException: # Something went wrong while reading input. # (E.g., a bug in the completer that propagates. Don't # crash the REPL.) @@ -149,7 +150,7 @@ class PythonRepl(PythonInput): clear_title() self._remove_from_namespace() - async def run_and_show_expression_async(self, text): + async def run_and_show_expression_async(self, text: str): loop = asyncio.get_event_loop() try: @@ -349,7 +350,7 @@ class PythonRepl(PythonInput): if not hasattr(black, "Mode"): raise ImportError except ImportError: - pass # no Black package in your installation + pass # no Black package in your installation else: result_repr = black.format_str( result_repr, @@ -725,17 +726,17 @@ def embed( configure(repl) # Start repl. - patch_context: ContextManager = ( + patch_context: ContextManager[None] = ( patch_stdout_context() if patch_stdout else DummyContext() ) if return_asyncio_coroutine: - async def coroutine(): + async def coroutine() -> None: with patch_context: await repl.run_async() - return coroutine() + return coroutine() # type: ignore else: with patch_context: repl.run() diff --git a/ptpython/signatures.py b/ptpython/signatures.py index 228b99b..e836d33 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -8,13 +8,16 @@ 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 typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple from prompt_toolkit.document import Document from .completer import DictionaryCompleter from .utils import get_jedi_script_from_document +if TYPE_CHECKING: + import jedi.api.classes + __all__ = ["Signature", "get_signatures_using_jedi", "get_signatures_using_eval"] @@ -120,7 +123,9 @@ class Signature: ) @classmethod - def from_jedi_signature(cls, signature) -> "Signature": + def from_jedi_signature( + cls, signature: "jedi.api.classes.Signature" + ) -> "Signature": parameters = [] for p in signature.params: diff --git a/ptpython/utils.py b/ptpython/utils.py index 2fb24a4..ef96ca4 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -2,12 +2,31 @@ For internal use only. """ import re -from typing import Callable, Iterable, Type, TypeVar, cast - +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + Optional, + Type, + TypeVar, + cast, +) + +from prompt_toolkit.document import Document 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 +if TYPE_CHECKING: + from jedi import Interpreter + + # See: prompt_toolkit/key_binding/key_bindings.py + # Annotating these return types as `object` is what works best, because + # `NotImplemented` is typed `Any`. + NotImplementedOrNone = object + __all__ = [ "has_unclosed_brackets", "get_jedi_script_from_document", @@ -45,7 +64,9 @@ def has_unclosed_brackets(text: str) -> bool: return False -def get_jedi_script_from_document(document, locals, globals): +def get_jedi_script_from_document( + document: Document, locals: Dict[str, Any], globals: Dict[str, Any] +) -> "Interpreter": import jedi # We keep this import in-line, to improve start-up time. # Importing Jedi is 'slow'. @@ -78,7 +99,7 @@ def get_jedi_script_from_document(document, locals, globals): _multiline_string_delims = re.compile("""[']{3}|["]{3}""") -def document_is_multiline_python(document): +def document_is_multiline_python(document: Document) -> bool: """ Determine whether this is a multiline Python document. """ @@ -133,7 +154,7 @@ def if_mousedown(handler: _T) -> _T: by the Window.) """ - def handle_if_mouse_down(mouse_event: MouseEvent): + def handle_if_mouse_down(mouse_event: MouseEvent) -> "NotImplementedOrNone": if mouse_event.event_type == MouseEventType.MOUSE_DOWN: return handler(mouse_event) else: @@ -142,7 +163,7 @@ def if_mousedown(handler: _T) -> _T: return cast(_T, handle_if_mouse_down) -_T_type = TypeVar("_T_type", bound=Type) +_T_type = TypeVar("_T_type", bound=type) def ptrepr_to_repr(cls: _T_type) -> _T_type: @@ -154,7 +175,8 @@ def ptrepr_to_repr(cls: _T_type) -> _T_type: "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method." ) - def __repr__(self) -> str: + def __repr__(self: object) -> str: + assert hasattr(cls, "__pt_repr__") return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self))) cls.__repr__ = __repr__ # type:ignore diff --git a/ptpython/validator.py b/ptpython/validator.py index 0f6a4ea..ffac583 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,3 +1,6 @@ +from typing import Callable, Optional + +from prompt_toolkit.document import Document from prompt_toolkit.validation import ValidationError, Validator from .utils import unindent_code @@ -13,10 +16,10 @@ class PythonValidator(Validator): active compiler flags. """ - def __init__(self, get_compiler_flags=None): + def __init__(self, get_compiler_flags: Optional[Callable[[], int]] = None) -> None: self.get_compiler_flags = get_compiler_flags - def validate(self, document): + def validate(self, document: Document) -> None: """ Check input for Python syntax errors. """ @@ -45,7 +48,7 @@ class PythonValidator(Validator): # 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 + (e.lineno or 1) - 1, (e.offset or 1) - 1 ) raise ValidationError(index, f"Syntax Error: {e}") except TypeError as e: @@ -1,7 +1,41 @@ [bdist_wheel] -universal = 1 - -[egg_info] -tag_build = -tag_date = 0 +universal=1 +[flake8] +exclude=__init__.py +max_line_length=150 +ignore= + E114, + E116, + E117, + E121, + E122, + E123, + E125, + E126, + E127, + E128, + E131, + E171, + E203, + E211, + E221, + E227, + E231, + E241, + E251, + E301, + E402, + E501, + E701, + E702, + E704, + E731, + E741, + F401, + F403, + F405, + F811, + W503, + W504, + E722 @@ -11,11 +11,12 @@ with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f: setup( name="ptpython", author="Jonathan Slenders", - version="3.0.20", + version="3.0.21", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, packages=find_packages("."), + package_data={"ptpython": ["py.typed"]}, install_requires=[ "appdirs", "importlib_metadata;python_version<'3.8'", |