1
0
Fork 0

Merging upstream version 3.0.30.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-04-16 08:48:46 +02:00
parent dd7a66a4f1
commit 3707c10a3c
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
35 changed files with 212 additions and 195 deletions

View file

@ -10,29 +10,32 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }} - uses: astral-sh/setup-uv@v5
uses: actions/setup-python@v1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install Dependencies - name: Type Checking
run: | run: |
sudo apt remove python3-pip uvx --with . mypy src/ptpython/
python -m pip install --upgrade pip uvx --with . mypy examples/
python -m pip install . ruff mypy pytest readme_renderer - name: Code formatting
pip list if: ${{ matrix.python-version == '3.13' }}
- name: Type Checker
run: | run: |
mypy ptpython uvx ruff check .
ruff check . uvx ruff format --check .
ruff format --check . - name: Typos
- name: Run Tests if: ${{ matrix.python-version == '3.13' }}
run: | run: |
./tests/run_tests.py uvx typos .
- name: Unit test
run: |
uvx --with . pytest tests/
- name: Validate README.md - name: Validate README.md
if: ${{ matrix.python-version == '3.13' }}
# Ensure that the README renders correctly (required for uploading to PyPI). # Ensure that the README renders correctly (required for uploading to PyPI).
run: | run: |
uv pip install readme_renderer
python -m readme_renderer README.rst > /dev/null python -m readme_renderer README.rst > /dev/null

View file

@ -1,6 +1,17 @@
CHANGELOG CHANGELOG
========= =========
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 3.0.29: 2024-07-22
------------------ ------------------

View file

@ -25,7 +25,7 @@ async def print_counter() -> None:
Coroutine that prints counters and saves it in a global variable. Coroutine that prints counters and saves it in a global variable.
""" """
while True: while True:
print("Counter: %i" % counter[0]) print(f"Counter: {counter[0]}")
counter[0] += 1 counter[0] += 1
await asyncio.sleep(3) await asyncio.sleep(3)

View file

@ -44,8 +44,8 @@ async def main(port: int = 8222) -> None:
def create_server() -> MySSHServer: def create_server() -> MySSHServer:
return MySSHServer(lambda: environ) return MySSHServer(lambda: environ)
print("Listening on :%i" % port) print(f"Listening on: {port}")
print('To connect, do "ssh localhost -p %i"' % port) print(f'To connect, do "ssh localhost -p {port}"')
await asyncssh.create_server( await asyncssh.create_server(
create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"] create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"]

View file

@ -6,6 +6,8 @@ Thanks to Vincent Michel for this!
https://gist.github.com/vxgmichel/7685685b3e5ead04ada4a3ba75a48eef https://gist.github.com/vxgmichel/7685685b3e5ead04ada4a3ba75a48eef
""" """
from __future__ import annotations
import asyncio import asyncio
import pathlib import pathlib
@ -15,7 +17,7 @@ from prompt_toolkit.contrib.ssh.server import (
PromptToolkitSSHServer, PromptToolkitSSHServer,
PromptToolkitSSHSession, PromptToolkitSSHSession,
) )
from prompt_toolkit.contrib.telnet.server import TelnetServer from prompt_toolkit.contrib.telnet.server import TelnetConnection, TelnetServer
from ptpython.repl import embed from ptpython.repl import embed
@ -28,7 +30,7 @@ def ensure_key(filename: str = "ssh_host_key") -> str:
return str(path) return str(path)
async def interact(connection: PromptToolkitSSHSession) -> None: async def interact(connection: PromptToolkitSSHSession | TelnetConnection) -> None:
global_dict = {**globals(), "print": print_formatted_text} global_dict = {**globals(), "print": print_formatted_text}
await embed(return_asyncio_coroutine=True, globals=global_dict) await embed(return_asyncio_coroutine=True, globals=global_dict)

View file

@ -1,6 +0,0 @@
[mypy]
ignore_missing_imports = True
no_implicit_optional = True
platform = win32
strict_equality = True
strict_optional = True

View file

