diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c62bdc39..d53bfcc1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,29 +10,32 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v2 - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python-version }} - - name: Install Dependencies + - name: Type Checking run: | - sudo apt remove python3-pip - python -m pip install --upgrade pip - python -m pip install . ruff mypy pytest readme_renderer - pip list - - name: Type Checker + uvx --with . mypy src/ptpython/ + uvx --with . mypy examples/ + - name: Code formatting + if: ${{ matrix.python-version == '3.13' }} run: | - mypy ptpython - ruff check . - ruff format --check . - - name: Run Tests + uvx ruff check . + uvx ruff format --check . + - name: Typos + if: ${{ matrix.python-version == '3.13' }} run: | - ./tests/run_tests.py + uvx typos . + - name: Unit test + run: | + uvx --with . pytest tests/ - name: Validate README.md + if: ${{ matrix.python-version == '3.13' }} # Ensure that the README renders correctly (required for uploading to PyPI). run: | + uv pip install readme_renderer python -m readme_renderer README.rst > /dev/null diff --git a/CHANGELOG b/CHANGELOG index bef7d07f..838303f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,36 @@ CHANGELOG ========= +3.0.32: 2025-11-20 +------------------ + +Fixes: +- Use ANSI colors (instead of RGB colors) for the default code theme. This + improves the chances of having a better contrast with the background color, + because we now use what is configured in the terminal emulator. The theme is + called 'default-ansi' and exists alongside 'default'. + + +3.0.31: 2025-08-27 +------------------ + +Fixes: +- Add `patch_stdout_raw` parameter to `embed()`. +- Fix repl.min_brightness and repl.max_brightness config in history. +- Fix positioning of exit confirmation (compatibility with latest prompt_toolkit). + + +3.0.30: 2025-04-15 +------------------ + +New features: +- Show exception cause/context when printing chained exceptions. +- Reworked project layout and use pyproject.toml instead of setup.py. + +Breaking changes: +- Drop Python 3.7 support. + + 3.0.29: 2024-07-22 ------------------ diff --git a/README.rst b/README.rst index 06c1e02b..2559a3c4 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ ptpython pip install ptpython -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/example1.png +.. image :: ./docs/images/example1.png Ptpython is an advanced Python REPL. It should work on all Python versions from 2.6 up to 3.11 and work cross platform (Linux, @@ -109,15 +109,15 @@ More screenshots The configuration menu: -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/ptpython-menu.png +.. image :: ./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 +.. image :: ./docs/images/ptpython-history-help.png Autocompletion: -.. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/file-completion.png +.. image :: ./docs/images/file-completion.png Embedding the REPL @@ -159,7 +159,7 @@ 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 +.. image :: ./docs/images/multiline.png Syntax validation @@ -169,7 +169,7 @@ 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 +.. image :: ./docs/images/validation.png Asyncio REPL and top level await @@ -208,7 +208,7 @@ 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 `_ +`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. @@ -222,7 +222,7 @@ 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 +.. image :: ./docs/images/ipython.png This is also available for embedding: @@ -253,7 +253,7 @@ 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 +.. image :: ./docs/images/windows.png Windows terminal integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index 38cc1c20..cb909731 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -25,7 +25,7 @@ async def print_counter() -> None: Coroutine that prints counters and saves it in a global variable. """ while True: - print("Counter: %i" % counter[0]) + print(f"Counter: {counter[0]}") counter[0] += 1 await asyncio.sleep(3) diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index 9bbad86f..bf79df78 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -44,8 +44,8 @@ async def main(port: int = 8222) -> None: def create_server() -> MySSHServer: return MySSHServer(lambda: environ) - print("Listening on :%i" % port) - print('To connect, do "ssh localhost -p %i"' % port) + print(f"Listening on: {port}") + print(f'To connect, do "ssh localhost -p {port}"') await asyncssh.create_server( create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"] diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index bfd3914e..fe8a9ae8 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -1,9 +1,10 @@ -""" +r""" Configuration example for ``ptpython``. Copy this file to $XDG_CONFIG_HOME/ptpython/config.py On Linux, this is: ~/.config/ptpython/config.py On macOS, this is: ~/Library/Application Support/ptpython/config.py +On Windows, this is: ~\AppData\Local\prompt_toolkit\ptpython\config.py """ from prompt_toolkit.filters import ViInsertMode diff --git a/examples/ssh-and-telnet-embed.py b/examples/ssh-and-telnet-embed.py index 62fa76d9..2b293e6f 100755 --- a/examples/ssh-and-telnet-embed.py +++ b/examples/ssh-and-telnet-embed.py @@ -6,6 +6,8 @@ https://gist.github.com/vxgmichel/7685685b3e5ead04ada4a3ba75a48eef """ +from __future__ import annotations + import asyncio import pathlib @@ -15,7 +17,7 @@ PromptToolkitSSHServer, PromptToolkitSSHSession, ) -from prompt_toolkit.contrib.telnet.server import TelnetServer +from prompt_toolkit.contrib.telnet.server import TelnetConnection, TelnetServer from ptpython.repl import embed @@ -28,7 +30,7 @@ def ensure_key(filename: str = "ssh_host_key") -> str: return str(path) -async def interact(connection: PromptToolkitSSHSession) -> None: +async def interact(connection: PromptToolkitSSHSession | TelnetConnection) -> None: global_dict = {**globals(), "print": print_formatted_text} await embed(return_asyncio_coroutine=True, globals=global_dict) diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 5a7ef2eb..00000000 --- a/mypy.ini +++ /dev/null @@ -1,6 +0,0 @@ -[mypy] -ignore_missing_imports = True -no_implicit_optional = True -platform = win32 -strict_equality = True -strict_optional = True diff --git a/pyproject.toml b/pyproject.toml index ce420372..cb6d1ed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,57 @@ +[project] +name = "ptpython" +version = "3.0.32" +description = "Python REPL build on top of prompt_toolkit" +readme = "README.rst" +authors = [{ name = "Jonathan Slenders" }] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python", +] +requires-python = ">=3.8" +dependencies = [ + "appdirs", + "jedi>=0.16.0", + # Use prompt_toolkit 3.0.43, because of `OneStyleAndTextTuple` import. + "prompt_toolkit>=3.0.43,<3.1.0", + "pygments", +] + + +[project.urls] +Homepage = "https://github.com/prompt-toolkit/ptpython" +Changelog = "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG" +"Bug Tracker" = "https://github.com/prompt-toolkit/ptpython/issues" +"Source Code" = "https://github.com/prompt-toolkit/ptpython" + + +[project.scripts] +ptpython = "ptpython.entry_points.run_ptpython:run" +ptipython = "ptpython.entry_points.run_ptipython:run" + + +[project.optional-dependencies] +ptipython = ["ipython"] # For ptipython, we need to have IPython + + +[tool.mypy] +ignore_missing_imports = true +no_implicit_optional = true +platform = "win32" +strict_equality = true +strict_optional = true + + [tool.ruff] -target-version = "py37" +target-version = "py38" lint.select = [ "E", # pycodestyle errors "W", # pycodestyle warnings @@ -22,14 +74,22 @@ lint.ignore = [ [tool.ruff.lint.per-file-ignores] "examples/*" = ["T201"] # Print allowed in examples. "examples/ptpython_config/config.py" = ["F401"] # Unused imports in config. -"ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage. -"ptpython/entry_points/run_ptpython.py" = ["T201"] # Print usage. -"ptpython/ipython.py" = ["T100"] # Import usage. -"ptpython/repl.py" = ["T201"] # Print usage. -"ptpython/printer.py" = ["T201"] # Print usage. -"tests/run_tests.py" = ["F401"] # Unused imports. +"src/ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage. +"src/ptpython/entry_points/run_ptpython.py" = ["T201"] # Print usage. +"src/ptpython/ipython.py" = ["T100"] # Import usage. +"src/ptpython/repl.py" = ["T201"] # Print usage. +"src/ptpython/printer.py" = ["T201"] # Print usage. [tool.ruff.lint.isort] known-first-party = ["ptpython"] known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] + +[tool.typos.default] +extend-ignore-re = [ + "impotr" # Intentional typo in: ./examples/ptpython_config/config.py +] + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 80dfec6a..00000000 --- a/setup.cfg +++ /dev/null @@ -1,41 +0,0 @@ -[bdist_wheel] -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 diff --git a/setup.py b/setup.py deleted file mode 100644 index aa101764..00000000 --- a/setup.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/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.29", - url="https://github.com/prompt-toolkit/ptpython", - description="Python REPL build on top of prompt_toolkit", - long_description=long_description, - package_urls={ - "Changelog": "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG", - }, - project_urls={ - "Bug Tracker": "https://github.com/prompt-toolkit/ptpython/issues", - "Source Code": "https://github.com/prompt-toolkit/ptpython", - "Changelog": "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG", - }, - packages=find_packages("."), - package_data={"ptpython": ["py.typed"]}, - install_requires=[ - "appdirs", - "importlib_metadata;python_version<'3.8'", - "jedi>=0.16.0", - # Use prompt_toolkit 3.0.43, because of `OneStyleAndTextTuple` import. - "prompt_toolkit>=3.0.43,<3.1.0", - "pygments", - ], - python_requires=">=3.7", - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "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", - f"ptpython{sys.version_info[0]} = ptpython.entry_points.run_ptpython:run", - "ptpython{}.{} = ptpython.entry_points.run_ptpython:run".format( - *sys.version_info[:2] - ), - f"ptipython{sys.version_info[0]} = ptpython.entry_points.run_ptipython:run", - "ptipython{}.{} = ptpython.entry_points.run_ptipython:run".format( - *sys.version_info[:2] - ), - ] - }, - extras_require={ - "ptipython": ["ipython"], # For ptipython, we need to have IPython - "all": ["black"], # Black not always possible on PyPy - }, -) diff --git a/ptpython/__init__.py b/src/ptpython/__init__.py similarity index 100% rename from ptpython/__init__.py rename to src/ptpython/__init__.py diff --git a/ptpython/__main__.py b/src/ptpython/__main__.py similarity index 100% rename from ptpython/__main__.py rename to src/ptpython/__main__.py diff --git a/ptpython/completer.py b/src/ptpython/completer.py similarity index 98% rename from ptpython/completer.py rename to src/ptpython/completer.py index e8bab285..40701cab 100644 --- a/ptpython/completer.py +++ b/src/ptpython/completer.py @@ -285,12 +285,17 @@ def get_completions( if jc.type == "param": suffix = "..." + style = _get_style_for_jedi_completion(jc) + display_meta = ( + jc.type if style == "" else [(f"{style}-meta", jc.type)] + ) + 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), + display_meta=display_meta, + style=style, ) diff --git a/ptpython/contrib/__init__.py b/src/ptpython/contrib/__init__.py similarity index 100% rename from ptpython/contrib/__init__.py rename to src/ptpython/contrib/__init__.py diff --git a/ptpython/contrib/asyncssh_repl.py b/src/ptpython/contrib/asyncssh_repl.py similarity index 100% rename from ptpython/contrib/asyncssh_repl.py rename to src/ptpython/contrib/asyncssh_repl.py diff --git a/ptpython/entry_points/__init__.py b/src/ptpython/entry_points/__init__.py similarity index 100% rename from ptpython/entry_points/__init__.py rename to src/ptpython/entry_points/__init__.py diff --git a/ptpython/entry_points/run_ptipython.py b/src/ptpython/entry_points/run_ptipython.py similarity index 100% rename from ptpython/entry_points/run_ptipython.py rename to src/ptpython/entry_points/run_ptipython.py diff --git a/ptpython/entry_points/run_ptpython.py b/src/ptpython/entry_points/run_ptpython.py similarity index 97% rename from ptpython/entry_points/run_ptpython.py rename to src/ptpython/entry_points/run_ptpython.py index 05df9714..d083858d 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/src/ptpython/entry_points/run_ptpython.py @@ -30,8 +30,9 @@ import os import pathlib import sys +from importlib import metadata from textwrap import dedent -from typing import IO +from typing import Protocol import appdirs from prompt_toolkit.formatted_text import HTML @@ -39,17 +40,15 @@ from ptpython.repl import PythonRepl, embed, enable_deprecation_warnings, run_config -try: - from importlib import metadata # type: ignore -except ImportError: - import importlib_metadata as metadata # type: ignore +__all__ = ["create_parser", "get_config_and_history_file", "run"] -__all__ = ["create_parser", "get_config_and_history_file", "run"] +class _SupportsWrite(Protocol): + def write(self, s: str, /) -> object: ... class _Parser(argparse.ArgumentParser): - def print_help(self, file: IO[str] | None = None) -> None: + def print_help(self, file: _SupportsWrite | None = None) -> None: super().print_help() print( dedent( diff --git a/ptpython/eventloop.py b/src/ptpython/eventloop.py similarity index 100% rename from ptpython/eventloop.py rename to src/ptpython/eventloop.py diff --git a/ptpython/filters.py b/src/ptpython/filters.py similarity index 100% rename from ptpython/filters.py rename to src/ptpython/filters.py diff --git a/ptpython/history_browser.py b/src/ptpython/history_browser.py similarity index 99% rename from ptpython/history_browser.py rename to src/ptpython/history_browser.py index ae0ac03e..101a6c5c 100644 --- a/ptpython/history_browser.py +++ b/src/ptpython/history_browser.py @@ -58,13 +58,15 @@ from .utils import if_mousedown if TYPE_CHECKING: + from typing_extensions import TypeAlias + from .python_input import PythonInput HISTORY_COUNT = 2000 __all__ = ["HistoryLayout", "PythonHistory"] -E = KeyPressEvent +E: TypeAlias = KeyPressEvent HELP_TEXT = """ This interface is meant to select multiple lines from the @@ -641,6 +643,7 @@ def accept_handler(buffer: Buffer) -> bool: layout=self.history_layout.layout, full_screen=True, style=python_input._current_style, + style_transformation=python_input.style_transformation, mouse_support=Condition(lambda: python_input.enable_mouse_support), key_bindings=create_key_bindings(self, python_input, history_mapping), ) diff --git a/ptpython/ipython.py b/src/ptpython/ipython.py similarity index 100% rename from ptpython/ipython.py rename to src/ptpython/ipython.py diff --git a/ptpython/key_bindings.py b/src/ptpython/key_bindings.py similarity index 99% rename from ptpython/key_bindings.py rename to src/ptpython/key_bindings.py index d7bb575e..48c5f5ae 100644 --- a/ptpython/key_bindings.py +++ b/src/ptpython/key_bindings.py @@ -22,6 +22,8 @@ from .utils import document_is_multiline_python if TYPE_CHECKING: + from typing_extensions import TypeAlias + from .python_input import PythonInput __all__ = [ @@ -30,7 +32,7 @@ "load_confirm_exit_bindings", ] -E = KeyPressEvent +E: TypeAlias = KeyPressEvent @Condition diff --git a/ptpython/layout.py b/src/ptpython/layout.py similarity index 98% rename from ptpython/layout.py rename to src/ptpython/layout.py index 622df594..8d030dd1 100644 --- a/ptpython/layout.py +++ b/src/ptpython/layout.py @@ -108,7 +108,7 @@ def append_category(category: OptionCategory[Any]) -> None: tokens.extend( [ ("class:sidebar", " "), - ("class:sidebar.title", " %-36s" % category.title), + ("class:sidebar.title", f" {category.title:36}"), ("class:sidebar", "\n"), ] ) @@ -130,7 +130,7 @@ def goto_next(mouse_event: MouseEvent) -> None: 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.label" + sel, f"{label:24}", select_item)) tokens.append(("class:sidebar.status" + sel, " ", select_item)) tokens.append(("class:sidebar.status" + sel, f"{status}", goto_next)) @@ -332,7 +332,7 @@ 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) + text = f"{line_number + 1} ".rjust(width) return [("class:line-number", text)] else: return to_formatted_text(get_prompt_style().in2_prompt(width)) @@ -368,8 +368,7 @@ def get_text_fragments() -> StyleAndTextTuples: append( ( TB, - "%i/%i " - % (python_buffer.working_index + 1, len(python_buffer._working_lines)), + f"{python_buffer.working_index + 1}/{len(python_buffer._working_lines)} ", ) ) @@ -492,8 +491,7 @@ def toggle_sidebar(mouse_event: MouseEvent) -> None: ("class:status-toolbar", " - "), ( "class:status-toolbar.python-version", - "%s %i.%i.%i" - % (platform.python_implementation(), version[0], version[1], version[2]), + f"{platform.python_implementation()} {version[0]}.{version[1]}.{version[2]}", ), ("class:status-toolbar", " "), ] @@ -696,7 +694,8 @@ def menu_position() -> int | None: ), Float( left=2, - bottom=1, + top=2, + height=3, content=self.exit_confirmation, ), Float( diff --git a/ptpython/lexer.py b/src/ptpython/lexer.py similarity index 100% rename from ptpython/lexer.py rename to src/ptpython/lexer.py diff --git a/ptpython/printer.py b/src/ptpython/printer.py similarity index 95% rename from ptpython/printer.py rename to src/ptpython/printer.py index 85bd9c88..a3578de7 100644 --- a/ptpython/printer.py +++ b/src/ptpython/printer.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys import traceback from dataclasses import dataclass from enum import Enum @@ -254,8 +253,7 @@ def _apply_soft_wrapping( columns_in_buffer += width current_line.append((style, c)) - if len(current_line) > 0: - yield current_line + yield current_line def _print_paginated_formatted_text( self, lines: Iterable[StyleAndTextTuples] @@ -323,14 +321,20 @@ def show_pager() -> None: def _format_exception_output( self, e: BaseException, highlight: bool ) -> Generator[OneStyleAndTextTuple, None, None]: - # 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)) + if e.__cause__: + yield from self._format_exception_output(e.__cause__, highlight=highlight) + yield ( + "", + "\nThe above exception was the direct cause of the following exception:\n\n", + ) + elif e.__context__: + yield from self._format_exception_output(e.__context__, highlight=highlight) + yield ( + "", + "\nDuring handling of the above exception, another exception occurred:\n\n", + ) + + tblist = list(traceback.extract_tb(e.__traceback__)) for line_nr, tb_tuple in enumerate(tblist): if tb_tuple[0] == "": @@ -340,7 +344,7 @@ def _format_exception_output( tb_list = traceback.format_list(tblist) if tb_list: tb_list.insert(0, "Traceback (most recent call last):\n") - tb_list.extend(traceback.format_exception_only(t, v)) + tb_list.extend(traceback.format_exception_only(type(e), e)) tb_str = "".join(tb_list) diff --git a/ptpython/prompt_style.py b/src/ptpython/prompt_style.py similarity index 100% rename from ptpython/prompt_style.py rename to src/ptpython/prompt_style.py diff --git a/ptpython/py.typed b/src/ptpython/py.typed similarity index 100% rename from ptpython/py.typed rename to src/ptpython/py.typed diff --git a/ptpython/python_input.py b/src/ptpython/python_input.py similarity index 99% rename from ptpython/python_input.py rename to src/ptpython/python_input.py index b1773643..7e218eb9 100644 --- a/ptpython/python_input.py +++ b/src/ptpython/python_input.py @@ -350,7 +350,7 @@ def __init__( #: Load styles. self.code_styles: dict[str, BaseStyle] = get_all_code_styles() self.ui_styles = get_all_ui_styles() - self._current_code_style_name: str = "default" + self._current_code_style_name: str = "default-ansi" self._current_ui_style_name: str = "default" if is_windows(): diff --git a/ptpython/repl.py b/src/ptpython/repl.py similarity index 92% rename from ptpython/repl.py rename to src/ptpython/repl.py index 6b60018e..90772689 100644 --- a/ptpython/repl.py +++ b/src/ptpython/repl.py @@ -20,7 +20,17 @@ import warnings from dis import COMPILER_FLAG_NAMES from pathlib import Path -from typing import Any, Callable, ContextManager, Iterable, NoReturn, Sequence +from typing import ( + Any, + Callable, + ContextManager, + Coroutine, + Iterable, + Literal, + NoReturn, + Sequence, + overload, +) from prompt_toolkit.formatted_text import OneStyleAndTextTuple from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context @@ -362,7 +372,7 @@ async def eval_async(self, line: str) -> object: def _store_eval_result(self, result: object) -> None: locals: dict[str, Any] = self.get_locals() - locals["_"] = locals["_%i" % self.current_statement_index] = result + locals["_"] = locals[f"_{self.current_statement_index}"] = result def get_compiler_flags(self) -> int: return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT @@ -378,6 +388,10 @@ def _compile_with_flags(self, code: str, mode: str) -> Any: ) def _handle_exception(self, e: BaseException) -> None: + # Required for pdb.post_mortem() to work. + t, v, tb = sys.exc_info() + sys.last_type, sys.last_value, sys.last_traceback = t, v, tb + self._get_output_printer().display_exception( e, highlight=self.enable_syntax_highlighting, @@ -501,6 +515,36 @@ class ReplExit(Exception): """ +@overload +def embed( + globals: dict[str, Any] | None = ..., + locals: dict[str, Any] | None = ..., + configure: Callable[[PythonRepl], None] | None = ..., + vi_mode: bool = ..., + history_filename: str | None = ..., + title: str | None = ..., + startup_paths: Sequence[str | Path] | None = ..., + patch_stdout: bool = ..., + patch_stdout_raw: bool = ..., + return_asyncio_coroutine: Literal[False] = ..., +) -> None: ... + + +@overload +def embed( + globals: dict[str, Any] | None = ..., + locals: dict[str, Any] | None = ..., + configure: Callable[[PythonRepl], None] | None = ..., + vi_mode: bool = ..., + history_filename: str | None = ..., + title: str | None = ..., + startup_paths: Sequence[str | Path] | None = ..., + patch_stdout: bool = ..., + patch_stdout_raw: bool = ..., + return_asyncio_coroutine: Literal[True] = ..., +) -> Coroutine[Any, Any, None]: ... + + def embed( globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None, @@ -510,8 +554,9 @@ def embed( title: str | None = None, startup_paths: Sequence[str | Path] | None = None, patch_stdout: bool = False, + patch_stdout_raw: bool = False, return_asyncio_coroutine: bool = False, -) -> None: +) -> None | Coroutine[Any, Any, None]: """ Call this to embed Python shell at the current point in your program. It's similar to `IPython.embed` and `bpython.embed`. :: @@ -525,6 +570,7 @@ def embed( :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. + :param patch_stdout_raw: When true, patch_stdout will not escape/remove vt100 terminal escape sequences. """ # Default globals/locals if globals is None: @@ -560,7 +606,7 @@ def get_locals() -> dict[str, Any]: # Start repl. patch_context: ContextManager[None] = ( - patch_stdout_context() if patch_stdout else DummyContext() + patch_stdout_context(raw=patch_stdout_raw) if patch_stdout else DummyContext() ) if return_asyncio_coroutine: @@ -573,3 +619,4 @@ async def coroutine() -> None: else: with patch_context: repl.run() + return None diff --git a/ptpython/signatures.py b/src/ptpython/signatures.py similarity index 100% rename from ptpython/signatures.py rename to src/ptpython/signatures.py diff --git a/ptpython/style.py b/src/ptpython/style.py similarity index 64% rename from ptpython/style.py rename to src/ptpython/style.py index c5a04e58..8242df31 100644 --- a/ptpython/style.py +++ b/src/ptpython/style.py @@ -17,6 +17,7 @@ def get_all_code_styles() -> dict[str, BaseStyle]: for name in get_all_styles() } result["win32"] = Style.from_dict(win32_code_style) + result["default-ansi"] = Style.from_dict(default_ansi_code_style) return result @@ -38,6 +39,63 @@ def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle: return merge_styles([python_style, ui_style]) +# Use ANSI colors for the default theme. +# This is `DefaultStyle` from Pygments, modified to use ANSI colors instead of +# RGB. This adapts better to light/dark mode, because the built-in themes from +# a terminal are typically designed for whatever background is used. All the +# other Pygments themes use RGB, which is fine, because the user consciously +# chooses what works for them. + +# To convert, do: +# from prompt_toolkit.output import ColorDepth +# from prompt_toolkit.output.vt100 import _EscapeCodeCache, _get_closest_ansi_color +# print(_get_closest_ansi_color( +# *_EscapeCodeCache(ColorDepth.DEPTH_8_BIT)._color_name_to_rgb('bbbbbb')) +# ) + +default_ansi_code_style = { + "pygments.whitespace": "ansigray", # "#bbbbbb", + "pygments.comment": "italic ansibrightblack", # "italic #3d7b7b", + "pygments.comment.preproc": "noitalic ansired", # "noitalic #9c6500", + "pygments.keyword": "bold ansigreen", # "bold #008000", + "pygments.keyword.pseudo": "nobold", + "pygments.keyword.type": "nobold ansired", # "nobold #b00040", + "pygments.operator": "ansibrightblack", # "#666666", + "pygments.operator.word": "bold ansimagenta", # "bold #aa22ff", + "pygments.name.builtin": "ansigreen", # "#008000", + "pygments.name.function": "ansibrightblue", # "#0000ff", + "pygments.name.class": "bold ansibrightblue", # "bold #0000ff", + "pygments.name.namespace": "bold ansibrightblack", # "bold #0000ff", + "pygments.name.exception": "bold ansired", # "bold #cb3f38", + "pygments.name.variable": "ansiblue", # "#19177c", + "pygments.name.constant": "ansired", # "#880000", + "pygments.name.label": "ansiyellow", # "#767600", + "pygments.name.entity": "bold ansibrightblack", # "bold #717171", + "pygments.name.attribute": "ansibrightblack", # "#687822", + "pygments.name.tag": "bold ansigreen", # "bold #008000", + "pygments.name.decorator": "ansimagenta", # "#aa22ff", + "pygments.literal.string": "ansired", # "#ba2121", + "pygments.literal.string.doc": "italic", + "pygments.literal.string.interpol": "bold ansibrightblack", # "bold #a45a77", + "pygments.literal.string.escape": "bold ansired", # "bold #aa5d1f", + "pygments.literal.string.regex": "ansibrightblack", # "#a45a77", + "pygments.literal.string.symbol": "ansiblue", # "#19177c", + "pygments.literal.string.other": "ansigreen", # "#008000", + "pygments.literal.number": "ansibrightblack", # "#666666", + "pygments.generic.heading": "bold ansiblue", # "bold #000080", + "pygments.generic.subheading": "bold ansimagenta", # "bold #800080", + "pygments.generic.deleted": "ansired", # "#a00000", + "pygments.generic.inserted": "ansigreen", # "#008400", + "pygments.generic.error": "ansigreen", # "#e40000", + "pygments.generic.emph": "italic", + "pygments.generic.strong": "bold", + "pygments.generic.emphstrong": "bold italic", + "pygments.generic.prompt": "bold ansiblue", # "bold #000080", + "pygments.generic.output": "ansibrightblack", # "#717171", + "pygments.generic.traceback": "ansiblue", # "#04d", + "pygments.error": "", # "border:#ff0000", +} + # Code style for Windows consoles. They support only 16 colors, # so we choose a combination that displays nicely. win32_code_style = { @@ -79,6 +137,14 @@ def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle: "completion.keyword": "fg:#008800", "completion.keyword fuzzymatch.inside": "fg:#008800", "completion.keyword fuzzymatch.outside": "fg:#44aa44", + # Styling for the meta completion menu that displays the type of each + # completion, e.g. param, builtin, keyword to the right of the item. + "completion.param-meta": "fg:ansiblue", + "completion.param-meta fuzzymatch.inside.character": "fg:ansiblue", + "completion.builtin-meta": "fg:ansigreen", + "completion.builtin-meta fuzzymatch.inside.character": "fg:ansigreen", + "completion.keyword-meta": "fg:ansired", + "completion.keyword-meta fuzzymatch.inside.character": "fg:ansired", # Separator between windows. (Used above docstring.) "separator": "#bbbbbb", # System toolbar diff --git a/ptpython/utils.py b/src/ptpython/utils.py similarity index 100% rename from ptpython/utils.py rename to src/ptpython/utils.py diff --git a/ptpython/validator.py b/src/ptpython/validator.py similarity index 100% rename from ptpython/validator.py rename to src/ptpython/validator.py diff --git a/tests/run_tests.py b/tests/run_tests.py deleted file mode 100755 index 0de37430..00000000 --- a/tests/run_tests.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -from __future__ import annotations - -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() diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100755 index 00000000..922c6a39 --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +from __future__ import annotations + +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 to do at least a syntax check. + + +def test_dummy() -> None: + assert ptpython.completer + assert ptpython.eventloop + assert ptpython.filters + assert ptpython.history_browser + assert ptpython.key_bindings + assert ptpython.layout + assert ptpython.python_input + assert ptpython.repl + assert ptpython.style + assert ptpython.utils + assert ptpython.validator