@ -1,3 +1,55 @@
[project]
name = "ptpython"
version = "3.0.30"
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] [tool.ruff]
target-version = "py37" target-version = "py37"
lint.select = [ lint.select = [
@ -22,14 +74,22 @@ lint.ignore = [
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"examples/*" = ["T201"] # Print allowed in examples. "examples/*" = ["T201"] # Print allowed in examples.
"examples/ptpython_config/config.py" = ["F401"] # Unused imports in config. "examples/ptpython_config/config.py" = ["F401"] # Unused imports in config.
"ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage. "src/ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage.
"ptpython/entry_points/run_ptpython.py" = ["T201"] # Print usage. "src/ptpython/entry_points/run_ptpython.py" = ["T201"] # Print usage.
"ptpython/ipython.py" = ["T100"] # Import usage. "src/ptpython/ipython.py" = ["T100"] # Import usage.
"ptpython/repl.py" = ["T201"] # Print usage. "src/ptpython/repl.py" = ["T201"] # Print usage.
"ptpython/printer.py" = ["T201"] # Print usage. "src/ptpython/printer.py" = ["T201"] # Print usage.
"tests/run_tests.py" = ["F401"] # Unused imports.
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["ptpython"] known-first-party = ["ptpython"]
known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] 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"

View file

@ -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

View file

@ -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
},
)

View file

@ -30,8 +30,9 @@ import asyncio
import os import os
import pathlib import pathlib
import sys import sys
from importlib import metadata
from textwrap import dedent from textwrap import dedent
from typing import IO from typing import Protocol
import appdirs import appdirs
from prompt_toolkit.formatted_text import HTML from prompt_toolkit.formatted_text import HTML
@ -39,17 +40,15 @@ from prompt_toolkit.shortcuts import print_formatted_text
from ptpython.repl import PythonRepl, embed, enable_deprecation_warnings, run_config 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): 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() super().print_help()
print( print(
dedent( dedent(

View file

@ -58,13 +58,15 @@ from ptpython.layout import get_inputmode_fragments
from .utils import if_mousedown from .utils import if_mousedown
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import TypeAlias
from .python_input import PythonInput from .python_input import PythonInput
HISTORY_COUNT = 2000 HISTORY_COUNT = 2000
__all__ = ["HistoryLayout", "PythonHistory"] __all__ = ["HistoryLayout", "PythonHistory"]
E = KeyPressEvent E: TypeAlias = KeyPressEvent
HELP_TEXT = """ HELP_TEXT = """
This interface is meant to select multiple lines from the This interface is meant to select multiple lines from the

View file

@ -22,6 +22,8 @@ from prompt_toolkit.keys import Keys
from .utils import document_is_multiline_python from .utils import document_is_multiline_python
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import TypeAlias
from .python_input import PythonInput from .python_input import PythonInput
__all__ = [ __all__ = [
@ -30,7 +32,7 @@ __all__ = [
"load_confirm_exit_bindings", "load_confirm_exit_bindings",
] ]
E = KeyPressEvent E: TypeAlias = KeyPressEvent
@Condition @Condition

View file

@ -108,7 +108,7 @@ def python_sidebar(python_input: PythonInput) -> Window:
tokens.extend( tokens.extend(
[ [
("class:sidebar", " "), ("class:sidebar", " "),
("class:sidebar.title", " %-36s" % category.title), ("class:sidebar.title", f" {category.title:36}"),
("class:sidebar", "\n"), ("class:sidebar", "\n"),
] ]
) )
@ -130,7 +130,7 @@ def python_sidebar(python_input: PythonInput) -> Window:
sel = ",selected" if selected else "" sel = ",selected" if selected else ""
tokens.append(("class:sidebar" + sel, " >" 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, " ", select_item))
tokens.append(("class:sidebar.status" + sel, f"{status}", goto_next)) tokens.append(("class:sidebar.status" + sel, f"{status}", goto_next))
@ -332,7 +332,7 @@ class PythonPromptMargin(PromptMargin):
width: int, line_number: int, is_soft_wrap: bool width: int, line_number: int, is_soft_wrap: bool
) -> StyleAndTextTuples: ) -> StyleAndTextTuples:
if python_input.show_line_numbers and not is_soft_wrap: 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)] return [("class:line-number", text)]
else: else:
return to_formatted_text(get_prompt_style().in2_prompt(width)) return to_formatted_text(get_prompt_style().in2_prompt(width))
@ -368,8 +368,7 @@ def status_bar(python_input: PythonInput) -> Container:
append( append(
( (
TB, TB,
"%i/%i " f"{python_buffer.working_index + 1}/{len(python_buffer._working_lines)} ",
% (python_buffer.working_index + 1, len(python_buffer._working_lines)),
) )
) )
@ -492,8 +491,7 @@ def show_sidebar_button_info(python_input: PythonInput) -> Container:
("class:status-toolbar", " - "), ("class:status-toolbar", " - "),
( (
"class:status-toolbar.python-version", "class:status-toolbar.python-version",
"%s %i.%i.%i" f"{platform.python_implementation()} {version[0]}.{version[1]}.{version[2]}",
% (platform.python_implementation(), version[0], version[1], version[2]),
), ),
("class:status-toolbar", " "), ("class:status-toolbar", " "),
] ]

View file

@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys
import traceback import traceback
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
@ -254,7 +253,6 @@ class OutputPrinter:
columns_in_buffer += width columns_in_buffer += width
current_line.append((style, c)) current_line.append((style, c))
if len(current_line) > 0:
yield current_line yield current_line
def _print_paginated_formatted_text( def _print_paginated_formatted_text(
@ -323,14 +321,20 @@ class OutputPrinter:
def _format_exception_output( def _format_exception_output(
self, e: BaseException, highlight: bool self, e: BaseException, highlight: bool
) -> Generator[OneStyleAndTextTuple, None, None]: ) -> Generator[OneStyleAndTextTuple, None, None]:
# Instead of just calling ``traceback.format_exc``, we take the if e.__cause__:
# traceback and skip the bottom calls of this framework. yield from self._format_exception_output(e.__cause__, highlight=highlight)
t, v, tb = sys.exc_info() 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",
)
# Required for pdb.post_mortem() to work. tblist = list(traceback.extract_tb(e.__traceback__))
sys.last_type, sys.last_value, sys.last_traceback = t, v, tb
tblist = list(traceback.extract_tb(tb))
for line_nr, tb_tuple in enumerate(tblist): for line_nr, tb_tuple in enumerate(tblist):
if tb_tuple[0] == "<stdin>": if tb_tuple[0] == "<stdin>":
@ -340,7 +344,7 @@ class OutputPrinter:
tb_list = traceback.format_list(tblist) tb_list = traceback.format_list(tblist)
if tb_list: if tb_list:
tb_list.insert(0, "Traceback (most recent call last):\n") 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) tb_str = "".join(tb_list)

View file

@ -20,7 +20,17 @@ import types
import warnings import warnings
from dis import COMPILER_FLAG_NAMES from dis import COMPILER_FLAG_NAMES
from pathlib import Path 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.formatted_text import OneStyleAndTextTuple
from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context
@ -362,7 +372,7 @@ class PythonRepl(PythonInput):
def _store_eval_result(self, result: object) -> None: def _store_eval_result(self, result: object) -> None:
locals: dict[str, Any] = self.get_locals() 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: def get_compiler_flags(self) -> int:
return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT
@ -378,6 +388,10 @@ class PythonRepl(PythonInput):
) )
def _handle_exception(self, e: BaseException) -> None: 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( self._get_output_printer().display_exception(
e, e,
highlight=self.enable_syntax_highlighting, highlight=self.enable_syntax_highlighting,
@ -501,6 +515,34 @@ 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 = ...,
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 = ...,
return_asyncio_coroutine: Literal[True] = ...,
) -> Coroutine[Any, Any, None]: ...
def embed( def embed(
globals: dict[str, Any] | None = None, globals: dict[str, Any] | None = None,
locals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None,
@ -511,7 +553,7 @@ def embed(
startup_paths: Sequence[str | Path] | None = None, startup_paths: Sequence[str | Path] | None = None,
patch_stdout: bool = False, patch_stdout: bool = False,
return_asyncio_coroutine: 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. Call this to embed Python shell at the current point in your program.
It's similar to `IPython.embed` and `bpython.embed`. :: It's similar to `IPython.embed` and `bpython.embed`. ::
@ -573,3 +615,4 @@ def embed(
else: else:
with patch_context: with patch_context:
repl.run() repl.run()
return None

View file

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

31
tests/test_dummy.py Executable file
View file

@ -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