From 042ecc3199b89819a071048fbd27e551ae25e114 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 28 Oct 2022 20:18:02 +0000 Subject: [PATCH 01/83] Improve DictionaryCompleter performance for slow mappings. The performance was bad when we had a huge custom mapping with an expensive `__getitem__`. --- ptpython/completer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 51a4086b..22698f8e 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -495,7 +495,7 @@ def abbr_meta(text: str) -> str: 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 +503,7 @@ def abbr_meta(text: str) -> str: 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 From 05d4aed170babf345e2daed90fff812349044ce4 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 28 Oct 2022 21:36:16 +0000 Subject: [PATCH 02/83] Added more precise types in various places. --- ptpython/completer.py | 25 ++++-- ptpython/entry_points/run_ptpython.py | 12 +-- ptpython/eventloop.py | 14 +-- ptpython/history_browser.py | 122 +++++++++++++++++--------- ptpython/ipython.py | 26 ++++-- ptpython/key_bindings.py | 61 +++++++------ ptpython/layout.py | 64 ++++++++------ ptpython/python_input.py | 52 +++++++---- ptpython/repl.py | 15 ++-- ptpython/signatures.py | 9 +- ptpython/utils.py | 36 ++++++-- ptpython/validator.py | 9 +- setup.cfg | 39 ++++++++ 13 files changed, 324 insertions(+), 160 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 22698f8e..2b6795d4 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -4,7 +4,7 @@ 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 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 @@ -574,7 +583,7 @@ def _sort_attribute_names(self, names: List[str]) -> List[str]: 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 @@ class ReprFailedError(Exception): _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 5ebe2b95..edffa44d 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -26,16 +26,16 @@ 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 @@ 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 c841972d..63dd7408 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -10,10 +10,12 @@ 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 wait_using_filehandler(): # 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 done(*a): 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 wait_using_polling(): 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/history_browser.py b/ptpython/history_browser.py index b7fe0865..08725ee0 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -5,6 +5,7 @@ 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.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 @@ 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 .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 @@ def __init__(self, history): 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 @@ def create_margin(self, window_render_info, width, height): 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 @@ def create_margin(self, window_render_info, width, height): 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 @@ def create_margin(self, window_render_info, width, height): 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 @@ def __init__(self, history, python_history, original_document): 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 @@ def get_new_document(self, cursor_pos=None): 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 _(event): @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 _(event): @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 _(event): @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 _(event): @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 _(event): 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 @@ def __init__(self, python_input, original_document): + 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 @@ def __init__(self, python_input, original_document): 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 @@ def __init__(self, python_input, original_document): 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 @@ def _default_buffer_pos_changed(self, _): ) ) - 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 91633340..9eafa995 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,7 @@ offer. """ +from typing import Iterable from warnings import warn from IPython import utils as ipy_utils @@ -15,6 +16,7 @@ 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.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 @@ def __init__(self, *args, **kwargs): 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 @@ def get_globals(): 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: diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index ae23a3df..147a321d 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.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 _(event): 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 _(event): 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 _(event): @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 is_multiline(): & ~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 _(event): & has_focus(DEFAULT_BUFFER) & is_multiline, ) - def _(event): + def _(event: E) -> None: """ Behaviour of the Enter key. @@ -153,11 +162,11 @@ def _(event): 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 at_the_end(b): 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 _(event): 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 _(event): @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 _(event): @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 _(event): @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 _(event): 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 _(event): 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 dc6b19bb..365f381b 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -5,7 +5,7 @@ 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 @@ 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 @@ 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 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 goto_next(mouse_event: MouseEvent) -> None: 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 move_cursor_up(self): ) -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 get_text_fragments(): ) -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 get_current_description(): i += 1 return "" - def get_help_text(): + def get_help_text() -> StyleAndTextTuples: return [(token, get_current_description())] return ConditionalContainer( @@ -225,7 +232,7 @@ def get_help_text(): ) -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 get_text_fragments() -> StyleAndTextTuples: 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 @@ def menu_position(): 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 @@ def menu_position(): 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 @@ def menu_position(): [ FloatContainer( content=HSplit( - [create_python_input_window()] + extra_body + [create_python_input_window()] + extra_body_list ), floats=[ Float( diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1785f523..c5611179 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,7 +6,18 @@ 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 @@ 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 @@ def __lt__(self, __other: Any) -> bool: _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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( # 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 @@ def option_count(self) -> int: 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 @@ def _generate_style(self) -> BaseStyle: 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 @@ def disable(attribute: str) -> bool: 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 @@ def get_values(): def _create_application( self, input: Optional[Input], output: Optional[Output] - ) -> Application: + ) -> Application[str]: """ Create an `Application` instance. """ @@ -926,7 +942,7 @@ def vi_mode(self, value: bool) -> None: 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 b55b5d56..3c729c0f 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -44,6 +44,7 @@ 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 @@ def _load_start_paths(self) -> None: 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 @@ def run(self) -> None: 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 @@ def run(self) -> None: 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 @@ def _format_result_output(self, result: object) -> StyleAndTextTuples: 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 get_locals(): 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 228b99b2..e836d33e 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -8,13 +8,16 @@ 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 @@ def get_annotation_name(annotation: object) -> str: ) @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 2fb24a41..ef96ca4b 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 handle_if_mouse_down(mouse_event: MouseEvent): 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 0f6a4eaf..ffac5839 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 @@ def validate(self, document): # 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: diff --git a/setup.cfg b/setup.cfg index 3c6e79cf..80dfec6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,41 @@ [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 From 1b7652d3f2fd35ea96789df60063516503811e68 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Nov 2022 09:10:12 +0000 Subject: [PATCH 03/83] Call Filter super() in PythonInputFilter. --- ptpython/filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/filters.py b/ptpython/filters.py index 1adac135..be85edf7 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -10,6 +10,7 @@ class PythonInputFilter(Filter): def __init__(self, python_input: "PythonInput") -> None: + super().__init__() self.python_input = python_input def __call__(self) -> bool: From d387b8e559803402105fe25e2c6d41f1beb583c8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 22 Nov 2022 09:11:22 +0000 Subject: [PATCH 04/83] Update test.yaml workflow. Test on 3.10 and skip mypy on 3.6. --- .github/workflows/test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0368ba7b..ef806cff 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 @@ -30,6 +30,7 @@ jobs: 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 From 8bbdc53179085371d4f88380ecb6273e86e6630d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 25 Nov 2022 13:56:44 +0000 Subject: [PATCH 05/83] Make ptipython respect more config changes See: https://github.com/prompt-toolkit/ptpython/pull/110 --- ptpython/ipython.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 9eafa995..db2a2049 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -277,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. @@ -290,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) From 100f4ae839e94dec1170523700e569058ca36aac Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 25 Nov 2022 14:50:21 +0000 Subject: [PATCH 06/83] Release 3.0.21 --- CHANGELOG | 16 ++++++++++++++++ setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 69a95e7d..ebc39c9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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/setup.py b/setup.py index a8214f27..274be8ee 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ 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, From b7205ac5657e0edf8a5877a1381a03beb66b9193 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Dec 2022 20:37:12 +0000 Subject: [PATCH 07/83] Improve rendering performance when there are many completions. (Make computing the "meta" text for the completion menu lazy.) --- ptpython/completer.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 2b6795d4..9252106e 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -476,14 +476,22 @@ def _get_item_lookup_completions( Complete dictionary keys. """ - def abbr_meta(text: str) -> str: + def meta_repr(value: object) -> Callable[[], str]: "Abbreviate meta text, make sure it fits on one line." - # Take first line, if multiple lines. - if len(text) > 20: - text = text[:20] + "..." - if "\n" in text: - text = text.split("\n", 1)[0] + "..." - return text + # We return a function, so that it gets computed when it's needed. + # When there are many completions, that improves the performance + # quite a bit (for the multi-column completion menu, we only need + # to display one meta text). + def get_value_repr() -> str: + text = self._do_repr(value) + + # Take first line, if multiple lines. + if "\n" in text: + text = text.split("\n", 1)[0] + "..." + + return text + + return get_value_repr match = self.item_lookup_pattern.search(document.text_before_cursor) if match is not None: @@ -512,12 +520,8 @@ def abbr_meta(text: str) -> str: k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=abbr_meta(self._do_repr(v)), + display_meta=meta_repr(v), ) - except KeyError: - # `result[k]` lookup failed. Trying to complete - # broken object. - pass except ReprFailedError: pass @@ -532,7 +536,7 @@ def abbr_meta(text: str) -> str: k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=abbr_meta(self._do_repr(result[k])), + display_meta=meta_repr(result[k]), ) except KeyError: # `result[k]` lookup failed. Trying to complete From d34704775faa5cd0926cfce9a4dcf3c26d0a178a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Dec 2022 20:56:40 +0000 Subject: [PATCH 08/83] Remove Python 3.6 from GitHub workflow (not supported anymore). --- .github/workflows/test.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ef806cff..7ec86626 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 @@ -23,14 +23,12 @@ jobs: 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 From b6fbf018ce252cb36dd296f5c93cdeb633c7acf1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Dec 2022 22:18:45 +0000 Subject: [PATCH 09/83] Release 3.0.22 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index ebc39c9c..916a5422 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.22: 2022-12-06 +------------------ + +New features: +- Improve rendering performance when there are many completions. + + 3.0.21: 2022-11-25 ------------------ diff --git a/setup.py b/setup.py index 274be8ee..2725dac4 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.21", + version="3.0.22", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 7a6b54026611d5ae9f6730cc476dceb79911654d Mon Sep 17 00:00:00 2001 From: Jongwook Choi Date: Wed, 7 Dec 2022 18:31:16 -0500 Subject: [PATCH 10/83] Fix documentation to correct ptpython.ipython import Fixes #506 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 15464ba4..2db3f695 100644 --- a/README.rst +++ b/README.rst @@ -213,7 +213,7 @@ This is also available for embedding: .. code:: python - from ptpython.ipython.repl import embed + from ptpython.ipython import embed embed(globals(), locals()) From af89ce2e82b09132daa3f6a62961e98d1105fbb3 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 21 Feb 2023 14:51:53 +0000 Subject: [PATCH 11/83] Fix code formatting (new Black version). --- ptpython/completer.py | 5 +---- ptpython/python_input.py | 2 +- ptpython/repl.py | 3 ++- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 9252106e..95383aaf 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -189,7 +189,6 @@ def get_completions( ): # If we are inside a string, Don't do Jedi completion. if not self._path_completer_grammar.match(document.text_before_cursor): - # Do Jedi Python completions. yield from self._jedi_completer.get_completions( document, complete_event @@ -399,7 +398,6 @@ def _lookup(self, expression: str, temp_locals: Dict[str, Any]) -> object: def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: - # First, find all for-loops, and assign the first item of the # collections they're iterating to the iterator variable, so that we # can provide code completion on the iterators. @@ -454,7 +452,6 @@ def _get_expression_completions( result = self.eval_expression(document, temp_locals) if result is not None: - if isinstance( result, (list, tuple, dict, collections_abc.Mapping, collections_abc.Sequence), @@ -478,6 +475,7 @@ def _get_item_lookup_completions( def meta_repr(value: object) -> Callable[[], str]: "Abbreviate meta text, make sure it fits on one line." + # We return a function, so that it gets computed when it's needed. # When there are many completions, that improves the performance # quite a bit (for the multi-column completion menu, we only need @@ -617,7 +615,6 @@ def __init__( def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: - completions = list(self.completer.get_completions(document, complete_event)) complete_private_attributes = self.complete_private_attributes() hide_private = False diff --git a/ptpython/python_input.py b/ptpython/python_input.py index c5611179..1a766c46 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -213,7 +213,6 @@ def __init__( _extra_toolbars=None, _input_buffer_height=None, ) -> None: - self.get_globals: _GetNamespace = get_globals or (lambda: {}) self.get_locals: _GetNamespace = get_locals or self.get_globals @@ -1043,6 +1042,7 @@ def read(self) -> str: This can raise EOFError, when Control-D is pressed. """ + # Capture the current input_mode in order to restore it after reset, # for ViState.reset() sets it to InputMode.INSERT unconditionally and # doesn't accept any arguments. diff --git a/ptpython/repl.py b/ptpython/repl.py index 3c729c0f..604d2b4a 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -96,7 +96,8 @@ def run_and_show_expression(self, expression: str) -> None: # Eval. try: result = self.eval(expression) - except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. + except KeyboardInterrupt: + # KeyboardInterrupt doesn't inherit from Exception. raise except SystemExit: raise From 1720189d9870a1059eacf7499b042c5f1ee5cf8d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 21 Feb 2023 13:57:28 +0000 Subject: [PATCH 12/83] Don't print exception twice in exception handler. The exception formatting itself already prints the exception message. Printing the exception again leads to lots of duplicated output if the exception contains a long multiline message. --- ptpython/repl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 604d2b4a..342852ff 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -573,8 +573,6 @@ def _handle_exception(self, e: BaseException) -> None: include_default_pygments_style=False, output=output, ) - - output.write("%s\n" % e) output.flush() def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None: From ec697aa4983085bbfa0cd0bfa78722f6cd1ff5b9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 21 Feb 2023 16:33:17 +0000 Subject: [PATCH 13/83] Add Python 3.11 to GitHub workflow. --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7ec86626..31837db3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] steps: - uses: actions/checkout@v2 From 3f24501e6e52a10669acf9c37d4dbdee24a00266 Mon Sep 17 00:00:00 2001 From: Itay R <0xItx@users.noreply.github.com> Date: Wed, 22 Feb 2023 12:20:28 +0200 Subject: [PATCH 14/83] Add macOS path to config.py's docstring (#501) --- examples/ptpython_config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index bf9d05fe..2b51dfc4 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -3,6 +3,7 @@ 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 """ from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.key_binding.key_processor import KeyPress From be972abf8d37f0cdc5553a945ef2d22bda4341b1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 21 Feb 2023 16:46:09 +0000 Subject: [PATCH 15/83] Drop Python 3.6. Now that prompt_toolkit itself dropped Python 3.6 support, we can drop Python 3.6 too. --- ptpython/__init__.py | 2 + ptpython/__main__.py | 2 + ptpython/completer.py | 40 ++++++++-------- ptpython/contrib/asyncssh_repl.py | 4 +- ptpython/entry_points/run_ptipython.py | 4 +- ptpython/entry_points/run_ptpython.py | 6 ++- ptpython/eventloop.py | 2 + ptpython/filters.py | 4 +- ptpython/history_browser.py | 32 ++++++------- ptpython/ipython.py | 6 ++- ptpython/key_bindings.py | 8 ++-- ptpython/layout.py | 46 +++++++++--------- ptpython/lexer.py | 4 +- ptpython/prompt_style.py | 4 +- ptpython/python_input.py | 65 ++++++++++++++------------ ptpython/repl.py | 20 ++++---- ptpython/signatures.py | 24 +++++----- ptpython/style.py | 8 ++-- ptpython/utils.py | 8 ++-- ptpython/validator.py | 4 +- setup.py | 4 +- tests/run_tests.py | 2 + 22 files changed, 169 insertions(+), 130 deletions(-) diff --git a/ptpython/__init__.py b/ptpython/__init__.py index 4908eba8..63c6233d 100644 --- a/ptpython/__init__.py +++ b/ptpython/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .repl import embed __all__ = ["embed"] diff --git a/ptpython/__main__.py b/ptpython/__main__.py index 83340a7b..c0062613 100644 --- a/ptpython/__main__.py +++ b/ptpython/__main__.py @@ -1,6 +1,8 @@ """ Make `python -m ptpython` an alias for running `./ptpython`. """ +from __future__ import annotations + from .entry_points.run_ptpython import run run() diff --git a/ptpython/completer.py b/ptpython/completer.py index 95383aaf..f610916e 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ast import collections.abc as collections_abc import inspect @@ -44,8 +46,8 @@ class PythonCompleter(Completer): def __init__( self, - get_globals: Callable[[], Dict[str, Any]], - get_locals: Callable[[], Dict[str, Any]], + get_globals: Callable[[], dict[str, Any]], + get_locals: Callable[[], dict[str, Any]], enable_dictionary_completion: Callable[[], bool], ) -> None: super().__init__() @@ -58,8 +60,8 @@ def __init__( self._jedi_completer = JediCompleter(get_globals, get_locals) self._dictionary_completer = DictionaryCompleter(get_globals, get_locals) - self._path_completer_cache: Optional[GrammarCompleter] = None - self._path_completer_grammar_cache: Optional["_CompiledGrammar"] = None + self._path_completer_cache: GrammarCompleter | None = None + self._path_completer_grammar_cache: _CompiledGrammar | None = None @property def _path_completer(self) -> GrammarCompleter: @@ -74,7 +76,7 @@ def _path_completer(self) -> GrammarCompleter: return self._path_completer_cache @property - def _path_completer_grammar(self) -> "_CompiledGrammar": + def _path_completer_grammar(self) -> _CompiledGrammar: """ Return the grammar for matching paths inside strings inside Python code. @@ -85,7 +87,7 @@ def _path_completer_grammar(self) -> "_CompiledGrammar": self._path_completer_grammar_cache = self._create_path_completer_grammar() return self._path_completer_grammar_cache - def _create_path_completer_grammar(self) -> "_CompiledGrammar": + def _create_path_completer_grammar(self) -> _CompiledGrammar: def unwrapper(text: str) -> str: return re.sub(r"\\(.)", r"\1", text) @@ -202,8 +204,8 @@ class JediCompleter(Completer): def __init__( self, - get_globals: Callable[[], Dict[str, Any]], - get_locals: Callable[[], Dict[str, Any]], + get_globals: Callable[[], dict[str, Any]], + get_locals: Callable[[], dict[str, Any]], ) -> None: super().__init__() @@ -241,7 +243,7 @@ def get_completions( # Jedi issue: "KeyError: u'a_lambda'." # https://github.com/jonathanslenders/ptpython/issues/89 pass - except IOError: + except OSError: # Jedi issue: "IOError: No such file or directory." # https://github.com/jonathanslenders/ptpython/issues/71 pass @@ -302,8 +304,8 @@ class DictionaryCompleter(Completer): def __init__( self, - get_globals: Callable[[], Dict[str, Any]], - get_locals: Callable[[], Dict[str, Any]], + get_globals: Callable[[], dict[str, Any]], + get_locals: Callable[[], dict[str, Any]], ) -> None: super().__init__() @@ -385,7 +387,7 @@ def __init__( re.VERBOSE, ) - def _lookup(self, expression: str, temp_locals: Dict[str, Any]) -> object: + def _lookup(self, expression: str, temp_locals: dict[str, Any]) -> object: """ Do lookup of `object_var` in the context. `temp_locals` is a dictionary, used for the locals. @@ -429,7 +431,7 @@ def _do_repr(self, obj: object) -> str: except BaseException: raise ReprFailedError - def eval_expression(self, document: Document, locals: Dict[str, Any]) -> object: + def eval_expression(self, document: Document, locals: dict[str, Any]) -> object: """ Evaluate """ @@ -444,7 +446,7 @@ def _get_expression_completions( self, document: Document, complete_event: CompleteEvent, - temp_locals: Dict[str, Any], + temp_locals: dict[str, Any], ) -> Iterable[Completion]: """ Complete the [ or . operator after an object. @@ -467,7 +469,7 @@ def _get_item_lookup_completions( self, document: Document, complete_event: CompleteEvent, - temp_locals: Dict[str, Any], + temp_locals: dict[str, Any], ) -> Iterable[Completion]: """ Complete dictionary keys. @@ -547,7 +549,7 @@ def _get_attribute_completions( self, document: Document, complete_event: CompleteEvent, - temp_locals: Dict[str, Any], + temp_locals: dict[str, Any], ) -> Iterable[Completion]: """ Complete attribute names. @@ -579,13 +581,13 @@ def get_suffix(name: str) -> str: suffix = get_suffix(name) yield Completion(name, -len(attr_name), display=name + suffix) - def _sort_attribute_names(self, names: List[str]) -> List[str]: + def _sort_attribute_names(self, names: list[str]) -> list[str]: """ Sort attribute names alphabetically, but move the double underscore and underscore names to the end. """ - def sort_key(name: str) -> Tuple[int, str]: + def sort_key(name: str) -> tuple[int, str]: if name.startswith("__"): return (2, name) # Double underscore comes latest. if name.startswith("_"): @@ -650,7 +652,7 @@ class ReprFailedError(Exception): def _get_style_for_jedi_completion( - jedi_completion: "jedi.api.classes.Completion", + jedi_completion: jedi.api.classes.Completion, ) -> str: """ Return completion style to use for this name. diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 4c36217d..0347aded 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -6,6 +6,8 @@ should make sure not to use Python 3-only syntax, because this package should be installable in Python 2 as well! """ +from __future__ import annotations + import asyncio from typing import Any, Optional, TextIO, cast @@ -29,7 +31,7 @@ class ReplSSHServerSession(asyncssh.SSHServerSession): """ def __init__( - self, get_globals: _GetNamespace, get_locals: Optional[_GetNamespace] = None + self, get_globals: _GetNamespace, get_locals: _GetNamespace | None = None ) -> None: self._chan: Any = None diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py index 21d70637..b660a0ac 100644 --- a/ptpython/entry_points/run_ptipython.py +++ b/ptpython/entry_points/run_ptipython.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +from __future__ import annotations + import os import sys @@ -58,7 +60,7 @@ def run(user_ns=None): code = compile(f.read(), path, "exec") exec(code, user_ns, user_ns) else: - print("File not found: {}\n\n".format(path)) + print(f"File not found: {path}\n\n") sys.exit(1) # Apply config file diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index edffa44d..1b4074d4 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -21,6 +21,8 @@ PTPYTHON_CONFIG_HOME: a configuration directory to use PYTHONSTARTUP: file executed on interactive startup (no default) """ +from __future__ import annotations + import argparse import os import pathlib @@ -44,7 +46,7 @@ class _Parser(argparse.ArgumentParser): - def print_help(self, file: Optional[IO[str]] = None) -> None: + def print_help(self, file: IO[str] | None = None) -> None: super().print_help() print( dedent( @@ -90,7 +92,7 @@ def create_parser() -> _Parser: return parser -def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str]: +def get_config_and_history_file(namespace: argparse.Namespace) -> tuple[str, str]: """ Check which config/history files to use, ensure that the directories for these files exist, and return the config and history path. diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 63dd7408..14ab64be 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -7,6 +7,8 @@ in readline. ``prompt-toolkit`` doesn't understand that input hook, but this will fix it for Tk.) """ +from __future__ import annotations + import sys import time diff --git a/ptpython/filters.py b/ptpython/filters.py index be85edf7..a2079fd3 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from prompt_toolkit.filters import Filter @@ -9,7 +11,7 @@ class PythonInputFilter(Filter): - def __init__(self, python_input: "PythonInput") -> None: + def __init__(self, python_input: PythonInput) -> None: super().__init__() self.python_input = python_input diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 08725ee0..81cc63ae 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -4,6 +4,8 @@ `create_history_application` creates an `Application` instance that runs will run as a sub application of the Repl/PythonInput. """ +from __future__ import annotations + from functools import partial from typing import TYPE_CHECKING, Callable, List, Optional, Set @@ -128,7 +130,7 @@ class HistoryLayout: application. """ - def __init__(self, history: "PythonHistory") -> None: + def __init__(self, history: PythonHistory) -> None: search_toolbar = SearchToolbar() self.help_buffer_control = BufferControl( @@ -224,7 +226,7 @@ def _get_top_toolbar_fragments() -> StyleAndTextTuples: return [("class:status-bar.title", "History browser - Insert from history")] -def _get_bottom_toolbar_fragments(history: "PythonHistory") -> StyleAndTextTuples: +def _get_bottom_toolbar_fragments(history: PythonHistory) -> StyleAndTextTuples: python_input = history.python_input @if_mousedown @@ -258,7 +260,7 @@ class HistoryMargin(Margin): This displays a green bar for the selected entries. """ - def __init__(self, history: "PythonHistory") -> None: + def __init__(self, history: PythonHistory) -> None: self.history_buffer = history.history_buffer self.history_mapping = history.history_mapping @@ -307,7 +309,7 @@ class ResultMargin(Margin): The margin to be shown in the result pane. """ - def __init__(self, history: "PythonHistory") -> None: + def __init__(self, history: PythonHistory) -> None: self.history_mapping = history.history_mapping self.history_buffer = history.history_buffer @@ -356,7 +358,7 @@ class GrayExistingText(Processor): Turn the existing input, before and after the inserted code gray. """ - def __init__(self, history_mapping: "HistoryMapping") -> None: + def __init__(self, history_mapping: HistoryMapping) -> None: self.history_mapping = history_mapping self._lines_before = len( history_mapping.original_document.text_before_cursor.splitlines() @@ -384,7 +386,7 @@ class HistoryMapping: def __init__( self, - history: "PythonHistory", + history: PythonHistory, python_history: History, original_document: Document, ) -> None: @@ -393,11 +395,11 @@ def __init__( self.original_document = original_document self.lines_starting_new_entries = set() - self.selected_lines: Set[int] = set() + self.selected_lines: set[int] = set() # Process history. history_strings = python_history.get_strings() - history_lines: List[str] = [] + history_lines: list[str] = [] for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]: self.lines_starting_new_entries.add(len(history_lines)) @@ -419,7 +421,7 @@ def __init__( else: self.result_line_offset = 0 - def get_new_document(self, cursor_pos: Optional[int] = None) -> Document: + def get_new_document(self, cursor_pos: int | None = None) -> Document: """ Create a `Document` instance that contains the resulting text. """ @@ -449,7 +451,7 @@ def update_default_buffer(self) -> None: b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True) -def _toggle_help(history: "PythonHistory") -> None: +def _toggle_help(history: PythonHistory) -> None: "Display/hide help." help_buffer_control = history.history_layout.help_buffer_control @@ -459,7 +461,7 @@ def _toggle_help(history: "PythonHistory") -> None: history.app.layout.current_control = help_buffer_control -def _select_other_window(history: "PythonHistory") -> None: +def _select_other_window(history: PythonHistory) -> None: "Toggle focus between left/right window." current_buffer = history.app.current_buffer layout = history.history_layout.layout @@ -472,8 +474,8 @@ def _select_other_window(history: "PythonHistory") -> None: def create_key_bindings( - history: "PythonHistory", - python_input: "PythonInput", + history: PythonHistory, + python_input: PythonInput, history_mapping: HistoryMapping, ) -> KeyBindings: """ @@ -592,9 +594,7 @@ def _(event: E) -> None: class PythonHistory: - def __init__( - self, python_input: "PythonInput", original_document: Document - ) -> None: + 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`. diff --git a/ptpython/ipython.py b/ptpython/ipython.py index db2a2049..fb4b5ed9 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,8 @@ offer. """ +from __future__ import annotations + from typing import Iterable from warnings import warn @@ -62,12 +64,12 @@ def out_prompt(self) -> AnyFormattedText: class IPythonValidator(PythonValidator): def __init__(self, *args, **kwargs): - super(IPythonValidator, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.isp = IPythonInputSplitter() def validate(self, document: Document) -> None: document = Document(text=self.isp.transform_cell(document.text)) - super(IPythonValidator, self).validate(document) + super().validate(document) def create_ipython_grammar(): diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 147a321d..6b4c1862 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from prompt_toolkit.application import get_app @@ -47,7 +49,7 @@ def tab_should_insert_whitespace() -> bool: return bool(b.text and (not before_cursor or before_cursor.isspace())) -def load_python_bindings(python_input: "PythonInput") -> KeyBindings: +def load_python_bindings(python_input: PythonInput) -> KeyBindings: """ Custom key bindings. """ @@ -218,7 +220,7 @@ def _(event: E) -> None: return bindings -def load_sidebar_bindings(python_input: "PythonInput") -> KeyBindings: +def load_sidebar_bindings(python_input: PythonInput) -> KeyBindings: """ Load bindings for the navigation in the sidebar. """ @@ -273,7 +275,7 @@ def _(event: E) -> None: return bindings -def load_confirm_exit_bindings(python_input: "PythonInput") -> KeyBindings: +def load_confirm_exit_bindings(python_input: PythonInput) -> KeyBindings: """ Handle yes/no key presses when the exit confirmation is shown. """ diff --git a/ptpython/layout.py b/ptpython/layout.py index 365f381b..2c6395ce 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -1,6 +1,8 @@ """ Creation of the `Layout` instance for the Python input/REPL. """ +from __future__ import annotations + import platform import sys from enum import Enum @@ -78,26 +80,26 @@ class CompletionVisualisation(Enum): TOOLBAR = "toolbar" -def show_completions_toolbar(python_input: "PythonInput") -> Condition: +def show_completions_toolbar(python_input: PythonInput) -> Condition: return Condition( lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR ) -def show_completions_menu(python_input: "PythonInput") -> Condition: +def show_completions_menu(python_input: PythonInput) -> Condition: return Condition( lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP ) -def show_multi_column_completions_menu(python_input: "PythonInput") -> Condition: +def show_multi_column_completions_menu(python_input: PythonInput) -> Condition: return Condition( lambda: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN ) -def python_sidebar(python_input: "PythonInput") -> Window: +def python_sidebar(python_input: PythonInput) -> Window: """ Create the `Layout` for the sidebar with the configurable options. """ @@ -105,7 +107,7 @@ def python_sidebar(python_input: "PythonInput") -> Window: def get_text_fragments() -> StyleAndTextTuples: tokens: StyleAndTextTuples = [] - def append_category(category: "OptionCategory[Any]") -> None: + def append_category(category: OptionCategory[Any]) -> None: tokens.extend( [ ("class:sidebar", " "), @@ -172,7 +174,7 @@ def move_cursor_up(self) -> None: ) -def python_sidebar_navigation(python_input: "PythonInput") -> Window: +def python_sidebar_navigation(python_input: PythonInput) -> Window: """ Create the `Layout` showing the navigation information for the sidebar. """ @@ -198,7 +200,7 @@ def get_text_fragments() -> StyleAndTextTuples: ) -def python_sidebar_help(python_input: "PythonInput") -> Container: +def python_sidebar_help(python_input: PythonInput) -> Container: """ Create the `Layout` for the help text for the current item in the sidebar. """ @@ -232,7 +234,7 @@ def get_help_text() -> StyleAndTextTuples: ) -def signature_toolbar(python_input: "PythonInput") -> Container: +def signature_toolbar(python_input: PythonInput) -> Container: """ Return the `Layout` for the signature. """ @@ -318,7 +320,7 @@ class PythonPromptMargin(PromptMargin): It shows something like "In [1]:". """ - def __init__(self, python_input: "PythonInput") -> None: + def __init__(self, python_input: PythonInput) -> None: self.python_input = python_input def get_prompt_style() -> PromptStyle: @@ -339,7 +341,7 @@ def get_continuation( super().__init__(get_prompt, get_continuation) -def status_bar(python_input: "PythonInput") -> Container: +def status_bar(python_input: PythonInput) -> Container: """ Create the `Layout` for the status bar. """ @@ -412,7 +414,7 @@ def get_text_fragments() -> StyleAndTextTuples: ) -def get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples: +def get_inputmode_fragments(python_input: PythonInput) -> StyleAndTextTuples: """ Return current input mode as a list of (token, text) tuples for use in a toolbar. @@ -440,7 +442,7 @@ def toggle_vi_mode(mouse_event: MouseEvent) -> None: recording_register = app.vi_state.recording_register if recording_register: append((token, " ")) - append((token + " class:record", "RECORD({})".format(recording_register))) + append((token + " class:record", f"RECORD({recording_register})")) append((token, " - ")) if app.current_buffer.selection_state is not None: @@ -473,7 +475,7 @@ def toggle_vi_mode(mouse_event: MouseEvent) -> None: return result -def show_sidebar_button_info(python_input: "PythonInput") -> Container: +def show_sidebar_button_info(python_input: PythonInput) -> Container: """ Create `Layout` for the information in the right-bottom corner. (The right part of the status bar.) @@ -519,7 +521,7 @@ def get_text_fragments() -> StyleAndTextTuples: def create_exit_confirmation( - python_input: "PythonInput", style: str = "class:exit-confirmation" + python_input: PythonInput, style: str = "class:exit-confirmation" ) -> Container: """ Create `Layout` for the exit message. @@ -543,7 +545,7 @@ def get_text_fragments() -> StyleAndTextTuples: ) -def meta_enter_message(python_input: "PythonInput") -> Container: +def meta_enter_message(python_input: PythonInput) -> Container: """ Create the `Layout` for the 'Meta+Enter` message. """ @@ -575,15 +577,15 @@ def extra_condition() -> bool: class PtPythonLayout: def __init__( self, - python_input: "PythonInput", + python_input: PythonInput, 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, + extra_body: AnyContainer | None = None, + extra_toolbars: list[AnyContainer] | None = None, + extra_buffer_processors: list[Processor] | None = None, + input_buffer_height: AnyDimension | None = None, ) -> None: D = Dimension - extra_body_list: List[AnyContainer] = [extra_body] if extra_body else [] + extra_body_list: list[AnyContainer] = [extra_body] if extra_body else [] extra_toolbars = extra_toolbars or [] input_buffer_height = input_buffer_height or D(min=6) @@ -591,7 +593,7 @@ def __init__( search_toolbar = SearchToolbar(python_input.search_buffer) def create_python_input_window() -> Window: - def menu_position() -> Optional[int]: + def menu_position() -> int | None: """ When there is no autocompletion menu to be shown, and we have a signature, set the pop-up position at `bracket_start`. diff --git a/ptpython/lexer.py b/ptpython/lexer.py index 62e470f8..81924c9d 100644 --- a/ptpython/lexer.py +++ b/ptpython/lexer.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Callable, Optional from prompt_toolkit.document import Document @@ -17,7 +19,7 @@ class PtpythonLexer(Lexer): use a Python 3 lexer. """ - def __init__(self, python_lexer: Optional[Lexer] = None) -> None: + def __init__(self, python_lexer: Lexer | None = None) -> None: self.python_lexer = python_lexer or PygmentsLexer(PythonLexer) self.system_lexer = PygmentsLexer(BashLexer) diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index e7334af2..96b738f7 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING @@ -40,7 +42,7 @@ class IPythonPrompt(PromptStyle): A prompt resembling the IPython prompt. """ - def __init__(self, python_input: "PythonInput") -> None: + def __init__(self, python_input: PythonInput) -> None: self.python_input = python_input def in_prompt(self) -> AnyFormattedText: diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1a766c46..e8170f2b 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -2,7 +2,7 @@ Application for reading Python input. This can be used for creation of Python REPLs. """ -import __future__ +from __future__ import annotations from asyncio import get_event_loop from functools import partial @@ -84,6 +84,11 @@ from .utils import unindent_code from .validator import PythonValidator +# Isort introduces a SyntaxError, if we'd write `import __future__`. +# https://github.com/PyCQA/isort/issues/2100 +__future__ = __import__("__future__") + + __all__ = ["PythonInput"] @@ -101,7 +106,7 @@ def __lt__(self, __other: Any) -> bool: class OptionCategory(Generic[_T]): - def __init__(self, title: str, options: List["Option[_T]"]) -> None: + def __init__(self, title: str, options: list[Option[_T]]) -> None: self.title = title self.options = options @@ -194,22 +199,22 @@ class PythonInput: def __init__( self, - get_globals: Optional[_GetNamespace] = None, - get_locals: Optional[_GetNamespace] = None, - history_filename: Optional[str] = None, + get_globals: _GetNamespace | None = None, + get_locals: _GetNamespace | None = None, + history_filename: str | None = None, vi_mode: bool = False, - color_depth: Optional[ColorDepth] = None, + color_depth: ColorDepth | None = None, # Input/output. - input: Optional[Input] = None, - output: Optional[Output] = None, + input: Input | None = None, + output: Output | None = None, # For internal use. - extra_key_bindings: Optional[KeyBindings] = None, + extra_key_bindings: KeyBindings | None = None, create_app: bool = True, - _completer: Optional[Completer] = None, - _validator: Optional[Validator] = None, - _lexer: Optional[Lexer] = None, + _completer: Completer | None = None, + _validator: Validator | None = None, + _lexer: Lexer | None = None, _extra_buffer_processors=None, - _extra_layout_body: Optional[AnyContainer] = None, + _extra_layout_body: AnyContainer | None = None, _extra_toolbars=None, _input_buffer_height=None, ) -> None: @@ -309,7 +314,7 @@ def __init__( self.show_exit_confirmation: bool = False # The title to be displayed in the terminal. (None or string.) - self.terminal_title: Optional[str] = None + self.terminal_title: str | None = None self.exit_message: str = "Do you really want to exit?" self.insert_blank_line_after_output: bool = True # (For the REPL.) @@ -324,7 +329,7 @@ def __init__( self.prompt_style: str = "classic" # The currently active style. # Styles selectable from the menu. - self.all_prompt_styles: Dict[str, PromptStyle] = { + self.all_prompt_styles: dict[str, PromptStyle] = { "ipython": IPythonPrompt(self), "classic": ClassicPrompt(), } @@ -338,7 +343,7 @@ def __init__( ].out_prompt() #: Load styles. - self.code_styles: Dict[str, BaseStyle] = get_all_code_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_ui_style_name: str = "default" @@ -360,7 +365,7 @@ def __init__( self.current_statement_index: int = 1 # Code signatures. (This is set asynchronously after a timeout.) - self.signatures: List[Signature] = [] + self.signatures: list[Signature] = [] # Boolean indicating whether we have a signatures thread running. # (Never run more than one at the same time.) @@ -399,9 +404,7 @@ def __init__( # 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[str]] = self._create_application( - input, output - ) + self._app: Application[str] | None = self._create_application(input, output) # Setting vi_mode will not work unless the prompt_toolkit # application has been created. if vi_mode: @@ -527,7 +530,7 @@ def _generate_style(self) -> BaseStyle: self.ui_styles[self._current_ui_style_name], ) - def _create_options(self) -> List[OptionCategory[Any]]: + def _create_options(self) -> list[OptionCategory[Any]]: """ Create a list of `Option` instances for the options sidebar. """ @@ -546,14 +549,14 @@ def simple_option( title: str, description: str, field_name: str, - values: Tuple[str, str] = ("off", "on"), + values: tuple[str, str] = ("off", "on"), ) -> Option[str]: "Create Simple on/of option." def get_current_value() -> str: return values[bool(getattr(self, field_name))] - def get_values() -> Dict[str, Callable[[], bool]]: + def get_values() -> dict[str, Callable[[], bool]]: return { values[1]: lambda: enable(field_name), values[0]: lambda: disable(field_name), @@ -730,10 +733,10 @@ def get_values() -> Dict[str, Callable[[], bool]]: title="Prompt", description="Visualisation of the prompt. ('>>>' or 'In [1]:')", get_current_value=lambda: self.prompt_style, - get_values=lambda: dict( - (s, partial(enable, "prompt_style", s)) + get_values=lambda: { + s: partial(enable, "prompt_style", s) for s in self.all_prompt_styles - ), + }, ), simple_option( title="Blank line after input", @@ -825,10 +828,10 @@ def get_values() -> Dict[str, Callable[[], bool]]: title="User interface", description="Color scheme to use for the user interface.", get_current_value=lambda: self._current_ui_style_name, - get_values=lambda: dict( - (name, partial(self.use_ui_colorscheme, name)) + get_values=lambda: { + name: partial(self.use_ui_colorscheme, name) for name in self.ui_styles - ), + }, ), Option( title="Color depth", @@ -862,7 +865,7 @@ def get_values() -> Dict[str, Callable[[], bool]]: ] def _create_application( - self, input: Optional[Input], output: Optional[Output] + self, input: Input | None, output: Output | None ) -> Application[str]: """ Create an `Application` instance. @@ -952,7 +955,7 @@ def _on_input_timeout(self, buff: Buffer) -> None: in another thread, get the signature of the current code. """ - def get_signatures_in_executor(document: Document) -> List[Signature]: + def get_signatures_in_executor(document: Document) -> list[Signature]: # First, get signatures from Jedi. If we didn't found any and if # "dictionary completion" (eval-based completion) is enabled, then # get signatures using eval. diff --git a/ptpython/repl.py b/ptpython/repl.py index 342852ff..a3dd788e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -7,6 +7,8 @@ embed(globals(), locals(), vi_mode=False) """ +from __future__ import annotations + import asyncio import builtins import os @@ -53,7 +55,7 @@ __all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"] -def _get_coroutine_flag() -> Optional[int]: +def _get_coroutine_flag() -> int | None: for k, v in COMPILER_FLAG_NAMES.items(): if v == "COROUTINE": return k @@ -62,7 +64,7 @@ def _get_coroutine_flag() -> Optional[int]: return None -COROUTINE_FLAG: Optional[int] = _get_coroutine_flag() +COROUTINE_FLAG: int | None = _get_coroutine_flag() def _has_coroutine_flag(code: types.CodeType) -> bool: @@ -89,7 +91,7 @@ def _load_start_paths(self) -> None: exec(code, self.get_globals(), self.get_locals()) else: output = self.app.output - output.write("WARNING | File not found: {}\n\n".format(path)) + output.write(f"WARNING | File not found: {path}\n\n") def run_and_show_expression(self, expression: str) -> None: try: @@ -300,7 +302,7 @@ async def eval_async(self, line: str) -> object: return 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 def get_compiler_flags(self) -> int: @@ -524,7 +526,7 @@ def show_pager() -> None: flush_page() - def create_pager_prompt(self) -> PromptSession["PagerResult"]: + def create_pager_prompt(self) -> PromptSession[PagerResult]: """ Create pager --MORE-- prompt. """ @@ -651,7 +653,7 @@ def enter_to_continue() -> None: # Run the config file in an empty namespace. try: - namespace: Dict[str, Any] = {} + namespace: dict[str, Any] = {} with open(config_file, "rb") as f: code = compile(f.read(), config_file, "exec") @@ -670,10 +672,10 @@ def enter_to_continue() -> None: def embed( globals=None, locals=None, - configure: Optional[Callable[[PythonRepl], None]] = None, + configure: Callable[[PythonRepl], None] | None = None, vi_mode: bool = False, - history_filename: Optional[str] = None, - title: Optional[str] = None, + history_filename: str | None = None, + title: str | None = None, startup_paths=None, patch_stdout: bool = False, return_asyncio_coroutine: bool = False, diff --git a/ptpython/signatures.py b/ptpython/signatures.py index e836d33e..5a6f286a 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -5,6 +5,8 @@ Either with the Jedi library, or using `inspect.signature` if Jedi fails and we can use `eval()` to evaluate the function object. """ +from __future__ import annotations + import inspect from inspect import Signature as InspectSignature from inspect import _ParameterKind as ParameterKind @@ -25,8 +27,8 @@ class Parameter: def __init__( self, name: str, - annotation: Optional[str], - default: Optional[str], + annotation: str | None, + default: str | None, kind: ParameterKind, ) -> None: self.name = name @@ -66,9 +68,9 @@ def __init__( name: str, docstring: str, parameters: Sequence[Parameter], - index: Optional[int] = None, + index: int | None = None, returns: str = "", - bracket_start: Tuple[int, int] = (0, 0), + bracket_start: tuple[int, int] = (0, 0), ) -> None: self.name = name self.docstring = docstring @@ -84,7 +86,7 @@ def from_inspect_signature( docstring: str, signature: InspectSignature, index: int, - ) -> "Signature": + ) -> Signature: parameters = [] def get_annotation_name(annotation: object) -> str: @@ -123,9 +125,7 @@ def get_annotation_name(annotation: object) -> str: ) @classmethod - def from_jedi_signature( - cls, signature: "jedi.api.classes.Signature" - ) -> "Signature": + def from_jedi_signature(cls, signature: jedi.api.classes.Signature) -> Signature: parameters = [] for p in signature.params: @@ -160,8 +160,8 @@ def __repr__(self) -> str: def get_signatures_using_jedi( - document: Document, locals: Dict[str, Any], globals: Dict[str, Any] -) -> List[Signature]: + document: Document, locals: dict[str, Any], globals: dict[str, Any] +) -> list[Signature]: script = get_jedi_script_from_document(document, locals, globals) # Show signatures in help text. @@ -195,8 +195,8 @@ def get_signatures_using_jedi( def get_signatures_using_eval( - document: Document, locals: Dict[str, Any], globals: Dict[str, Any] -) -> List[Signature]: + document: Document, locals: dict[str, Any], globals: dict[str, Any] +) -> list[Signature]: """ Look for the signature of the function before the cursor position without use of Jedi. This uses a similar approach as the `DictionaryCompleter` of diff --git a/ptpython/style.py b/ptpython/style.py index 4b54d0cd..199d5abf 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Dict from prompt_toolkit.styles import BaseStyle, Style, merge_styles @@ -8,11 +10,11 @@ __all__ = ["get_all_code_styles", "get_all_ui_styles", "generate_style"] -def get_all_code_styles() -> Dict[str, BaseStyle]: +def get_all_code_styles() -> dict[str, BaseStyle]: """ Return a mapping from style names to their classes. """ - result: Dict[str, BaseStyle] = { + result: dict[str, BaseStyle] = { name: style_from_pygments_cls(get_style_by_name(name)) for name in get_all_styles() } @@ -20,7 +22,7 @@ def get_all_code_styles() -> Dict[str, BaseStyle]: return result -def get_all_ui_styles() -> Dict[str, BaseStyle]: +def get_all_ui_styles() -> dict[str, BaseStyle]: """ Return a dict mapping {ui_style_name -> style_dict}. """ diff --git a/ptpython/utils.py b/ptpython/utils.py index ef96ca4b..53488997 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -1,6 +1,8 @@ """ For internal use only. """ +from __future__ import annotations + import re from typing import ( TYPE_CHECKING, @@ -65,8 +67,8 @@ def has_unclosed_brackets(text: str) -> bool: def get_jedi_script_from_document( - document: Document, locals: Dict[str, Any], globals: Dict[str, Any] -) -> "Interpreter": + 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'. @@ -154,7 +156,7 @@ def if_mousedown(handler: _T) -> _T: by the Window.) """ - def handle_if_mouse_down(mouse_event: MouseEvent) -> "NotImplementedOrNone": + def handle_if_mouse_down(mouse_event: MouseEvent) -> NotImplementedOrNone: if mouse_event.event_type == MouseEventType.MOUSE_DOWN: return handler(mouse_event) else: diff --git a/ptpython/validator.py b/ptpython/validator.py index ffac5839..3b36d273 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Callable, Optional from prompt_toolkit.document import Document @@ -16,7 +18,7 @@ class PythonValidator(Validator): active compiler flags. """ - def __init__(self, get_compiler_flags: Optional[Callable[[], int]] = None) -> None: + def __init__(self, get_compiler_flags: Callable[[], int] | None = None) -> None: self.get_compiler_flags = get_compiler_flags def validate(self, document: Document) -> None: diff --git a/setup.py b/setup.py index 2725dac4..c4087f9c 100644 --- a/setup.py +++ b/setup.py @@ -25,10 +25,10 @@ "prompt_toolkit>=3.0.18,<3.1.0", "pygments", ], - python_requires=">=3.6", + python_requires=">=3.7", classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", diff --git a/tests/run_tests.py b/tests/run_tests.py index 2f945163..0de37430 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +from __future__ import annotations + import unittest import ptpython.completer From 2d4b0b0d04973e49cf0fe35a71e62c4ca486eed1 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sun, 17 Jul 2022 08:46:58 +1000 Subject: [PATCH 16/83] docs: Fix a few typos There are small typos in: - docs/concurrency-challenges.rst - examples/ptpython_config/config.py - ptpython/completer.py - ptpython/history_browser.py - ptpython/key_bindings.py - ptpython/repl.py Fixes: - Should read `returns` rather than `retuns`. - Should read `parentheses` rather than `parethesis`. - Should read `output` rather than `ouptut`. - Should read `navigation` rather than `navigaton`. - Should read `executor` rather than `excecutor`. - Should read `depending` rather than `deponding`. Signed-off-by: Tim Gates --- docs/concurrency-challenges.rst | 2 +- examples/ptpython_config/config.py | 2 +- ptpython/completer.py | 2 +- ptpython/history_browser.py | 2 +- ptpython/key_bindings.py | 2 +- ptpython/repl.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/concurrency-challenges.rst b/docs/concurrency-challenges.rst index b56d9698..0ff9c6c3 100644 --- a/docs/concurrency-challenges.rst +++ b/docs/concurrency-challenges.rst @@ -67,7 +67,7 @@ When a normal blocking embed is used: 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(...)`. + and do an `await loop.run_in_executor(...)`. * The "eval" happens again in the main thread. * "print" is also similar, except that the pager code (if used) runs in an executor too. diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 2b51dfc4..2f3f49dd 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -50,7 +50,7 @@ def configure(repl): # Swap light/dark colors on or off repl.swap_light_and_dark = False - # Highlight matching parethesis. + # Highlight matching parentheses. repl.highlight_matching_parenthesis = True # Line wrapping. (Instead of horizontal scrolling.) diff --git a/ptpython/completer.py b/ptpython/completer.py index f610916e..3c5dd32f 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -599,7 +599,7 @@ def sort_key(name: str) -> tuple[int, str]: class HidePrivateCompleter(Completer): """ - Wrapper around completer that hides private fields, deponding on whether or + Wrapper around completer that hides private fields, depending on whether or not public fields are shown. (The reason this is implemented as a `Completer` wrapper is because this diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 81cc63ae..eea81c2e 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -599,7 +599,7 @@ def __init__(self, python_input: PythonInput, original_document: Document) -> No Create an `Application` for the history screen. This has to be run as a sub application of `python_input`. - When this application runs and returns, it retuns the selected lines. + When this application runs and returns, it returns the selected lines. """ self.python_input = python_input diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index 6b4c1862..d7bb575e 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -159,7 +159,7 @@ def _(event: E) -> None: Behaviour of the Enter key. Auto indent after newline/Enter. - (When not in Vi navigaton mode, and when multiline is enabled.) + (When not in Vi navigation mode, and when multiline is enabled.) """ b = event.current_buffer empty_lines_required = python_input.accept_input_on_enter or 10000 diff --git a/ptpython/repl.py b/ptpython/repl.py index a3dd788e..02a5075d 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -405,7 +405,7 @@ def _format_result_output(self, result: object) -> StyleAndTextTuples: def show_result(self, result: object) -> None: """ - Show __repr__ for an `eval` result and print to ouptut. + Show __repr__ for an `eval` result and print to output. """ formatted_text_output = self._format_result_output(result) From ea6b2c51db96e260b2ce32574938bd844f7a01ce Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 22 Feb 2023 10:35:43 +0000 Subject: [PATCH 17/83] Fix completer suffix for mappings/sequences. --- ptpython/completer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 3c5dd32f..f28d2b16 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -568,9 +568,9 @@ def get_suffix(name: str) -> str: obj = getattr(result, name, None) if inspect.isfunction(obj) or inspect.ismethod(obj): return "()" - if isinstance(obj, dict): + if isinstance(obj, collections_abc.Mapping): return "{}" - if isinstance(obj, (list, tuple)): + if isinstance(obj, collections_abc.Sequence): return "[]" except: pass From ee047a2701fcd269592a626e947bf9625db5eb6d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 22 Feb 2023 10:36:12 +0000 Subject: [PATCH 18/83] Add cursor shape support. --- ptpython/layout.py | 2 +- ptpython/python_input.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ptpython/layout.py b/ptpython/layout.py index 2c6395ce..d15e52e2 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -151,7 +151,7 @@ def goto_next(mouse_event: MouseEvent) -> None: append_category(category) for option in category.options: - append(i, option.title, "%s" % option.get_current_value()) + append(i, option.title, "%s" % (option.get_current_value(),)) i += 1 tokens.pop() # Remove last newline. diff --git a/ptpython/python_input.py b/ptpython/python_input.py index e8170f2b..da19076b 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -34,6 +34,12 @@ ThreadedCompleter, merge_completers, ) +from prompt_toolkit.cursor_shapes import ( + AnyCursorShapeConfig, + CursorShape, + DynamicCursorShapeConfig, + ModalCursorShapeConfig, +) from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import Condition @@ -325,6 +331,18 @@ def __init__( self.search_buffer: Buffer = Buffer() self.docstring_buffer: Buffer = Buffer(read_only=True) + # Cursor shapes. + self.cursor_shape_config = "Block" + self.all_cursor_shape_configs: Dict[str, AnyCursorShapeConfig] = { + "Block": CursorShape.BLOCK, + "Underline": CursorShape.UNDERLINE, + "Beam": CursorShape.BEAM, + "Modal (vi)": ModalCursorShapeConfig(), + "Blink block": CursorShape.BLINKING_BLOCK, + "Blink under": CursorShape.BLINKING_UNDERLINE, + "Blink beam": CursorShape.BLINKING_BEAM, + } + # Tokens to be shown at the prompt. self.prompt_style: str = "classic" # The currently active style. @@ -584,6 +602,16 @@ def get_values() -> dict[str, Callable[[], bool]]: "Vi": lambda: enable("vi_mode"), }, ), + Option( + title="Cursor shape", + description="Change the cursor style, possibly according " + "to the Vi input mode.", + get_current_value=lambda: self.cursor_shape_config, + get_values=lambda: dict( + (s, partial(enable, "cursor_shape_config", s)) + for s in self.all_cursor_shape_configs + ), + ), simple_option( title="Paste mode", description="When enabled, don't indent automatically.", @@ -896,6 +924,9 @@ def _create_application( style_transformation=self.style_transformation, include_default_pygments_style=False, reverse_vi_search_direction=True, + cursor=DynamicCursorShapeConfig( + lambda: self.all_cursor_shape_configs[self.cursor_shape_config] + ), input=input, output=output, ) From f8399dd5a13d4bb8e5cd98365f2435cdfaf628a8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 22 Feb 2023 10:52:04 +0000 Subject: [PATCH 19/83] Set minimum prompt_toolkit version to 3.0.28, because of cursor shape support. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c4087f9c..ce5be98d 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.18, because of the `in_thread` option. - "prompt_toolkit>=3.0.18,<3.1.0", + # Use prompt_toolkit 3.0.28, because of cursor shape support. + "prompt_toolkit>=3.0.28,<3.1.0", "pygments", ], python_requires=">=3.7", From 44f0c6e57d616d41de458daccbf36e8d8eb5fb3d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 22 Feb 2023 10:52:15 +0000 Subject: [PATCH 20/83] Release 3.0.23 --- CHANGELOG | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 916a5422..645ca60b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ CHANGELOG ========= +3.0.23: 2023-02-22 +------------------ + +Fixes: +- Don't print exception messages twice for unhandled exceptions. +- Added cursor shape support. + +Breaking changes: +- Drop Python 3.6 support. + + 3.0.22: 2022-12-06 ------------------ diff --git a/setup.py b/setup.py index ce5be98d..18d2911a 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.22", + version="3.0.23", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 91d2c3589310452a0f79f2fa1a4a6847fc095481 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 12 Apr 2023 15:07:11 +0000 Subject: [PATCH 21/83] Fix various typos. --- CHANGELOG | 2 +- ptpython/completer.py | 6 +++--- ptpython/python_input.py | 4 ++-- ptpython/utils.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 645ca60b..e753cfd9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -191,7 +191,7 @@ New features: - Optional pager for displaying outputs that don't fit on the screen. - Added --light-bg and --dark-bg flags to automatically optimize the brightness of the colors according to the terminal background. -- Addd `PTPYTHON_CONFIG_HOME` for explicitely setting the config directory. +- Add `PTPYTHON_CONFIG_HOME` for explicitly setting the config directory. - Show completion suffixes (like '(' for functions). Fixes: diff --git a/ptpython/completer.py b/ptpython/completer.py index f28d2b16..85a96d7c 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -259,7 +259,7 @@ def get_completions( # See: https://github.com/jonathanslenders/ptpython/issues/223 pass except Exception: - # Supress all other Jedi exceptions. + # Suppress all other Jedi exceptions. pass else: # Move function parameters to the top. @@ -367,7 +367,7 @@ def __init__( rf""" {expression} - # Dict loopup to complete (square bracket open + start of + # Dict lookup to complete (square bracket open + start of # string). \[ \s* ([^\[\]]*)$ @@ -380,7 +380,7 @@ def __init__( rf""" {expression} - # Attribute loopup to complete (dot + varname). + # Attribute lookup to complete (dot + varname). \. \s* ([a-zA-Z0-9_]*)$ """, diff --git a/ptpython/python_input.py b/ptpython/python_input.py index da19076b..0c7fef6f 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -379,7 +379,7 @@ def __init__( self.options = self._create_options() self.selected_option_index: int = 0 - #: Incremeting integer counting the current statement. + #: Incrementing integer counting the current statement. self.current_statement_index: int = 1 # Code signatures. (This is set asynchronously after a timeout.) @@ -835,7 +835,7 @@ def get_values() -> dict[str, Callable[[], bool]]: [ simple_option( title="Syntax highlighting", - description="Use colors for syntax highligthing", + description="Use colors for syntax highlighting", field_name="enable_syntax_highlighting", ), simple_option( diff --git a/ptpython/utils.py b/ptpython/utils.py index 53488997..d973d726 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -91,7 +91,7 @@ def get_jedi_script_from_document( # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514 return None except KeyError: - # Workaroud for a crash when the input is "u'", the start of a unicode string. + # Workaround for a crash when the input is "u'", the start of a unicode string. return None except Exception: # Workaround for: https://github.com/jonathanslenders/ptpython/issues/91 From 6c2d650649e5003d9ee01c01df508c11c6b28e9b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 3 Nov 2023 13:07:14 +0000 Subject: [PATCH 22/83] Use ruff for linting and formatting. - Removed unused typing imports. - Renamed ambiguous variable. - Fix dict literal usage. - Ruff formatting. - Removed unnecessary trailing commas. --- .github/workflows/test.yaml | 6 ++-- ptpython/completer.py | 2 +- ptpython/contrib/asyncssh_repl.py | 2 +- ptpython/entry_points/run_ptpython.py | 6 ++-- ptpython/history_browser.py | 3 +- ptpython/layout.py | 22 ++++++------- ptpython/lexer.py | 2 +- ptpython/python_input.py | 21 +++--------- ptpython/repl.py | 12 +++---- ptpython/signatures.py | 3 +- ptpython/style.py | 2 -- ptpython/utils.py | 12 +------ ptpython/validator.py | 2 +- pyproject.toml | 47 +++++++++++++++++++-------- setup.py | 10 +++--- 15 files changed, 75 insertions(+), 77 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 31837db3..9a50f3bc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,13 +22,13 @@ jobs: 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 . ruff mypy pytest readme_renderer pip list - name: Type Checker run: | mypy ptpython - isort -c --profile black ptpython examples setup.py - black --check ptpython examples setup.py + ruff . + ruff format --check . - name: Run Tests run: | ./tests/run_tests.py diff --git a/ptpython/completer.py b/ptpython/completer.py index 85a96d7c..91d66474 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -6,7 +6,7 @@ import keyword import re from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Callable, Iterable from prompt_toolkit.completion import ( CompleteEvent, diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 0347aded..051519de 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -9,7 +9,7 @@ from __future__ import annotations import asyncio -from typing import Any, Optional, TextIO, cast +from typing import Any, TextIO, cast import asyncssh from prompt_toolkit.data_structures import Size diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 1b4074d4..c0b4078b 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -28,7 +28,7 @@ import pathlib import sys from textwrap import dedent -from typing import IO, Optional, Tuple +from typing import IO import appdirs from prompt_toolkit.formatted_text import HTML @@ -72,12 +72,12 @@ def create_parser() -> _Parser: "--light-bg", action="store_true", help="Run on a light background (use dark colors for text).", - ), + ) parser.add_argument( "--dark-bg", action="store_true", help="Run on a dark background (use light colors for text).", - ), + ) parser.add_argument( "--config-file", type=str, help="Location of configuration file." ) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index eea81c2e..b667be12 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -7,7 +7,7 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, Callable, List, Optional, Set +from typing import TYPE_CHECKING, Callable from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app @@ -107,6 +107,7 @@ class BORDER: "Box drawing characters." + HORIZONTAL = "\u2501" VERTICAL = "\u2503" TOP_LEFT = "\u250f" diff --git a/ptpython/layout.py b/ptpython/layout.py index d15e52e2..2c1ec15f 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -7,7 +7,7 @@ import sys from enum import Enum from inspect import _ParameterKind as ParameterKind -from typing import TYPE_CHECKING, Any, List, Optional, Type +from typing import TYPE_CHECKING, Any from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER @@ -17,11 +17,7 @@ is_done, renderer_height_is_known, ) -from prompt_toolkit.formatted_text import ( - AnyFormattedText, - fragment_list_width, - to_formatted_text, -) +from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import ( @@ -60,7 +56,6 @@ SystemToolbar, ValidationToolbar, ) -from pygments.lexers import PythonLexer from .filters import HasSignature, ShowDocstring, ShowSidebar, ShowSignature from .prompt_style import PromptStyle @@ -74,6 +69,7 @@ class CompletionVisualisation(Enum): "Visualisation method for the completions." + NONE = "none" POP_UP = "pop-up" MULTI_COLUMN = "multi-column" @@ -151,7 +147,7 @@ def goto_next(mouse_event: MouseEvent) -> None: append_category(category) for option in category.options: - append(i, option.title, "%s" % (option.get_current_value(),)) + append(i, option.title, str(option.get_current_value())) i += 1 tokens.pop() # Remove last newline. @@ -302,13 +298,15 @@ def get_text_fragments() -> StyleAndTextTuples: content=Window( FormattedTextControl(get_text_fragments), height=Dimension.exact(1) ), - filter= # Show only when there is a signature - HasSignature(python_input) & + filter=HasSignature(python_input) + & # Signature needs to be shown. - ShowSignature(python_input) & + ShowSignature(python_input) + & # And no sidebar is visible. - ~ShowSidebar(python_input) & + ~ShowSidebar(python_input) + & # Not done yet. ~is_done, ) diff --git a/ptpython/lexer.py b/ptpython/lexer.py index 81924c9d..d925e95c 100644 --- a/ptpython/lexer.py +++ b/ptpython/lexer.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable, Optional +from typing import Callable from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import StyleAndTextTuples diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 0c7fef6f..211d36c9 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,18 +6,7 @@ from asyncio import get_event_loop from functools import partial -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Generic, - List, - Mapping, - Optional, - Tuple, - TypeVar, -) +from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar from prompt_toolkit.application import Application, get_app from prompt_toolkit.auto_suggest import ( @@ -333,7 +322,7 @@ def __init__( # Cursor shapes. self.cursor_shape_config = "Block" - self.all_cursor_shape_configs: Dict[str, AnyCursorShapeConfig] = { + self.all_cursor_shape_configs: dict[str, AnyCursorShapeConfig] = { "Block": CursorShape.BLOCK, "Underline": CursorShape.UNDERLINE, "Beam": CursorShape.BEAM, @@ -607,10 +596,10 @@ def get_values() -> dict[str, Callable[[], bool]]: description="Change the cursor style, possibly according " "to the Vi input mode.", get_current_value=lambda: self.cursor_shape_config, - get_values=lambda: dict( - (s, partial(enable, "cursor_shape_config", s)) + get_values=lambda: { + s: partial(enable, "cursor_shape_config", s) for s in self.all_cursor_shape_configs - ), + }, ), simple_option( title="Paste mode", diff --git a/ptpython/repl.py b/ptpython/repl.py index 02a5075d..3a74c3c3 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -18,7 +18,7 @@ import warnings from dis import COMPILER_FLAG_NAMES from enum import Enum -from typing import Any, Callable, ContextManager, Dict, Optional +from typing import Any, Callable, ContextManager from prompt_toolkit.formatted_text import ( HTML, @@ -547,12 +547,12 @@ def _format_exception_output(self, e: BaseException) -> PygmentsTokens: tblist = tblist[line_nr:] break - l = traceback.format_list(tblist) - if l: - l.insert(0, "Traceback (most recent call last):\n") - l.extend(traceback.format_exception_only(t, v)) + tb_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_str = "".join(l) + tb_str = "".join(tb_list) # Format exception and write to output. # (We use the default style. Most other styles result diff --git a/ptpython/signatures.py b/ptpython/signatures.py index 5a6f286a..d4cb98c2 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -10,7 +10,7 @@ import inspect from inspect import Signature as InspectSignature from inspect import _ParameterKind as ParameterKind -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Any, Sequence from prompt_toolkit.document import Document @@ -203,7 +203,6 @@ def get_signatures_using_eval( running `eval()` over the detected function name. """ # Look for open parenthesis, before cursor position. - text = document.text_before_cursor pos = document.cursor_position - 1 paren_mapping = {")": "(", "}": "{", "]": "["} diff --git a/ptpython/style.py b/ptpython/style.py index 199d5abf..c5a04e58 100644 --- a/ptpython/style.py +++ b/ptpython/style.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Dict - from prompt_toolkit.styles import BaseStyle, Style, merge_styles from prompt_toolkit.styles.pygments import style_from_pygments_cls from prompt_toolkit.utils import is_conemu_ansi, is_windows, is_windows_vt100_supported diff --git a/ptpython/utils.py b/ptpython/utils.py index d973d726..28887d20 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -4,17 +4,7 @@ from __future__ import annotations import re -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - Optional, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Any, Callable, Iterable, TypeVar, cast from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import to_formatted_text diff --git a/ptpython/validator.py b/ptpython/validator.py index 3b36d273..91b9c284 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable, Optional +from typing import Callable from prompt_toolkit.document import Document from prompt_toolkit.validation import ValidationError, Validator diff --git a/pyproject.toml b/pyproject.toml index b356239f..d9d839ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,34 @@ -[tool.black] -target-version = ['py36'] - - -[tool.isort] -# isort configuration that is compatible with Black. -multi_line_output = 3 -include_trailing_comma = true -known_first_party = "ptpython" -known_third_party = "prompt_toolkit,pygments,asyncssh" -force_grid_wrap = 0 -use_parentheses = true -line_length = 88 +[tool.ruff] +target-version = "py37" +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "C", # flake8-comprehensions + "T", # Print. + "I", # isort + # "B", # flake8-bugbear + "UP", # pyupgrade + "RUF100", # unused-noqa + "Q", # quotes +] +ignore = [ + "E501", # Line too long, handled by black + "C901", # Too complex + "E722", # bare except. +] + + +[tool.ruff.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. +"tests/run_tests.py" = ["F401"] # Unused imports. + + +[tool.ruff.isort] +known-first-party = ["ptpython"] +known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] diff --git a/setup.py b/setup.py index 18d2911a..ae9838ea 100644 --- a/setup.py +++ b/setup.py @@ -39,12 +39,14 @@ "ptpython = ptpython.entry_points.run_ptpython:run", "ptipython = ptpython.entry_points.run_ptipython:run", "ptpython%s = ptpython.entry_points.run_ptpython:run" % sys.version_info[0], - "ptpython%s.%s = ptpython.entry_points.run_ptpython:run" - % sys.version_info[:2], + "ptpython{}.{} = ptpython.entry_points.run_ptpython:run".format( + *sys.version_info[:2] + ), "ptipython%s = ptpython.entry_points.run_ptipython:run" % sys.version_info[0], - "ptipython%s.%s = ptpython.entry_points.run_ptipython:run" - % sys.version_info[:2], + "ptipython{}.{} = ptpython.entry_points.run_ptipython:run".format( + *sys.version_info[:2] + ), ] }, extras_require={ From 945426bfaa61ecc8c602983c2e2f023a9d17df22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tero=20Ykspet=C3=A4j=C3=A4?= Date: Tue, 12 Sep 2023 09:42:54 +0300 Subject: [PATCH 23/83] Add cursor_shape_config example Describe setting the cursor shape to modal for Vi mode. List other possible options for the setting. --- examples/ptpython_config/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 2f3f49dd..9e13879a 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -70,6 +70,9 @@ def configure(repl): # Vi mode. repl.vi_mode = False + # Enable the modal cursor (when using Vi mode). Other options are 'Block', 'Underline', 'Beam', 'Blink under', 'Blink block', and 'Blink beam' + repl.cursor_shape_config = 'Modal (vi)' + # Paste mode. (When True, don't insert whitespace after new line.) repl.paste_mode = False From 9ea323855977119fb5e49f428eadcf552b2260f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E7=A9=86=E7=A9=86?= Date: Wed, 6 Sep 2023 13:36:04 +0800 Subject: [PATCH 24/83] fix top-level await in ipython --- ptpython/ipython.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index fb4b5ed9..ae85a265 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -38,6 +38,7 @@ from .completer import PythonCompleter from .python_input import PythonInput +from .repl import PyCF_ALLOW_TOP_LEVEL_AWAIT from .style import default_ui_style from .validator import PythonValidator @@ -211,6 +212,12 @@ def __init__(self, ipython_shell, *a, **kw): self.ui_styles = {"default": Style.from_dict(style_dict)} self.use_ui_colorscheme("default") + def get_compiler_flags(self): + flags = super().get_compiler_flags() + if self.ipython_shell.autoawait: + flags |= PyCF_ALLOW_TOP_LEVEL_AWAIT + return flags + class InteractiveShellEmbed(_InteractiveShellEmbed): """ From 48c7b3885c786fcb3f5f75404cd4cc2caa20bfe9 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 22 Jul 2023 15:12:19 +0200 Subject: [PATCH 25/83] Fix IPython DeprecationWarning ptipython raises the following error since IPython 7: >>> IPython.core.inputsplitter is deprecated since IPython 7 in favor of `IPython.core.inputtransformer2` --- ptpython/ipython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptpython/ipython.py b/ptpython/ipython.py index ae85a265..ad0516a3 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -14,7 +14,7 @@ from warnings import warn from IPython import utils as ipy_utils -from IPython.core.inputsplitter import IPythonInputSplitter +from IPython.core.inputtransformer2 import TransformerManager from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed from IPython.terminal.ipapp import load_default_config from prompt_toolkit.completion import ( @@ -66,7 +66,7 @@ def out_prompt(self) -> AnyFormattedText: class IPythonValidator(PythonValidator): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.isp = IPythonInputSplitter() + self.isp = TransformerManager() def validate(self, document: Document) -> None: document = Document(text=self.isp.transform_cell(document.text)) From d25e67874ffc745a3a65a6776c1b9aa401dca076 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 08:01:04 -0500 Subject: [PATCH 26/83] Only interrupt run_config() for explicitly passed config_file --- ptpython/repl.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index 3a74c3c3..ce92c660 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -630,23 +630,28 @@ def enable_deprecation_warnings() -> None: warnings.filterwarnings("default", category=DeprecationWarning, module="__main__") -def run_config( - repl: PythonInput, config_file: str = "~/.config/ptpython/config.py" -) -> None: +DEFAULT_CONFIG_FILE = "~/.config/ptpython/config.py" + + +def run_config(repl: PythonInput, config_file: str | None = None) -> None: """ Execute REPL config file. :param repl: `PythonInput` instance. :param config_file: Path of the configuration file. """ + explicit_config_file = config_file is not None + # Expand tildes. - config_file = os.path.expanduser(config_file) + config_file = os.path.expanduser( + config_file if config_file is not None else DEFAULT_CONFIG_FILE + ) def enter_to_continue() -> None: input("\nPress ENTER to continue...") # Check whether this file exists. - if not os.path.exists(config_file): + if not os.path.exists(config_file) and explicit_config_file: print("Impossible to read %r" % config_file) enter_to_continue() return From dc2163383e3dcc54eb19795fe87c0162a578bbfb Mon Sep 17 00:00:00 2001 From: Ryan Delaney Date: Fri, 10 Mar 2023 17:01:23 -0500 Subject: [PATCH 27/83] Add BSD License classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ae9838ea..ad26545a 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ ], python_requires=">=3.7", classifiers=[ + "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7", From 03b279ecd6a0ec670cba144f5f680a7a78fc2fc7 Mon Sep 17 00:00:00 2001 From: Ryan Delaney Date: Fri, 10 Mar 2023 17:01:38 -0500 Subject: [PATCH 28/83] Update copyright dates --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 910b80a7..89a51144 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015, Jonathan Slenders +Copyright (c) 2015-2023, Jonathan Slenders All rights reserved. Redistribution and use in source and binary forms, with or without modification, From 46b1076cea63f7d0642b2e820d7fbcbff89336a1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Fri, 3 Nov 2023 13:45:22 +0000 Subject: [PATCH 29/83] Fix code formatting. --- examples/ptpython_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 9e13879a..b25850a2 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -71,7 +71,7 @@ def configure(repl): repl.vi_mode = False # Enable the modal cursor (when using Vi mode). Other options are 'Block', 'Underline', 'Beam', 'Blink under', 'Blink block', and 'Blink beam' - repl.cursor_shape_config = 'Modal (vi)' + repl.cursor_shape_config = "Modal (vi)" # Paste mode. (When True, don't insert whitespace after new line.) repl.paste_mode = False From 655b354a83aa56c423ba7ecebe4df88928c99526 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 10:43:26 +0000 Subject: [PATCH 30/83] Refactor output printer so that it can render big outputs without memory issues. Previously, an expression like `b'\x90' * 40_000_000` would kill ptpython because it rendered the whole output at once. This implementation streams the rendering logic while it's paginating. --- ptpython/printer.py | 435 ++++++++++++++++++++++++++++++++++++++++++++ ptpython/repl.py | 400 +++++----------------------------------- pyproject.toml | 1 + 3 files changed, 478 insertions(+), 358 deletions(-) create mode 100644 ptpython/printer.py diff --git a/ptpython/printer.py b/ptpython/printer.py new file mode 100644 index 00000000..3618934e --- /dev/null +++ b/ptpython/printer.py @@ -0,0 +1,435 @@ +from __future__ import annotations + +import sys +import traceback +from dataclasses import dataclass +from enum import Enum +from typing import Generator, Iterable + +from prompt_toolkit.formatted_text import ( + HTML, + AnyFormattedText, + FormattedText, + OneStyleAndTextTuple, + StyleAndTextTuples, + fragment_list_width, + merge_formatted_text, + to_formatted_text, +) +from prompt_toolkit.formatted_text.utils import split_lines +from prompt_toolkit.input import Input +from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent +from prompt_toolkit.output import Output +from prompt_toolkit.shortcuts import PromptSession, print_formatted_text +from prompt_toolkit.styles import BaseStyle, StyleTransformation +from prompt_toolkit.styles.pygments import pygments_token_to_classname +from prompt_toolkit.utils import get_cwidth +from pygments.lexers import PythonLexer, PythonTracebackLexer + +__all__ = ["OutputPrinter"] + +# Never reformat results larger than this: +MAX_REFORMAT_SIZE = 1_000_000 + + +@dataclass +class OutputPrinter: + """ + Result printer. + + Usage:: + + printer = OutputPrinter(...) + printer.display_result(...) + printer.display_exception(...) + """ + + output: Output + input: Input + style: BaseStyle + title: AnyFormattedText + style_transformation: StyleTransformation + + def display_result( + self, + result: object, + *, + out_prompt: AnyFormattedText, + reformat: bool, + highlight: bool, + paginate: bool, + ) -> None: + """ + Show __repr__ (or `__pt_repr__`) for an `eval` result and print to output. + + :param reformat: Reformat result using 'black' before printing if the + result is parsable as Python code. + :param highlight: Syntax highlight the result. + :param paginate: Show paginator when the result does not fit on the + screen. + """ + out_prompt = to_formatted_text(out_prompt) + out_prompt_width = fragment_list_width(out_prompt) + + result = self._insert_out_prompt_and_split_lines( + self._format_result_output( + result, + reformat=reformat, + highlight=highlight, + line_length=self.output.get_size().columns - out_prompt_width, + paginate=paginate, + ), + out_prompt=out_prompt, + ) + self._display_result(result, paginate=paginate) + + def display_exception( + self, e: BaseException, *, highlight: bool, paginate: bool + ) -> None: + """ + Render an exception. + """ + result = self._insert_out_prompt_and_split_lines( + self._format_exception_output(e, highlight=highlight), + out_prompt="", + ) + self._display_result(result, paginate=paginate) + + def display_style_and_text_tuples( + self, + result: Iterable[OneStyleAndTextTuple], + *, + paginate: bool, + ) -> None: + self._display_result( + self._insert_out_prompt_and_split_lines(result, out_prompt=""), + paginate=paginate, + ) + + def _display_result( + self, + lines: Iterable[StyleAndTextTuples], + *, + paginate: bool, + ) -> None: + if paginate: + self._print_paginated_formatted_text(lines) + else: + for line in lines: + self._print_formatted_text(line) + + self.output.flush() + + def _print_formatted_text(self, line: StyleAndTextTuples, end: str = "\n") -> None: + print_formatted_text( + FormattedText(line), + style=self.style, + style_transformation=self.style_transformation, + include_default_pygments_style=False, + output=self.output, + end=end, + ) + + def _format_result_output( + self, + result: object, + *, + reformat: bool, + highlight: bool, + line_length: int, + paginate: bool, + ) -> Generator[OneStyleAndTextTuple, None, None]: + """ + Format __repr__ for an `eval` result. + + Note: this can raise `KeyboardInterrupt` if either calling `__repr__`, + `__pt_repr__` or formatting the output with "Black" takes to long + and the user presses Control-C. + """ + # If __pt_repr__ is present, take this. This can return prompt_toolkit + # formatted text. + try: + if hasattr(result, "__pt_repr__"): + formatted_result_repr = to_formatted_text( + getattr(result, "__pt_repr__")() + ) + yield from formatted_result_repr + return + except KeyboardInterrupt: + raise # Don't catch here. + except: + # For bad code, `__getattr__` can raise something that's not an + # `AttributeError`. This happens already when calling `hasattr()`. + pass + + # Call `__repr__` of given object first, to turn it in a string. + try: + result_repr = repr(result) + except KeyboardInterrupt: + raise # Don't catch here. + except BaseException as e: + # Calling repr failed. + self.display_exception(e, highlight=highlight, paginate=paginate) + return + + # Determine whether it's valid Python code. If not, + # reformatting/highlighting won't be applied. + if len(result_repr) < MAX_REFORMAT_SIZE: + try: + compile(result_repr, "", "eval") + except SyntaxError: + valid_python = False + else: + valid_python = True + else: + valid_python = False + + if valid_python and reformat: + # Inline import. Slightly speed up start-up time if black is + # not used. + try: + import black + + if not hasattr(black, "Mode"): + raise ImportError + except ImportError: + pass # no Black package in your installation + else: + result_repr = black.format_str( + result_repr, + mode=black.Mode(line_length=line_length), + ) + + if valid_python and highlight: + yield from _lex_python_result(result_repr) + else: + yield ("", result_repr) + + def _insert_out_prompt_and_split_lines( + self, result: Iterable[OneStyleAndTextTuple], out_prompt: AnyFormattedText + ) -> Iterable[StyleAndTextTuples]: + r""" + Split styled result in lines (based on the \n characters in the result) + an insert output prompt on whitespace in front of each line. (This does + not yet do the soft wrapping.) + + Yield lines as a result. + """ + out_prompt = to_formatted_text(out_prompt) + out_prompt_width = fragment_list_width(out_prompt) + prefix = ("", " " * out_prompt_width) + + for i, line in enumerate(split_lines(result)): + if i == 0: + line = [*out_prompt, *line] + else: + line = [prefix, *line] + yield line + + def _apply_soft_wrapping( + self, lines: Iterable[StyleAndTextTuples] + ) -> Iterable[StyleAndTextTuples]: + """ + Apply soft wrapping to the given lines. Wrap according to the terminal + width. Insert whitespace in front of each wrapped line to align it with + the output prompt. + """ + line_length = self.output.get_size().columns + + # Iterate over hard wrapped lines. + for lineno, line in enumerate(lines): + columns_in_buffer = 0 + current_line: list[OneStyleAndTextTuple] = [] + + for style, text, *_ in line: + for c in text: + width = get_cwidth(c) + + # (Soft) wrap line if it doesn't fit. + if columns_in_buffer + width > line_length: + yield current_line + columns_in_buffer = 0 + current_line = [] + + columns_in_buffer += width + current_line.append((style, c)) + + if len(current_line) > 0: + yield current_line + + def _print_paginated_formatted_text( + self, lines: Iterable[StyleAndTextTuples] + ) -> None: + """ + Print formatted text, using --MORE-- style pagination. + (Avoid filling up the terminal's scrollback buffer.) + """ + lines = self._apply_soft_wrapping(lines) + pager_prompt = create_pager_prompt( + self.style, self.title, output=self.output, input=self.input + ) + + abort = False + print_all = False + + # Max number of lines allowed in the buffer before painting. + size = self.output.get_size() + max_rows = size.rows - 1 + + # Page buffer. + page: StyleAndTextTuples = [] + + def show_pager() -> None: + nonlocal abort, max_rows, print_all + + # Run pager prompt in another thread. + # Same as for the input. This prevents issues with nested event + # loops. + pager_result = pager_prompt.prompt(in_thread=True) + + if pager_result == PagerResult.ABORT: + print("...") + abort = True + + elif pager_result == PagerResult.NEXT_LINE: + max_rows = 1 + + elif pager_result == PagerResult.NEXT_PAGE: + max_rows = size.rows - 1 + + elif pager_result == PagerResult.PRINT_ALL: + print_all = True + + # Loop over lines. Show --MORE-- prompt when page is filled. + rows = 0 + + for lineno, line in enumerate(lines): + page.extend(line) + page.append(("", "\n")) + rows += 1 + + if rows >= max_rows: + self._print_formatted_text(page, end="") + page = [] + rows = 0 + + if not print_all: + show_pager() + if abort: + return + + self._print_formatted_text(page) + + 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)) + + for line_nr, tb_tuple in enumerate(tblist): + if tb_tuple[0] == "": + tblist = tblist[line_nr:] + break + + 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_str = "".join(tb_list) + + # Format exception and write to output. + # (We use the default style. Most other styles result + # in unreadable colors for the traceback.) + if highlight: + for index, tokentype, text in PythonTracebackLexer().get_tokens_unprocessed( + tb_str + ): + yield ("class:" + pygments_token_to_classname(tokentype), text) + else: + yield ("", tb_str) + + +class PagerResult(Enum): + ABORT = "ABORT" + NEXT_LINE = "NEXT_LINE" + NEXT_PAGE = "NEXT_PAGE" + PRINT_ALL = "PRINT_ALL" + + +def create_pager_prompt( + style: BaseStyle, + title: AnyFormattedText = "", + input: Input | None = None, + output: Output | None = None, +) -> PromptSession[PagerResult]: + """ + Create a "--MORE--" prompt for paginated output. + """ + bindings = KeyBindings() + + @bindings.add("enter") + @bindings.add("down") + def next_line(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.NEXT_LINE) + + @bindings.add("space") + def next_page(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.NEXT_PAGE) + + @bindings.add("a") + def print_all(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.PRINT_ALL) + + @bindings.add("q") + @bindings.add("c-c") + @bindings.add("c-d") + @bindings.add("escape", eager=True) + def no(event: KeyPressEvent) -> None: + event.app.exit(result=PagerResult.ABORT) + + @bindings.add("") + def _(event: KeyPressEvent) -> None: + "Disallow inserting other text." + pass + + session: PromptSession[PagerResult] = PromptSession( + merge_formatted_text( + [ + title, + HTML( + "" + " -- MORE -- " + "[Enter] Scroll " + "[Space] Next page " + "[a] Print all " + "[q] Quit " + ": " + ), + ] + ), + key_bindings=bindings, + erase_when_done=True, + style=style, + input=input, + output=output, + ) + return session + + +def _lex_python_result(result: str) -> Generator[tuple[str, str], None, None]: + "Return token list for Python string." + lexer = PythonLexer() + # Use `get_tokens_unprocessed`, so that we get exactly the same string, + # without line endings appended. `print_formatted_text` already appends a + # line ending, and otherwise we'll have two line endings. + tokens = lexer.get_tokens_unprocessed(result) + + for index, tokentype, text in tokens: + yield ("class:" + pygments_token_to_classname(tokentype), text) diff --git a/ptpython/repl.py b/ptpython/repl.py index ce92c660..98b01afa 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -17,33 +17,18 @@ import types import warnings from dis import COMPILER_FLAG_NAMES -from enum import Enum -from typing import Any, Callable, ContextManager - -from prompt_toolkit.formatted_text import ( - HTML, - AnyFormattedText, - FormattedText, - PygmentsTokens, - StyleAndTextTuples, - fragment_list_width, - merge_formatted_text, - to_formatted_text, -) -from prompt_toolkit.formatted_text.utils import fragment_list_to_text, split_lines -from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent +from typing import Any, Callable, ContextManager, Iterable + +from prompt_toolkit.formatted_text import OneStyleAndTextTuple from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context from prompt_toolkit.shortcuts import ( - PromptSession, clear_title, - print_formatted_text, set_title, ) -from prompt_toolkit.styles import BaseStyle -from prompt_toolkit.utils import DummyContext, get_cwidth -from pygments.lexers import PythonLexer, PythonTracebackLexer -from pygments.token import Token +from prompt_toolkit.utils import DummyContext +from pygments.lexers import PythonTracebackLexer # noqa: F401 +from .printer import OutputPrinter from .python_input import PythonInput PyCF_ALLOW_TOP_LEVEL_AWAIT: int @@ -108,7 +93,9 @@ def run_and_show_expression(self, expression: str) -> None: else: # Print. if result is not None: - self.show_result(result) + self._show_result(result) + if self.insert_blank_line_after_output: + self.app.output.write("\n") # Loop. self.current_statement_index += 1 @@ -123,6 +110,24 @@ def run_and_show_expression(self, expression: str) -> None: # any case.) self._handle_keyboard_interrupt(e) + def _get_output_printer(self) -> OutputPrinter: + return OutputPrinter( + output=self.app.output, + input=self.app.input, + style=self._current_style, + style_transformation=self.style_transformation, + title=self.title, + ) + + def _show_result(self, result: object) -> None: + self._get_output_printer().display_result( + result=result, + out_prompt=self.get_output_prompt(), + reformat=self.enable_output_formatting, + highlight=self.enable_syntax_highlighting, + paginate=self.enable_pager, + ) + def run(self) -> None: """ Run the REPL loop. @@ -167,7 +172,7 @@ async def run_and_show_expression_async(self, text: str): else: # Print. if result is not None: - await loop.run_in_executor(None, lambda: self.show_result(result)) + await loop.run_in_executor(None, lambda: self._show_result(result)) # Loop. self.current_statement_index += 1 @@ -318,264 +323,12 @@ def _compile_with_flags(self, code: str, mode: str): dont_inherit=True, ) - def _format_result_output(self, result: object) -> StyleAndTextTuples: - """ - Format __repr__ for an `eval` result. - - Note: this can raise `KeyboardInterrupt` if either calling `__repr__`, - `__pt_repr__` or formatting the output with "Black" takes to long - and the user presses Control-C. - """ - out_prompt = to_formatted_text(self.get_output_prompt()) - - # If the repr is valid Python code, use the Pygments lexer. - try: - result_repr = repr(result) - except KeyboardInterrupt: - raise # Don't catch here. - except BaseException as e: - # Calling repr failed. - self._handle_exception(e) - return [] - - try: - compile(result_repr, "", "eval") - except SyntaxError: - formatted_result_repr = to_formatted_text(result_repr) - else: - # Syntactically correct. Format with black and syntax highlight. - if self.enable_output_formatting: - # Inline import. Slightly speed up start-up time if black is - # not used. - try: - import black - - if not hasattr(black, "Mode"): - raise ImportError - except ImportError: - pass # no Black package in your installation - else: - result_repr = black.format_str( - result_repr, - mode=black.Mode(line_length=self.app.output.get_size().columns), - ) - - formatted_result_repr = to_formatted_text( - PygmentsTokens(list(_lex_python_result(result_repr))) - ) - - # If __pt_repr__ is present, take this. This can return prompt_toolkit - # formatted text. - try: - if hasattr(result, "__pt_repr__"): - formatted_result_repr = to_formatted_text( - getattr(result, "__pt_repr__")() - ) - if isinstance(formatted_result_repr, list): - formatted_result_repr = FormattedText(formatted_result_repr) - except KeyboardInterrupt: - raise # Don't catch here. - except: - # For bad code, `__getattr__` can raise something that's not an - # `AttributeError`. This happens already when calling `hasattr()`. - pass - - # Align every line to the prompt. - line_sep = "\n" + " " * fragment_list_width(out_prompt) - indented_repr: StyleAndTextTuples = [] - - lines = list(split_lines(formatted_result_repr)) - - for i, fragment in enumerate(lines): - indented_repr.extend(fragment) - - # Add indentation separator between lines, not after the last line. - if i != len(lines) - 1: - indented_repr.append(("", line_sep)) - - # Write output tokens. - if self.enable_syntax_highlighting: - formatted_output = merge_formatted_text([out_prompt, indented_repr]) - else: - formatted_output = FormattedText( - out_prompt + [("", fragment_list_to_text(formatted_result_repr))] - ) - - return to_formatted_text(formatted_output) - - def show_result(self, result: object) -> None: - """ - Show __repr__ for an `eval` result and print to output. - """ - formatted_text_output = self._format_result_output(result) - - if self.enable_pager: - self.print_paginated_formatted_text(formatted_text_output) - else: - self.print_formatted_text(formatted_text_output) - - self.app.output.flush() - - if self.insert_blank_line_after_output: - self.app.output.write("\n") - - def print_formatted_text( - self, formatted_text: StyleAndTextTuples, end: str = "\n" - ) -> None: - print_formatted_text( - FormattedText(formatted_text), - style=self._current_style, - style_transformation=self.style_transformation, - include_default_pygments_style=False, - output=self.app.output, - end=end, - ) - - def print_paginated_formatted_text( - self, - formatted_text: StyleAndTextTuples, - end: str = "\n", - ) -> None: - """ - Print formatted text, using --MORE-- style pagination. - (Avoid filling up the terminal's scrollback buffer.) - """ - pager_prompt = self.create_pager_prompt() - size = self.app.output.get_size() - - abort = False - print_all = False - - # Max number of lines allowed in the buffer before painting. - max_rows = size.rows - 1 - - # Page buffer. - rows_in_buffer = 0 - columns_in_buffer = 0 - page: StyleAndTextTuples = [] - - def flush_page() -> None: - nonlocal page, columns_in_buffer, rows_in_buffer - self.print_formatted_text(page, end="") - page = [] - columns_in_buffer = 0 - rows_in_buffer = 0 - - def show_pager() -> None: - nonlocal abort, max_rows, print_all - - # Run pager prompt in another thread. - # Same as for the input. This prevents issues with nested event - # loops. - pager_result = pager_prompt.prompt(in_thread=True) - - if pager_result == PagerResult.ABORT: - print("...") - abort = True - - elif pager_result == PagerResult.NEXT_LINE: - max_rows = 1 - - elif pager_result == PagerResult.NEXT_PAGE: - max_rows = size.rows - 1 - - elif pager_result == PagerResult.PRINT_ALL: - print_all = True - - # Loop over lines. Show --MORE-- prompt when page is filled. - - formatted_text = formatted_text + [("", end)] - lines = list(split_lines(formatted_text)) - - for lineno, line in enumerate(lines): - for style, text, *_ in line: - for c in text: - width = get_cwidth(c) - - # (Soft) wrap line if it doesn't fit. - if columns_in_buffer + width > size.columns: - # Show pager first if we get too many lines after - # wrapping. - if rows_in_buffer + 1 >= max_rows and not print_all: - page.append(("", "\n")) - flush_page() - show_pager() - if abort: - return - - rows_in_buffer += 1 - columns_in_buffer = 0 - - columns_in_buffer += width - page.append((style, c)) - - if rows_in_buffer + 1 >= max_rows and not print_all: - page.append(("", "\n")) - flush_page() - show_pager() - if abort: - return - else: - # Add line ending between lines (if `end="\n"` was given, one - # more empty line is added in `split_lines` automatically to - # take care of the final line ending). - if lineno != len(lines) - 1: - page.append(("", "\n")) - rows_in_buffer += 1 - columns_in_buffer = 0 - - flush_page() - - def create_pager_prompt(self) -> PromptSession[PagerResult]: - """ - Create pager --MORE-- prompt. - """ - return create_pager_prompt(self._current_style, self.title) - - def _format_exception_output(self, e: BaseException) -> PygmentsTokens: - # Instead of just calling ``traceback.format_exc``, we take the - # traceback and skip the bottom calls of this framework. - t, v, tb = sys.exc_info() - - # Required for pdb.post_mortem() to work. - sys.last_type, sys.last_value, sys.last_traceback = t, v, tb - - tblist = list(traceback.extract_tb(tb)) - - for line_nr, tb_tuple in enumerate(tblist): - if tb_tuple[0] == "": - tblist = tblist[line_nr:] - break - - 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_str = "".join(tb_list) - - # Format exception and write to output. - # (We use the default style. Most other styles result - # in unreadable colors for the traceback.) - if self.enable_syntax_highlighting: - tokens = list(_lex_python_traceback(tb_str)) - else: - tokens = [(Token, tb_str)] - return PygmentsTokens(tokens) - def _handle_exception(self, e: BaseException) -> None: - output = self.app.output - - tokens = self._format_exception_output(e) - - print_formatted_text( - tokens, - style=self._current_style, - style_transformation=self.style_transformation, - include_default_pygments_style=False, - output=output, + self._get_output_printer().display_exception( + e, + highlight=self.enable_syntax_highlighting, + paginate=self.enable_pager, ) - output.flush() def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None: output = self.app.output @@ -602,21 +355,16 @@ def _remove_from_namespace(self) -> None: globals = self.get_globals() del globals["get_ptpython"] - -def _lex_python_traceback(tb): - "Return token list for traceback string." - lexer = PythonTracebackLexer() - return lexer.get_tokens(tb) - - -def _lex_python_result(tb): - "Return token list for Python string." - lexer = PythonLexer() - # Use `get_tokens_unprocessed`, so that we get exactly the same string, - # without line endings appended. `print_formatted_text` already appends a - # line ending, and otherwise we'll have two line endings. - tokens = lexer.get_tokens_unprocessed(tb) - return [(tokentype, value) for index, tokentype, value in tokens] + def print_paginated_formatted_text( + self, + formatted_text: Iterable[OneStyleAndTextTuple], + end: str = "\n", + ) -> None: + # Warning: This is mainly here backwards-compatibility. Some projects + # call `print_paginated_formatted_text` on the Repl object. + self._get_output_printer().display_style_and_text_tuples( + formatted_text, paginate=True + ) def enable_deprecation_warnings() -> None: @@ -746,67 +494,3 @@ async def coroutine() -> None: else: with patch_context: repl.run() - - -class PagerResult(Enum): - ABORT = "ABORT" - NEXT_LINE = "NEXT_LINE" - NEXT_PAGE = "NEXT_PAGE" - PRINT_ALL = "PRINT_ALL" - - -def create_pager_prompt( - style: BaseStyle, title: AnyFormattedText = "" -) -> PromptSession[PagerResult]: - """ - Create a "continue" prompt for paginated output. - """ - bindings = KeyBindings() - - @bindings.add("enter") - @bindings.add("down") - def next_line(event: KeyPressEvent) -> None: - event.app.exit(result=PagerResult.NEXT_LINE) - - @bindings.add("space") - def next_page(event: KeyPressEvent) -> None: - event.app.exit(result=PagerResult.NEXT_PAGE) - - @bindings.add("a") - def print_all(event: KeyPressEvent) -> None: - event.app.exit(result=PagerResult.PRINT_ALL) - - @bindings.add("q") - @bindings.add("c-c") - @bindings.add("c-d") - @bindings.add("escape", eager=True) - def no(event: KeyPressEvent) -> None: - event.app.exit(result=PagerResult.ABORT) - - @bindings.add("") - def _(event: KeyPressEvent) -> None: - "Disallow inserting other text." - pass - - style - - session: PromptSession[PagerResult] = PromptSession( - merge_formatted_text( - [ - title, - HTML( - "" - " -- MORE -- " - "[Enter] Scroll " - "[Space] Next page " - "[a] Print all " - "[q] Quit " - ": " - ), - ] - ), - key_bindings=bindings, - erase_when_done=True, - style=style, - ) - return session diff --git a/pyproject.toml b/pyproject.toml index d9d839ed..5421c454 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ ignore = [ "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. From 6801f94006951e5c06f232862e40fa19cd58aa82 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 20:27:49 +0000 Subject: [PATCH 31/83] Fix type annotations in various places. --- examples/asyncio-python-embed.py | 15 +++--- examples/asyncio-ssh-python-embed.py | 18 +++---- examples/python-embed-with-custom-prompt.py | 12 ++--- examples/python-embed.py | 2 +- examples/ssh-and-telnet-embed.py | 11 ++-- ptpython/contrib/asyncssh_repl.py | 26 +++++---- ptpython/python_input.py | 58 ++++++++++++++------- ptpython/repl.py | 2 +- 8 files changed, 80 insertions(+), 64 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index 05f52f1d..a8fbba5a 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -19,7 +19,7 @@ counter = [0] -async def print_counter(): +async def print_counter() -> None: """ Coroutine that prints counters and saves it in a global variable. """ @@ -29,7 +29,7 @@ async def print_counter(): await asyncio.sleep(3) -async def interactive_shell(): +async def interactive_shell() -> None: """ Coroutine that starts a Python REPL from which we can access the global counter variable. @@ -44,13 +44,10 @@ async def interactive_shell(): loop.stop() -def main(): - asyncio.ensure_future(print_counter()) - asyncio.ensure_future(interactive_shell()) - - loop.run_forever() - loop.close() +async def main() -> None: + asyncio.create_task(print_counter()) + await interactive_shell() if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index 86b56073..be0689e7 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -32,31 +32,25 @@ def session_requested(self): return ReplSSHServerSession(self.get_namespace) -def main(port=8222): +async def main(port: int = 8222) -> None: """ Example that starts the REPL through an SSH server. """ - loop = asyncio.get_event_loop() - # Namespace exposed in the REPL. environ = {"hello": "world"} # Start SSH server. - def create_server(): + def create_server() -> MySSHServer: return MySSHServer(lambda: environ) print("Listening on :%i" % port) print('To connect, do "ssh localhost -p %i"' % port) - loop.run_until_complete( - asyncssh.create_server( - create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"] - ) + await asyncssh.create_server( + create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"] ) - - # Run eventloop. - loop.run_forever() + await asyncio.Future() # Wait forever. if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index 968aedc5..d54da1da 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -2,26 +2,26 @@ """ Example of embedding a Python REPL, and setting a custom prompt. """ -from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.formatted_text import HTML, AnyFormattedText from ptpython.prompt_style import PromptStyle from ptpython.repl import embed -def configure(repl): +def configure(repl) -> None: # Probably, the best is to add a new PromptStyle to `all_prompt_styles` and # activate it. This way, the other styles are still selectable from the # menu. class CustomPrompt(PromptStyle): - def in_prompt(self): + def in_prompt(self) -> AnyFormattedText: return HTML("Input[%s]: ") % ( repl.current_statement_index, ) - def in2_prompt(self, width): + def in2_prompt(self, width: int) -> AnyFormattedText: return "...: ".rjust(width) - def out_prompt(self): + def out_prompt(self) -> AnyFormattedText: return HTML("Result[%s]: ") % ( repl.current_statement_index, ) @@ -30,7 +30,7 @@ def out_prompt(self): repl.prompt_style = "custom" -def main(): +def main() -> None: embed(globals(), locals(), configure=configure) diff --git a/examples/python-embed.py b/examples/python-embed.py index ac2cd06f..49224ac2 100755 --- a/examples/python-embed.py +++ b/examples/python-embed.py @@ -4,7 +4,7 @@ from ptpython.repl import embed -def main(): +def main() -> None: embed(globals(), locals(), vi_mode=False) diff --git a/examples/ssh-and-telnet-embed.py b/examples/ssh-and-telnet-embed.py index 378784ce..62fa76d9 100755 --- a/examples/ssh-and-telnet-embed.py +++ b/examples/ssh-and-telnet-embed.py @@ -11,13 +11,16 @@ import asyncssh from prompt_toolkit import print_formatted_text -from prompt_toolkit.contrib.ssh.server import PromptToolkitSSHServer +from prompt_toolkit.contrib.ssh.server import ( + PromptToolkitSSHServer, + PromptToolkitSSHSession, +) from prompt_toolkit.contrib.telnet.server import TelnetServer from ptpython.repl import embed -def ensure_key(filename="ssh_host_key"): +def ensure_key(filename: str = "ssh_host_key") -> str: path = pathlib.Path(filename) if not path.exists(): rsa_key = asyncssh.generate_private_key("ssh-rsa") @@ -25,12 +28,12 @@ def ensure_key(filename="ssh_host_key"): return str(path) -async def interact(connection=None): +async def interact(connection: PromptToolkitSSHSession) -> None: global_dict = {**globals(), "print": print_formatted_text} await embed(return_asyncio_coroutine=True, globals=global_dict) -async def main(ssh_port=8022, telnet_port=8023): +async def main(ssh_port: int = 8022, telnet_port: int = 8023) -> None: ssh_server = PromptToolkitSSHServer(interact=interact) await asyncssh.create_server( lambda: ssh_server, "", ssh_port, server_host_keys=[ensure_key()] diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 051519de..35da7426 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -9,20 +9,20 @@ from __future__ import annotations import asyncio -from typing import Any, TextIO, cast +from typing import Any, AnyStr, TextIO, cast import asyncssh from prompt_toolkit.data_structures import Size from prompt_toolkit.input import create_pipe_input from prompt_toolkit.output.vt100 import Vt100_Output -from ptpython.python_input import _GetNamespace +from ptpython.python_input import _GetNamespace, _Namespace from ptpython.repl import PythonRepl __all__ = ["ReplSSHServerSession"] -class ReplSSHServerSession(asyncssh.SSHServerSession): +class ReplSSHServerSession(asyncssh.SSHServerSession[str]): """ SSH server session that runs a Python REPL. @@ -35,7 +35,7 @@ def __init__( ) -> None: self._chan: Any = None - def _globals() -> dict: + def _globals() -> _Namespace: data = get_globals() data.setdefault("print", self._print) return data @@ -79,7 +79,7 @@ def _get_size(self) -> Size: width, height, pixwidth, pixheight = self._chan.get_terminal_size() return Size(rows=height, columns=width) - def connection_made(self, chan): + def connection_made(self, chan: Any) -> None: """ Client connected, run repl in coroutine. """ @@ -89,7 +89,7 @@ def connection_made(self, chan): f = asyncio.ensure_future(self.repl.run_async()) # Close channel when done. - def done(_) -> None: + def done(_: object) -> None: chan.close() self._chan = None @@ -98,24 +98,28 @@ def done(_) -> None: def shell_requested(self) -> bool: return True - def terminal_size_changed(self, width, height, pixwidth, pixheight): + def terminal_size_changed( + self, width: int, height: int, pixwidth: int, pixheight: int + ) -> None: """ When the terminal size changes, report back to CLI. """ self.repl.app._on_resize() - def data_received(self, data, datatype): + def data_received(self, data: AnyStr, datatype: int | None) -> None: """ When data is received, send to inputstream of the CLI and repaint. """ self._input_pipe.send(data) - def _print(self, *data, sep=" ", end="\n", file=None) -> None: + def _print( + self, *data: object, sep: str = " ", end: str = "\n", file: Any = None + ) -> None: """ Alternative 'print' function that prints back into the SSH channel. """ # Pop keyword-only arguments. (We cannot use the syntax from the # signature. Otherwise, Python2 will give a syntax error message when # installing.) - data = sep.join(map(str, data)) - self._chan.write(data + end) + data_as_str = sep.join(map(str, data)) + self._chan.write(data_as_str + end) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 211d36c9..14995db4 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,7 +6,7 @@ from asyncio import get_event_loop from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar, Union from prompt_toolkit.application import Application, get_app from prompt_toolkit.auto_suggest import ( @@ -31,7 +31,7 @@ ) from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode -from prompt_toolkit.filters import Condition +from prompt_toolkit.filters import Condition, FilterOrBool from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.history import ( FileHistory, @@ -49,8 +49,13 @@ from prompt_toolkit.key_binding.bindings.open_in_editor import ( load_open_in_editor_bindings, ) +from prompt_toolkit.key_binding.key_bindings import Binding, KeyHandlerCallable +from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import AnyContainer +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.layout.processors import Processor from prompt_toolkit.lexers import DynamicLexer, Lexer, SimpleLexer from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.styles import ( @@ -91,22 +96,23 @@ from typing_extensions import Protocol class _SupportsLessThan(Protocol): - # Taken from typeshed. _T is used by "sorted", which needs anything + # Taken from typeshed. _T_lt is used by "sorted", which needs anything # sortable. def __lt__(self, __other: Any) -> bool: ... -_T = TypeVar("_T", bound="_SupportsLessThan") +_T_lt = TypeVar("_T_lt", bound="_SupportsLessThan") +_T_kh = TypeVar("_T_kh", bound=Union[KeyHandlerCallable, Binding]) -class OptionCategory(Generic[_T]): - def __init__(self, title: str, options: list[Option[_T]]) -> None: +class OptionCategory(Generic[_T_lt]): + def __init__(self, title: str, options: list[Option[_T_lt]]) -> None: self.title = title self.options = options -class Option(Generic[_T]): +class Option(Generic[_T_lt]): """ Ptpython configuration option that can be shown and modified from the sidebar. @@ -122,10 +128,10 @@ def __init__( self, title: str, description: str, - get_current_value: Callable[[], _T], + get_current_value: Callable[[], _T_lt], # 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[[], Mapping[_T, Callable[[], object]]], + get_values: Callable[[], Mapping[_T_lt, Callable[[], object]]], ) -> None: self.title = title self.description = description @@ -133,7 +139,7 @@ def __init__( self.get_values = get_values @property - def values(self) -> Mapping[_T, Callable[[], object]]: + def values(self) -> Mapping[_T_lt, Callable[[], object]]: return self.get_values() def activate_next(self, _previous: bool = False) -> None: @@ -208,10 +214,10 @@ def __init__( _completer: Completer | None = None, _validator: Validator | None = None, _lexer: Lexer | None = None, - _extra_buffer_processors=None, + _extra_buffer_processors: list[Processor] | None = None, _extra_layout_body: AnyContainer | None = None, - _extra_toolbars=None, - _input_buffer_height=None, + _extra_toolbars: list[AnyContainer] | None = None, + _input_buffer_height: AnyDimension | None = None, ) -> None: self.get_globals: _GetNamespace = get_globals or (lambda: {}) self.get_locals: _GetNamespace = get_locals or self.get_globals @@ -466,24 +472,36 @@ def get_compiler_flags(self) -> int: return flags - @property - def add_key_binding(self) -> Callable[[_T], _T]: + def add_key_binding( + self, + *keys: Keys | str, + filter: FilterOrBool = True, + eager: FilterOrBool = False, + is_global: FilterOrBool = False, + save_before: Callable[[KeyPressEvent], bool] = (lambda e: True), + record_in_macro: FilterOrBool = True, + ) -> Callable[[_T_kh], _T_kh]: """ Shortcut for adding new key bindings. (Mostly useful for a config.py file, that receives a PythonInput/Repl instance as input.) + All arguments are identical to prompt_toolkit's `KeyBindings.add`. + :: @python_input.add_key_binding(Keys.ControlX, filter=...) def handler(event): ... """ - - def add_binding_decorator(*k, **kw): - return self.extra_key_bindings.add(*k, **kw) - - return add_binding_decorator + return self.extra_key_bindings.add( + *keys, + filter=filter, + eager=eager, + is_global=is_global, + save_before=save_before, + record_in_macro=record_in_macro, + ) def install_code_colorscheme(self, name: str, style: BaseStyle) -> None: """ diff --git a/ptpython/repl.py b/ptpython/repl.py index 98b01afa..1db2e64f 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -158,7 +158,7 @@ def run(self) -> None: clear_title() self._remove_from_namespace() - async def run_and_show_expression_async(self, text: str): + async def run_and_show_expression_async(self, text: str) -> object: loop = asyncio.get_event_loop() try: From f0193017e5a38b07e9ad1dfdac8bc5e416229089 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 22:22:00 +0000 Subject: [PATCH 32/83] Many asyncio REPL improvements. - Added `--asyncio` flag to the `ptpython` entry point to activate the asyncio-REPL. This will ensure that an event loop is created at the start in which we can run top-level await statements. - Use `get_running_loop()` instead of `get_event_loop()`. - Better handling of `SystemExit` and control-c in the async REPL. --- ptpython/contrib/asyncssh_repl.py | 2 +- ptpython/entry_points/run_ptpython.py | 13 ++++- ptpython/python_input.py | 4 +- ptpython/repl.py | 76 +++++++++++++++++++-------- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 35da7426..2f74eb2b 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -110,7 +110,7 @@ def data_received(self, data: AnyStr, datatype: int | None) -> None: """ When data is received, send to inputstream of the CLI and repaint. """ - self._input_pipe.send(data) + self._input_pipe.send(data) # type: ignore def _print( self, *data: object, sep: str = " ", end: str = "\n", file: Any = None diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index c0b4078b..7fa69c66 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -9,6 +9,7 @@ -h, --help show this help message and exit --vi Enable Vi key bindings -i, --interactive Start interactive shell after executing this file. + --asyncio Run an asyncio event loop to support top-level "await". --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 @@ -24,6 +25,7 @@ from __future__ import annotations import argparse +import asyncio import os import pathlib import sys @@ -68,6 +70,11 @@ def create_parser() -> _Parser: action="store_true", help="Start interactive shell after executing this file.", ) + parser.add_argument( + "--asyncio", + action="store_true", + help='Run an asyncio event loop to support top-level "await".', + ) parser.add_argument( "--light-bg", action="store_true", @@ -206,7 +213,7 @@ def configure(repl: PythonRepl) -> None: import __main__ - embed( + embed_result = embed( # type: ignore vi_mode=a.vi, history_filename=history_file, configure=configure, @@ -214,8 +221,12 @@ def configure(repl: PythonRepl) -> None: globals=__main__.__dict__, startup_paths=startup_paths, title="Python REPL (ptpython)", + return_asyncio_coroutine=a.asyncio, ) + if a.asyncio: + asyncio.run(embed_result) + if __name__ == "__main__": run() diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 14995db4..54ddbef2 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -4,7 +4,7 @@ """ from __future__ import annotations -from asyncio import get_event_loop +from asyncio import get_running_loop from functools import partial from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar, Union @@ -1010,7 +1010,7 @@ def get_signatures_in_executor(document: Document) -> list[Signature]: app = self.app async def on_timeout_task() -> None: - loop = get_event_loop() + loop = get_running_loop() # Never run multiple get-signature threads. if self._get_signatures_thread_running: diff --git a/ptpython/repl.py b/ptpython/repl.py index 1db2e64f..e7058ea1 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -12,6 +12,7 @@ import asyncio import builtins import os +import signal import sys import traceback import types @@ -158,27 +159,58 @@ def run(self) -> None: clear_title() self._remove_from_namespace() - async def run_and_show_expression_async(self, text: str) -> object: - loop = asyncio.get_event_loop() + async def run_and_show_expression_async(self, text: str) -> Any: + loop = asyncio.get_running_loop() + system_exit: SystemExit | None = None try: - result = await self.eval_async(text) - except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception. - raise - except SystemExit: - return - except BaseException as e: - self._handle_exception(e) - else: - # Print. - if result is not None: - await loop.run_in_executor(None, lambda: self._show_result(result)) + try: + # Create `eval` task. Ensure that control-c will cancel this + # task. + async def eval() -> Any: + nonlocal system_exit + try: + return await self.eval_async(text) + except SystemExit as e: + # Don't propagate SystemExit in `create_task()`. That + # will kill the event loop. We want to handle it + # gracefully. + system_exit = e + + task = asyncio.create_task(eval()) + loop.add_signal_handler(signal.SIGINT, lambda *_: task.cancel()) + result = await task + + if system_exit is not None: + raise system_exit + except KeyboardInterrupt: + # KeyboardInterrupt doesn't inherit from Exception. + raise + except SystemExit: + raise + except BaseException as e: + self._handle_exception(e) + else: + # Print. + if result is not None: + await loop.run_in_executor(None, lambda: self._show_result(result)) - # Loop. - self.current_statement_index += 1 - self.signatures = [] - # Return the result for future consumers. - return result + # Loop. + self.current_statement_index += 1 + self.signatures = [] + # Return the result for future consumers. + return result + finally: + loop.remove_signal_handler(signal.SIGINT) + + except KeyboardInterrupt as e: + # Handle all possible `KeyboardInterrupt` errors. This can + # happen during the `eval`, but also during the + # `show_result` if something takes too long. + # (Try/catch is around the whole block, because we want to + # prevent that a Control-C keypress terminates the REPL in + # any case.) + self._handle_keyboard_interrupt(e) async def run_async(self) -> None: """ @@ -192,7 +224,7 @@ async def run_async(self) -> None: (Both for control-C to work, as well as for the code to see the right thread in which it was embedded). """ - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() if self.terminal_title: set_title(self.terminal_title) @@ -222,6 +254,8 @@ async def run_async(self) -> None: # `KeyboardInterrupt` exceptions can end up in the event # loop selector. self._handle_keyboard_interrupt(e) + except SystemExit: + return finally: if self.terminal_title: clear_title() @@ -250,7 +284,7 @@ def eval(self, line: str) -> object: result = eval(code, self.get_globals(), self.get_locals()) if _has_coroutine_flag(code): - result = asyncio.get_event_loop().run_until_complete(result) + result = asyncio.get_running_loop().run_until_complete(result) self._store_eval_result(result) return result @@ -263,7 +297,7 @@ def eval(self, line: str) -> object: result = eval(code, self.get_globals(), self.get_locals()) if _has_coroutine_flag(code): - result = asyncio.get_event_loop().run_until_complete(result) + result = asyncio.get_running_loop().run_until_complete(result) return None From eb39a3201eb6f45f95f1d47434c5e31f3bd4ed36 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 22:30:49 +0000 Subject: [PATCH 33/83] Show help information when starting asyncio-REPL. --- ptpython/entry_points/run_ptpython.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 7fa69c66..1d4a5329 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -225,6 +225,8 @@ def configure(repl: PythonRepl) -> None: ) if a.asyncio: + print("Starting ptpython asyncio REPL") + print('Use "await" directly instead of "asyncio.run()".') asyncio.run(embed_result) From 96d621cf305ae4cf9a29db5d92f0a5b510470cf0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 12 Dec 2023 22:34:23 +0000 Subject: [PATCH 34/83] Added info about top-level await to the README. --- README.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.rst b/README.rst index 2db3f695..8ec9aca4 100644 --- a/README.rst +++ b/README.rst @@ -71,6 +71,7 @@ The help menu shows basic command-line options. -h, --help show this help message and exit --vi Enable Vi key bindings -i, --interactive Start interactive shell after executing this file. + --asyncio Run an asyncio event loop to support top-level "await". --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 @@ -171,6 +172,20 @@ error. .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/validation.png +Asyncio REPL and top level await +******************************** + +In order to get top-level ``await`` support, start ptpython as follows: + +.. code:: + + ptpython --asyncio + +This will spawn an asyncio event loop and embed the async REPL in the event +loop. After this, top-level await will work and statements like ``await +asyncio.sleep(10)`` will execute. + + Additional features ******************* From eda7f58d453c3c1b96e4357dfa203f3160cfc4c1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 13 Dec 2023 09:33:54 +0000 Subject: [PATCH 35/83] Required prompt_toolkit 3.0.34 because of 'OneStyleAndTextTuple' import. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ad26545a..d091d290 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.28, because of cursor shape support. - "prompt_toolkit>=3.0.28,<3.1.0", + # Use prompt_toolkit 3.0.34, because of `OneStyleAndTextTuple` import. + "prompt_toolkit>=3.0.34,<3.1.0", "pygments", ], python_requires=">=3.7", From d2e35e7c617a015299ce10b53d30067b347b03c9 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 13 Dec 2023 09:35:13 +0000 Subject: [PATCH 36/83] Release 3.0.24 --- CHANGELOG | 20 ++++++++++++++++++++ setup.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e753cfd9..879e7439 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,26 @@ CHANGELOG ========= +3.0.24: 2023-12-13 +------------------ + +Fixes: +- Don't show "Impossible to read config file" warnings when no config file was + passed to `run_config()`. +- IPython integration fixes: + * Fix top-level await in IPython. + * Fix IPython `DeprecationWarning`. +- Output printing fixes: + * Paginate exceptions if pagination is enabled. + * Handle big outputs without running out of memory. +- Asyncio REPL improvements: + * From now on, passing `--asyncio` is required to activate the asyncio-REPL. + This will ensure that an event loop is created at the start in which we can + run top-level await statements. + * Use `get_running_loop()` instead of `get_event_loop()`. + * Better handling of `SystemExit` and control-c in the async REPL. + + 3.0.23: 2023-02-22 ------------------ diff --git a/setup.py b/setup.py index d091d290..a35a4797 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.23", + version="3.0.24", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 961b945abb20d4d57615da97905d5d00ab10f1fe Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 13 Dec 2023 12:12:04 +0000 Subject: [PATCH 37/83] Fix handling of 'config file does not exist' when embedding ptpython. --- ptpython/repl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index e7058ea1..fc9b9da1 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -433,9 +433,10 @@ def enter_to_continue() -> None: input("\nPress ENTER to continue...") # Check whether this file exists. - if not os.path.exists(config_file) and explicit_config_file: - print("Impossible to read %r" % config_file) - enter_to_continue() + if not os.path.exists(config_file): + if explicit_config_file: + print(f"Impossible to read {config_file}") + enter_to_continue() return # Run the config file in an empty namespace. From 1a96f0ee6a2691c18dd91d756d045f488975faec Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 14 Dec 2023 09:33:03 +0000 Subject: [PATCH 38/83] Release 3.0.25 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 879e7439..e8277002 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.25: 2023-12-14 +------------------ + +Fixes: +- Fix handling of 'config file does not exist' when embedding ptpython. + + 3.0.24: 2023-12-13 ------------------ diff --git a/setup.py b/setup.py index a35a4797..bc1241bb 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.24", + version="3.0.25", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 17d04b9f9d4c812ed4d161c110fe9cd54069c4be Mon Sep 17 00:00:00 2001 From: tomaszchalupnik Date: Fri, 2 Feb 2024 22:28:50 +0100 Subject: [PATCH 39/83] Reraise GeneratorExit error as excepted exception --- ptpython/printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptpython/printer.py b/ptpython/printer.py index 3618934e..85bd9c88 100644 --- a/ptpython/printer.py +++ b/ptpython/printer.py @@ -155,7 +155,7 @@ def _format_result_output( ) yield from formatted_result_repr return - except KeyboardInterrupt: + except (GeneratorExit, KeyboardInterrupt): raise # Don't catch here. except: # For bad code, `__getattr__` can raise something that's not an From 1c558f861c2d47ad7bdf639567fa9a5c9237ade1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 6 Feb 2024 10:16:06 +0000 Subject: [PATCH 40/83] Release 3.0.26 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e8277002..d8738625 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.26: 2024-02-06 +------------------ + +Fixes: +- Handle `GeneratorExit` exception when leaving the paginator. + + 3.0.25: 2023-12-14 ------------------ diff --git a/setup.py b/setup.py index bc1241bb..a54da35d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.25", + version="3.0.26", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 4f6b3a3d8a60387cf9e22e6112a320809ab91679 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 Dec 2023 07:11:01 -0600 Subject: [PATCH 41/83] Package: Add PyPI Links to repo, issues, and changelog --- setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup.py b/setup.py index a54da35d..38f30282 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,14 @@ 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=[ From d63ebc5cdb60fd57db524eaee97b099acf45dee6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 Dec 2023 07:01:28 -0600 Subject: [PATCH 42/83] docs(README): Update GitHub action button --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 8ec9aca4..8616132a 100644 --- a/README.rst +++ b/README.rst @@ -288,8 +288,8 @@ Special thanks to - `wcwidth `_: Determine columns needed for a wide characters. - `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# +.. |Build Status| image:: https://github.com/prompt-toolkit/ptpython/actions/workflows/test.yaml/badge.svg + :target: https://github.com/prompt-toolkit/ptpython/actions/workflows/test.yaml .. |License| image:: https://img.shields.io/github/license/prompt-toolkit/ptpython.svg :target: https://github.com/prompt-toolkit/ptpython/blob/master/LICENSE From f40e091012e9022babafe5a077bea7da154e3b39 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 Dec 2023 07:02:33 -0600 Subject: [PATCH 43/83] docs(README): Fix PyPI badge and link --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 8616132a..63d9aca5 100644 --- a/README.rst +++ b/README.rst @@ -294,6 +294,6 @@ Special thanks to .. |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/ +.. |PyPI| image:: https://img.shields.io/pypi/v/ptpython.svg + :target: https://pypi.org/project/ptpython/ :alt: Latest Version From 7f76e0df8697fd134e4d785343e143ba3b2f0780 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Tue, 27 Feb 2024 18:07:25 -0500 Subject: [PATCH 44/83] Update `prompt_toolkit` from `3.0.34` to `3.0.43` Resolves https://github.com/prompt-toolkit/ptpython/issues/564 where `cannot import name 'OneStyleAndTextTuple'` is emitted when launching `ptipython`. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 38f30282..b2fde169 100644 --- a/setup.py +++ b/setup.py @@ -29,8 +29,8 @@ "appdirs", "importlib_metadata;python_version<'3.8'", "jedi>=0.16.0", - # Use prompt_toolkit 3.0.34, because of `OneStyleAndTextTuple` import. - "prompt_toolkit>=3.0.34,<3.1.0", + # Use prompt_toolkit 3.0.43, because of `OneStyleAndTextTuple` import. + "prompt_toolkit>=3.0.43,<3.1.0", "pygments", ], python_requires=">=3.7", From 3df92f35d86f048f5c634c3c8ba853ad7bc80568 Mon Sep 17 00:00:00 2001 From: "David J. Mack" Date: Fri, 18 Nov 2022 16:32:48 +0100 Subject: [PATCH 45/83] docs: Add windows terminal profile configuration --- README.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.rst b/README.rst index 63d9aca5..130e4581 100644 --- a/README.rst +++ b/README.rst @@ -255,6 +255,22 @@ Windows. Some things might not work, but it is usable: .. image :: https://github.com/jonathanslenders/ptpython/raw/master/docs/images/windows.png +Windows terminal integration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are using the `Windows Terminal `_ and want to +integrate ``ptpython`` as a profile, go to *Settings -> Open JSON file* and add the +following profile under *profiles.list*: + +.. code-block:: JSON + + { + "commandline": "%SystemRoot%\\System32\\cmd.exe /k ptpython", + "guid": "{f91d49a3-741b-409c-8a15-c4360649121f}", + "hidden": false, + "icon": "https://upload.wikimedia.org/wikipedia/commons/e/e6/Python_Windows_interpreter_icon_2006%E2%80%932016_Tiny.png", + "name": "ptpython@cmd" + } FAQ *** From 394fe38a2ec1206131036d901688dd695bdda439 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 16 May 2024 12:05:31 +0000 Subject: [PATCH 46/83] Limit number of completions to 5k (for performance). --- ptpython/completer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 91d66474..264918e8 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -6,6 +6,7 @@ import keyword import re from enum import Enum +from itertools import islice from typing import TYPE_CHECKING, Any, Callable, Iterable from prompt_toolkit.completion import ( @@ -617,7 +618,10 @@ def __init__( def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: - completions = list(self.completer.get_completions(document, complete_event)) + completions = list( + # Limit at 5k completions for performance. + islice(self.completer.get_completions(document, complete_event), 0, 5000) + ) complete_private_attributes = self.complete_private_attributes() hide_private = False From 5fb21bd51f71d220c018fca9d732df48c72c52b8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 16 May 2024 12:07:47 +0000 Subject: [PATCH 47/83] Apply latest Ruff for formatting. --- examples/asyncio-python-embed.py | 1 + examples/asyncio-ssh-python-embed.py | 1 + examples/ptpython_config/config.py | 1 + examples/python-embed-with-custom-prompt.py | 1 + examples/python-embed.py | 4 ++-- examples/python-input.py | 4 ++-- examples/test-cases/ptpython-in-other-thread.py | 1 + ptpython/__main__.py | 1 + ptpython/contrib/asyncssh_repl.py | 1 + ptpython/entry_points/run_ptpython.py | 1 + ptpython/eventloop.py | 1 + ptpython/history_browser.py | 1 + ptpython/ipython.py | 1 + ptpython/layout.py | 1 + ptpython/python_input.py | 4 ++-- ptpython/repl.py | 1 + ptpython/signatures.py | 1 + ptpython/utils.py | 1 + pyproject.toml | 8 ++++---- 19 files changed, 25 insertions(+), 10 deletions(-) diff --git a/examples/asyncio-python-embed.py b/examples/asyncio-python-embed.py index a8fbba5a..38cc1c20 100755 --- a/examples/asyncio-python-embed.py +++ b/examples/asyncio-python-embed.py @@ -11,6 +11,7 @@ to stdout, it won't break the input line, but instead writes nicely above the prompt. """ + import asyncio from ptpython.repl import embed diff --git a/examples/asyncio-ssh-python-embed.py b/examples/asyncio-ssh-python-embed.py index be0689e7..9bbad86f 100755 --- a/examples/asyncio-ssh-python-embed.py +++ b/examples/asyncio-ssh-python-embed.py @@ -5,6 +5,7 @@ Run this example and then SSH to localhost, port 8222. """ + import asyncio import logging diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index b25850a2..bfd3914e 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -5,6 +5,7 @@ On Linux, this is: ~/.config/ptpython/config.py On macOS, this is: ~/Library/Application Support/ptpython/config.py """ + from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.key_binding.key_processor import KeyPress from prompt_toolkit.keys import Keys diff --git a/examples/python-embed-with-custom-prompt.py b/examples/python-embed-with-custom-prompt.py index d54da1da..5e8c7079 100755 --- a/examples/python-embed-with-custom-prompt.py +++ b/examples/python-embed-with-custom-prompt.py @@ -2,6 +2,7 @@ """ Example of embedding a Python REPL, and setting a custom prompt. """ + from prompt_toolkit.formatted_text import HTML, AnyFormattedText from ptpython.prompt_style import PromptStyle diff --git a/examples/python-embed.py b/examples/python-embed.py index 49224ac2..a7481011 100755 --- a/examples/python-embed.py +++ b/examples/python-embed.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ + from ptpython.repl import embed diff --git a/examples/python-input.py b/examples/python-input.py index 567c2ee6..d586d0f5 100755 --- a/examples/python-input.py +++ b/examples/python-input.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ + from ptpython.python_input import PythonInput diff --git a/examples/test-cases/ptpython-in-other-thread.py b/examples/test-cases/ptpython-in-other-thread.py index 7c788464..bfe14109 100644 --- a/examples/test-cases/ptpython-in-other-thread.py +++ b/examples/test-cases/ptpython-in-other-thread.py @@ -5,6 +5,7 @@ (For testing whether it's working fine if it's not embedded in the main thread.) """ + import threading from ptpython.repl import embed diff --git a/ptpython/__main__.py b/ptpython/__main__.py index c0062613..3a2f7ddf 100644 --- a/ptpython/__main__.py +++ b/ptpython/__main__.py @@ -1,6 +1,7 @@ """ Make `python -m ptpython` an alias for running `./ptpython`. """ + from __future__ import annotations from .entry_points.run_ptpython import run diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py index 2f74eb2b..a86737b6 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -6,6 +6,7 @@ should make sure not to use Python 3-only syntax, because this package should be installable in Python 2 as well! """ + from __future__ import annotations import asyncio diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 1d4a5329..05df9714 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -22,6 +22,7 @@ PTPYTHON_CONFIG_HOME: a configuration directory to use PYTHONSTARTUP: file executed on interactive startup (no default) """ + from __future__ import annotations import argparse diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 14ab64be..670d09bc 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -7,6 +7,7 @@ in readline. ``prompt-toolkit`` doesn't understand that input hook, but this will fix it for Tk.) """ + from __future__ import annotations import sys diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index b667be12..383cd975 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -4,6 +4,7 @@ `create_history_application` creates an `Application` instance that runs will run as a sub application of the Repl/PythonInput. """ + from __future__ import annotations from functools import partial diff --git a/ptpython/ipython.py b/ptpython/ipython.py index ad0516a3..263a981d 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,7 @@ offer. """ + from __future__ import annotations from typing import Iterable diff --git a/ptpython/layout.py b/ptpython/layout.py index 2c1ec15f..fc00005b 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -1,6 +1,7 @@ """ Creation of the `Layout` instance for the Python input/REPL. """ + from __future__ import annotations import platform diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 54ddbef2..18421c88 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -2,6 +2,7 @@ Application for reading Python input. This can be used for creation of Python REPLs. """ + from __future__ import annotations from asyncio import get_running_loop @@ -98,8 +99,7 @@ class _SupportsLessThan(Protocol): # Taken from typeshed. _T_lt is used by "sorted", which needs anything # sortable. - def __lt__(self, __other: Any) -> bool: - ... + def __lt__(self, __other: Any) -> bool: ... _T_lt = TypeVar("_T_lt", bound="_SupportsLessThan") diff --git a/ptpython/repl.py b/ptpython/repl.py index fc9b9da1..bbbd852e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -7,6 +7,7 @@ embed(globals(), locals(), vi_mode=False) """ + from __future__ import annotations import asyncio diff --git a/ptpython/signatures.py b/ptpython/signatures.py index d4cb98c2..b3e5c914 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -5,6 +5,7 @@ Either with the Jedi library, or using `inspect.signature` if Jedi fails and we can use `eval()` to evaluate the function object. """ + from __future__ import annotations import inspect diff --git a/ptpython/utils.py b/ptpython/utils.py index 28887d20..92cfc2a1 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -1,6 +1,7 @@ """ For internal use only. """ + from __future__ import annotations import re diff --git a/pyproject.toml b/pyproject.toml index 5421c454..ce420372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.ruff] target-version = "py37" -select = [ +lint.select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes @@ -12,14 +12,14 @@ select = [ "RUF100", # unused-noqa "Q", # quotes ] -ignore = [ +lint.ignore = [ "E501", # Line too long, handled by black "C901", # Too complex "E722", # bare except. ] -[tool.ruff.per-file-ignores] +[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. @@ -30,6 +30,6 @@ ignore = [ "tests/run_tests.py" = ["F401"] # Unused imports. -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["ptpython"] known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] From 3ec97d7360450f9d79a745a67e14312243227825 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 16 May 2024 12:09:19 +0000 Subject: [PATCH 48/83] Apply latest ruff fixes. --- ptpython/history_browser.py | 2 +- ptpython/ipython.py | 9 ++++----- ptpython/layout.py | 4 ++-- ptpython/prompt_style.py | 4 ++-- ptpython/python_input.py | 8 ++++---- ptpython/validator.py | 2 +- setup.py | 5 ++--- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index 383cd975..ae0ac03e 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -411,7 +411,7 @@ def __init__( if len(history_strings) > HISTORY_COUNT: history_lines[0] = ( - "# *** History has been truncated to %s lines ***" % HISTORY_COUNT + f"# *** History has been truncated to {HISTORY_COUNT} lines ***" ) self.history_lines = history_lines diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 263a981d..0692214d 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -157,7 +157,7 @@ def get_completions( for m in sorted(self.magics_manager.magics["line"]): if m.startswith(text): - yield Completion("%s" % m, -len(text)) + yield Completion(f"{m}", -len(text)) class AliasCompleter(Completer): @@ -173,7 +173,7 @@ def get_completions( for a, cmd in sorted(aliases, key=lambda a: a[0]): if a.startswith(text): - yield Completion("%s" % a, -len(text), display_meta=cmd) + yield Completion(f"{a}", -len(text), display_meta=cmd) class IPythonInput(PythonInput): @@ -280,9 +280,8 @@ def initialize_extensions(shell, extensions): shell.extension_manager.load_extension(ext) except: warn( - "Error in loading extension: %s" % ext - + "\nCheck your config files in %s" - % ipy_utils.path.get_ipython_dir() + f"Error in loading extension: {ext}" + + f"\nCheck your config files in {ipy_utils.path.get_ipython_dir()}" ) shell.showtraceback() diff --git a/ptpython/layout.py b/ptpython/layout.py index fc00005b..622df594 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -132,7 +132,7 @@ def goto_next(mouse_event: MouseEvent) -> None: tokens.append(("class:sidebar" + sel, " >" if selected else " ")) tokens.append(("class:sidebar.label" + sel, "%-24s" % label, select_item)) tokens.append(("class:sidebar.status" + sel, " ", select_item)) - tokens.append(("class:sidebar.status" + sel, "%s" % status, goto_next)) + tokens.append(("class:sidebar.status" + sel, f"{status}", goto_next)) if selected: tokens.append(("[SetCursorPosition]", "")) @@ -529,7 +529,7 @@ def create_exit_confirmation( def get_text_fragments() -> StyleAndTextTuples: # Show "Do you really want to exit?" return [ - (style, "\n %s ([y]/n) " % python_input.exit_message), + (style, f"\n {python_input.exit_message} ([y]/n) "), ("[SetCursorPosition]", ""), (style, " \n"), ] diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py index 96b738f7..465c3dbe 100644 --- a/ptpython/prompt_style.py +++ b/ptpython/prompt_style.py @@ -48,7 +48,7 @@ def __init__(self, python_input: PythonInput) -> None: def in_prompt(self) -> AnyFormattedText: return [ ("class:in", "In ["), - ("class:in.number", "%s" % self.python_input.current_statement_index), + ("class:in.number", f"{self.python_input.current_statement_index}"), ("class:in", "]: "), ] @@ -58,7 +58,7 @@ def in2_prompt(self, width: int) -> AnyFormattedText: def out_prompt(self) -> AnyFormattedText: return [ ("class:out", "Out["), - ("class:out.number", "%s" % self.python_input.current_statement_index), + ("class:out.number", f"{self.python_input.current_statement_index}"), ("class:out", "]:"), ("", " "), ] diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 18421c88..d66b5ae8 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -880,18 +880,18 @@ def get_values() -> dict[str, Callable[[], bool]]: Option( title="Min brightness", description="Minimum brightness for the color scheme (default=0.0).", - get_current_value=lambda: "%.2f" % self.min_brightness, + get_current_value=lambda: f"{self.min_brightness:.2f}", get_values=lambda: { - "%.2f" % value: partial(self._set_min_brightness, value) + f"{value:.2f}": partial(self._set_min_brightness, value) for value in brightness_values }, ), Option( title="Max brightness", description="Maximum brightness for the color scheme (default=1.0).", - get_current_value=lambda: "%.2f" % self.max_brightness, + get_current_value=lambda: f"{self.max_brightness:.2f}", get_values=lambda: { - "%.2f" % value: partial(self._set_max_brightness, value) + f"{value:.2f}": partial(self._set_max_brightness, value) for value in brightness_values }, ), diff --git a/ptpython/validator.py b/ptpython/validator.py index 91b9c284..cf2ee542 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -59,4 +59,4 @@ def validate(self, document: Document) -> None: except ValueError as e: # In Python 2, compiling "\x9" (an invalid escape sequence) raises # ValueError instead of SyntaxError. - raise ValidationError(0, "Syntax Error: %s" % e) + raise ValidationError(0, f"Syntax Error: {e}") diff --git a/setup.py b/setup.py index b2fde169..a2618a61 100644 --- a/setup.py +++ b/setup.py @@ -47,12 +47,11 @@ "console_scripts": [ "ptpython = ptpython.entry_points.run_ptpython:run", "ptipython = ptpython.entry_points.run_ptipython:run", - "ptpython%s = ptpython.entry_points.run_ptpython:run" % sys.version_info[0], + f"ptpython{sys.version_info[0]} = ptpython.entry_points.run_ptpython:run", "ptpython{}.{} = ptpython.entry_points.run_ptpython:run".format( *sys.version_info[:2] ), - "ptipython%s = ptpython.entry_points.run_ptipython:run" - % sys.version_info[0], + f"ptipython{sys.version_info[0]} = ptpython.entry_points.run_ptipython:run", "ptipython{}.{} = ptpython.entry_points.run_ptipython:run".format( *sys.version_info[:2] ), From c1a431047e88ae4b2e2b0613bf66c68095f61a4c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 16 May 2024 12:21:32 +0000 Subject: [PATCH 49/83] Several typing fixes. --- ptpython/python_input.py | 14 ++++++-------- ptpython/repl.py | 17 +++++++++-------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index d66b5ae8..975d3d98 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -347,14 +347,6 @@ def __init__( "classic": ClassicPrompt(), } - self.get_input_prompt = lambda: self.all_prompt_styles[ - self.prompt_style - ].in_prompt() - - self.get_output_prompt = lambda: self.all_prompt_styles[ - self.prompt_style - ].out_prompt() - #: Load styles. self.code_styles: dict[str, BaseStyle] = get_all_code_styles() self.ui_styles = get_all_ui_styles() @@ -425,6 +417,12 @@ def __init__( else: self._app = None + def get_input_prompt(self) -> AnyFormattedText: + return self.all_prompt_styles[self.prompt_style].in_prompt() + + def get_output_prompt(self) -> AnyFormattedText: + return self.all_prompt_styles[self.prompt_style].out_prompt() + def _accept_handler(self, buff: Buffer) -> bool: app = get_app() app.exit(result=buff.text) diff --git a/ptpython/repl.py b/ptpython/repl.py index bbbd852e..ea2d84f0 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -19,7 +19,8 @@ import types import warnings from dis import COMPILER_FLAG_NAMES -from typing import Any, Callable, ContextManager, Iterable +from pathlib import Path +from typing import Any, Callable, ContextManager, Iterable, Sequence from prompt_toolkit.formatted_text import OneStyleAndTextTuple from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context @@ -64,7 +65,7 @@ def _has_coroutine_flag(code: types.CodeType) -> bool: class PythonRepl(PythonInput): def __init__(self, *a, **kw) -> None: - self._startup_paths = kw.pop("startup_paths", None) + self._startup_paths: Sequence[str | Path] | None = kw.pop("startup_paths", None) super().__init__(*a, **kw) self._load_start_paths() @@ -348,7 +349,7 @@ def _store_eval_result(self, result: object) -> None: def get_compiler_flags(self) -> int: return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT - def _compile_with_flags(self, code: str, mode: str): + def _compile_with_flags(self, code: str, mode: str) -> Any: "Compile code with the right compiler flags." return compile( code, @@ -459,13 +460,13 @@ def enter_to_continue() -> None: def embed( - globals=None, - locals=None, + globals: dict[str, Any] | None = None, + locals: dict[str, Any] | None = None, configure: Callable[[PythonRepl], None] | None = None, vi_mode: bool = False, history_filename: str | None = None, title: str | None = None, - startup_paths=None, + startup_paths: Sequence[str | Path] | None = None, patch_stdout: bool = False, return_asyncio_coroutine: bool = False, ) -> None: @@ -494,10 +495,10 @@ def embed( locals = locals or globals - def get_globals(): + def get_globals() -> dict[str, Any]: return globals - def get_locals(): + def get_locals() -> dict[str, Any]: return locals # Create REPL. From 95afc939fe348558486139909b6273f1f7fa245c Mon Sep 17 00:00:00 2001 From: Elliot Ford Date: Fri, 10 May 2024 16:41:19 +0100 Subject: [PATCH 50/83] Update supported versions on README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 130e4581..06c1e02b 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ 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, +Python versions from 2.6 up to 3.11 and work cross platform (Linux, BSD, OS X and Windows). Note: this version of ptpython requires at least Python 3.6. Install ptpython From 8f68b6ceccbe57d15cb864fae45a5e7b82524bdc Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 27 May 2024 20:52:04 +0000 Subject: [PATCH 51/83] Ruff compatibility: fix import order. --- ptpython/eventloop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index 670d09bc..a6462748 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -24,9 +24,8 @@ def _inputhook_tk(inputhook_context: InputHookContext) -> None: Run the Tk eventloop until prompt-toolkit needs to process the next input. """ # Get the current TK application. - import tkinter - import _tkinter # Keep this imports inline! + import tkinter root = tkinter._default_root # type: ignore From fb9bed1e5956ac5f109fd4cb401b3fae997efcd7 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 27 May 2024 20:46:01 +0000 Subject: [PATCH 52/83] Release 3.0.27 --- CHANGELOG | 7 +++++++ setup.py | 7 +++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d8738625..6f2bbb9a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.27: 2024-05-27 +------------------ + +- Limit number of completions to 5k (for performance). +- Several typing fixes. + + 3.0.26: 2024-02-06 ------------------ diff --git a/setup.py b/setup.py index a2618a61..84f18be2 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.26", + version="3.0.27", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, @@ -38,8 +38,11 @@ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", - "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", ], From f66a289544a21089f561e21f7632305ff4eed204 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Fri, 28 Jun 2024 16:36:46 +0200 Subject: [PATCH 53/83] Clean up signatures on ctrl-c --- ptpython/python_input.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 975d3d98..b1773643 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -1116,4 +1116,5 @@ def pre_run( return result except KeyboardInterrupt: # Abort - try again. + self.signatures = [] self.default_buffer.document = Document() From 4b456890f9b06fc9ea75eef681bb9773c2172c89 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 09:21:01 +0000 Subject: [PATCH 54/83] Add custom 'exit' function to return from REPL. - Don't terminate `sys.stdin` when `exit` is called (important for `embed()`). - Don't require 'exit' to be called with parentheses. --- ptpython/repl.py | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/ptpython/repl.py b/ptpython/repl.py index ea2d84f0..6b60018e 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -20,7 +20,7 @@ import warnings from dis import COMPILER_FLAG_NAMES from pathlib import Path -from typing import Any, Callable, ContextManager, Iterable, Sequence +from typing import Any, Callable, ContextManager, Iterable, NoReturn, Sequence from prompt_toolkit.formatted_text import OneStyleAndTextTuple from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context @@ -40,7 +40,15 @@ except ImportError: PyCF_ALLOW_TOP_LEVEL_AWAIT = 0 -__all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"] + +__all__ = [ + "PythonRepl", + "enable_deprecation_warnings", + "run_config", + "embed", + "exit", + "ReplExit", +] def _get_coroutine_flag() -> int | None: @@ -91,9 +99,16 @@ def run_and_show_expression(self, expression: str) -> None: raise except SystemExit: raise + except ReplExit: + raise except BaseException as e: self._handle_exception(e) else: + if isinstance(result, exit): + # When `exit` is evaluated without parentheses. + # Automatically trigger the `ReplExit` exception. + raise ReplExit + # Print. if result is not None: self._show_result(result) @@ -155,7 +170,10 @@ def run(self) -> None: continue # Run it; display the result (or errors if applicable). - self.run_and_show_expression(text) + try: + self.run_and_show_expression(text) + except ReplExit: + return finally: if self.terminal_title: clear_title() @@ -383,6 +401,7 @@ def get_ptpython() -> PythonInput: return self globals["get_ptpython"] = get_ptpython + globals["exit"] = exit() def _remove_from_namespace(self) -> None: """ @@ -459,6 +478,29 @@ def enter_to_continue() -> None: enter_to_continue() +class exit: + """ + Exit the ptpython REPL. + """ + + # This custom exit function ensures that the `embed` function returns from + # where we are embedded, and Python doesn't close `sys.stdin` like + # the default `exit` from `_sitebuiltins.Quitter` does. + + def __call__(self) -> NoReturn: + raise ReplExit + + def __repr__(self) -> str: + # (Same message as the built-in Python REPL.) + return "Use exit() or Ctrl-D (i.e. EOF) to exit" + + +class ReplExit(Exception): + """ + Exception raised by ptpython's exit function. + """ + + def embed( globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None, From cd54c27a6205226bdb00c5c44f045c32d9547acd Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 09:28:07 +0000 Subject: [PATCH 55/83] Fix GitHub actions workflow. Use 'ruff check' instead of 'ruff'. --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9a50f3bc..c62bdc39 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -27,7 +27,7 @@ jobs: - name: Type Checker run: | mypy ptpython - ruff . + ruff check . ruff format --check . - name: Run Tests run: | From 79cb14b1982fee48b86e1b0fdee8f70d8f849d56 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 09:36:04 +0000 Subject: [PATCH 56/83] Release 3.0.28 --- CHANGELOG | 13 +++++++++++++ setup.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6f2bbb9a..999f13d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,19 @@ CHANGELOG ========= +3.0.28: 2024-07-22 +------------------ + +New features: +- Custom 'exit' function to return from REPL that + * doesn't terminate `sys.stdin` when `exit` is called (important for + `embed()`). + * doesn't require to be called with parentheses. + +Fixes: +- Clean up signatures on control-c. + + 3.0.27: 2024-05-27 ------------------ diff --git a/setup.py b/setup.py index 84f18be2..8e84906e 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.27", + version="3.0.28", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From 3e7f68ee48995de1d89e1d4c6ba255bdd1bc7ff2 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 12:26:12 +0000 Subject: [PATCH 57/83] Improve dictionary completion performance. This improves the performance for dictionary-like objects where iterating over the keys is fast, but doing a lookup for the values is slow. This change ensures we only do value lookups when really needed. The change also caches the meta text so that we don't have to recompute it during navigation of the completion menu. --- ptpython/completer.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/ptpython/completer.py b/ptpython/completer.py index 264918e8..e8bab285 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -476,20 +476,34 @@ def _get_item_lookup_completions( Complete dictionary keys. """ - def meta_repr(value: object) -> Callable[[], str]: + def meta_repr(obj: object, key: object) -> Callable[[], str]: "Abbreviate meta text, make sure it fits on one line." + cached_result: str | None = None # We return a function, so that it gets computed when it's needed. # When there are many completions, that improves the performance # quite a bit (for the multi-column completion menu, we only need # to display one meta text). + # Note that we also do the lookup itself in here (`obj[key]`), + # because this part can also be slow for some mapping + # implementations. def get_value_repr() -> str: - text = self._do_repr(value) + nonlocal cached_result + if cached_result is not None: + return cached_result + + try: + value = obj[key] # type: ignore + + text = self._do_repr(value) + except BaseException: + return "-" # Take first line, if multiple lines. if "\n" in text: text = text.split("\n", 1)[0] + "..." + cached_result = text return text return get_value_repr @@ -504,24 +518,24 @@ def get_value_repr() -> str: # If this object is a dictionary, complete the keys. if isinstance(result, (dict, collections_abc.Mapping)): # Try to evaluate the key. - key_obj = key + key_obj_str = str(key) for k in [key, key + '"', key + "'"]: try: - key_obj = ast.literal_eval(k) + key_obj_str = str(ast.literal_eval(k)) except (SyntaxError, ValueError): continue else: break - for k, v in result.items(): - if str(k).startswith(str(key_obj)): + for k in result: + if str(k).startswith(key_obj_str): try: k_repr = self._do_repr(k) yield Completion( k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=meta_repr(v), + display_meta=meta_repr(result, k), ) except ReprFailedError: pass @@ -537,7 +551,7 @@ def get_value_repr() -> str: k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=meta_repr(result[k]), + display_meta=meta_repr(result, k), ) except KeyError: # `result[k]` lookup failed. Trying to complete From 5021832f76309755097b744f274c4e687a690b85 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 22 Jul 2024 12:42:43 +0000 Subject: [PATCH 58/83] Release 3.0.29 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 999f13d6..bef7d07f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +3.0.29: 2024-07-22 +------------------ + +Fixes: +- Further improve performance of dictionary completions. + + 3.0.28: 2024-07-22 ------------------ diff --git a/setup.py b/setup.py index 8e84906e..aa101764 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="ptpython", author="Jonathan Slenders", - version="3.0.28", + version="3.0.29", url="https://github.com/prompt-toolkit/ptpython", description="Python REPL build on top of prompt_toolkit", long_description=long_description, From b5d8c28535578eca504572c11a6ff893728ecac0 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 12:53:50 +0000 Subject: [PATCH 59/83] Show exception cause/context when printing an exception. --- ptpython/printer.py | 27 ++++++++++++++++----------- ptpython/repl.py | 4 ++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/ptpython/printer.py b/ptpython/printer.py index 85bd9c88..81ea16f3 100644 --- a/ptpython/printer.py +++ b/ptpython/printer.py @@ -254,8 +254,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 +322,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 +345,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/repl.py b/ptpython/repl.py index 6b60018e..9142d909 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -378,6 +378,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, From 37763164fd444771c9232ed10e1021d34b7a5d20 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 12:58:18 +0000 Subject: [PATCH 60/83] Drop Python 3.8, given it's end of life and no longer supported on GitHub CI. Also some typing fixes. --- .github/workflows/test.yaml | 6 +++--- ptpython/entry_points/run_ptpython.py | 13 ++++++------- setup.py | 5 ++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c62bdc39..2311e02a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,12 +10,12 @@ 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 + - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 05df9714..d083858d 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/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/setup.py b/setup.py index aa101764..bd2f962a 100644 --- a/setup.py +++ b/setup.py @@ -27,22 +27,21 @@ 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", + python_requires=">=3.8", 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.13", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python", ], From 04235d791b483af0ad36f578608d06bf4331f825 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 13:36:23 +0000 Subject: [PATCH 61/83] Use f-strings instead of %-style formatting. --- examples/asyncio-python-embed.py | 2 +- examples/asyncio-ssh-python-embed.py | 4 ++-- ptpython/layout.py | 12 +++++------- ptpython/printer.py | 1 - ptpython/repl.py | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) 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/ptpython/layout.py b/ptpython/layout.py index 622df594..9768598e 100644 --- a/ptpython/layout.py +++ b/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", " "), ] diff --git a/ptpython/printer.py b/ptpython/printer.py index 81ea16f3..a3578de7 100644 --- a/ptpython/printer.py +++ b/ptpython/printer.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys import traceback from dataclasses import dataclass from enum import Enum diff --git a/ptpython/repl.py b/ptpython/repl.py index 9142d909..ba6717fb 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -362,7 +362,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 From ce3a9e2f5495a7ae5146942e468e3565cbe3a87c Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 13:51:45 +0000 Subject: [PATCH 62/83] Use uv in github actions. --- .github/workflows/test.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2311e02a..c9fb0ae8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,20 +10,18 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + + - uses: astral-sh/setup-uv@v5 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 . ruff mypy pytest readme_renderer - pip list + uv pip install . ruff mypy pytest readme_renderer + uv pip list - name: Type Checker run: | mypy ptpython From 1f1eb1796a67699bbc2bba21129aaf1e6dab978b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 14:00:42 +0000 Subject: [PATCH 63/83] Reworked dummy test directory. --- .github/workflows/test.yaml | 2 +- tests/run_tests.py | 24 ------------------------ tests/test_dummy.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 25 deletions(-) delete mode 100755 tests/run_tests.py create mode 100755 tests/test_dummy.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c9fb0ae8..3f527abe 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,7 +29,7 @@ jobs: ruff format --check . - name: Run Tests run: | - ./tests/run_tests.py + pytest tests/ - name: Validate README.md # Ensure that the README renders correctly (required for uploading to PyPI). run: | 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 From f1dea7efe97426eec9e7218a0fdc0e17bc47aca8 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 20:35:59 +0000 Subject: [PATCH 64/83] Use pyproject.toml instead of setup.py Cherry-picked from: https://github.com/prompt-toolkit/ptpython/pull/599 Thanks to: Branch Vincent --- pyproject.toml | 58 +++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 41 ------------------------------- setup.py | 66 -------------------------------------------------- 3 files changed, 57 insertions(+), 108 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index ce420372..3780f9d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,55 @@ +[project] +name = "ptpython" +version = "3.0.29" +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" lint.select = [ @@ -27,9 +79,13 @@ lint.ignore = [ "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. [tool.ruff.lint.isort] known-first-party = ["ptpython"] known-third-party = ["prompt_toolkit", "pygments", "asyncssh"] + + +[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 bd2f962a..00000000 --- a/setup.py +++ /dev/null @@ -1,66 +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", - "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.8", - 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", - ], - 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 - }, -) From acf61459a7b203815a738cf6dc5ec20288e3ce19 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 20:48:11 +0000 Subject: [PATCH 65/83] Use uvx in GitHub workflows. --- .github/workflows/test.yaml | 18 ++++++++---------- ptpython/history_browser.py | 4 +++- ptpython/key_bindings.py | 4 +++- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3f527abe..74c3c7b8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,23 +14,21 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python-version }} - - name: Install Dependencies + - name: Type Checking run: | - uv pip install . ruff mypy pytest readme_renderer - uv pip list - - name: Type Checker + uvx --with . mypy ptpython + - name: Code formatting run: | - mypy ptpython - ruff check . - ruff format --check . - - name: Run Tests + uvx ruff check . + uvx ruff format --check . + - name: Unit test run: | - pytest tests/ + uvx --with . pytest tests/ - name: Validate README.md # 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/ptpython/history_browser.py b/ptpython/history_browser.py index ae0ac03e..72bc576d 100644 --- a/ptpython/history_browser.py +++ b/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 diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index d7bb575e..48c5f5ae 100644 --- a/ptpython/key_bindings.py +++ b/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 From 39b1cbda27e7b579e7b470311d409924457e072b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 20:59:43 +0000 Subject: [PATCH 66/83] Remove mypy.ini --- mypy.ini | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 mypy.ini 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 From 1527d0527625a2c72b154a6cb937f0e4dec9a87a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 21:07:52 +0000 Subject: [PATCH 67/83] use src/ directory for source code. --- .github/workflows/test.yaml | 2 +- pyproject.toml | 10 +++++----- {ptpython => src/ptpython}/__init__.py | 0 {ptpython => src/ptpython}/__main__.py | 0 {ptpython => src/ptpython}/completer.py | 0 {ptpython => src/ptpython}/contrib/__init__.py | 0 {ptpython => src/ptpython}/contrib/asyncssh_repl.py | 0 {ptpython => src/ptpython}/entry_points/__init__.py | 0 .../ptpython}/entry_points/run_ptipython.py | 0 .../ptpython}/entry_points/run_ptpython.py | 0 {ptpython => src/ptpython}/eventloop.py | 0 {ptpython => src/ptpython}/filters.py | 0 {ptpython => src/ptpython}/history_browser.py | 0 {ptpython => src/ptpython}/ipython.py | 0 {ptpython => src/ptpython}/key_bindings.py | 0 {ptpython => src/ptpython}/layout.py | 0 {ptpython => src/ptpython}/lexer.py | 0 {ptpython => src/ptpython}/printer.py | 0 {ptpython => src/ptpython}/prompt_style.py | 0 {ptpython => src/ptpython}/py.typed | 0 {ptpython => src/ptpython}/python_input.py | 0 {ptpython => src/ptpython}/repl.py | 0 {ptpython => src/ptpython}/signatures.py | 0 {ptpython => src/ptpython}/style.py | 0 {ptpython => src/ptpython}/utils.py | 0 {ptpython => src/ptpython}/validator.py | 0 26 files changed, 6 insertions(+), 6 deletions(-) rename {ptpython => src/ptpython}/__init__.py (100%) rename {ptpython => src/ptpython}/__main__.py (100%) rename {ptpython => src/ptpython}/completer.py (100%) rename {ptpython => src/ptpython}/contrib/__init__.py (100%) rename {ptpython => src/ptpython}/contrib/asyncssh_repl.py (100%) rename {ptpython => src/ptpython}/entry_points/__init__.py (100%) rename {ptpython => src/ptpython}/entry_points/run_ptipython.py (100%) rename {ptpython => src/ptpython}/entry_points/run_ptpython.py (100%) rename {ptpython => src/ptpython}/eventloop.py (100%) rename {ptpython => src/ptpython}/filters.py (100%) rename {ptpython => src/ptpython}/history_browser.py (100%) rename {ptpython => src/ptpython}/ipython.py (100%) rename {ptpython => src/ptpython}/key_bindings.py (100%) rename {ptpython => src/ptpython}/layout.py (100%) rename {ptpython => src/ptpython}/lexer.py (100%) rename {ptpython => src/ptpython}/printer.py (100%) rename {ptpython => src/ptpython}/prompt_style.py (100%) rename {ptpython => src/ptpython}/py.typed (100%) rename {ptpython => src/ptpython}/python_input.py (100%) rename {ptpython => src/ptpython}/repl.py (100%) rename {ptpython => src/ptpython}/signatures.py (100%) rename {ptpython => src/ptpython}/style.py (100%) rename {ptpython => src/ptpython}/utils.py (100%) rename {ptpython => src/ptpython}/validator.py (100%) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 74c3c7b8..457a4e48 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Type Checking run: | - uvx --with . mypy ptpython + uvx --with . mypy src/ptpython - name: Code formatting run: | uvx ruff check . diff --git a/pyproject.toml b/pyproject.toml index 3780f9d6..680d7087 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,11 +74,11 @@ 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. +"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] 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 100% rename from ptpython/completer.py rename to src/ptpython/completer.py 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 100% rename from ptpython/entry_points/run_ptpython.py rename to src/ptpython/entry_points/run_ptpython.py 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 100% rename from ptpython/history_browser.py rename to src/ptpython/history_browser.py 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 100% rename from ptpython/key_bindings.py rename to src/ptpython/key_bindings.py diff --git a/ptpython/layout.py b/src/ptpython/layout.py similarity index 100% rename from ptpython/layout.py rename to src/ptpython/layout.py 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 100% rename from ptpython/printer.py rename to src/ptpython/printer.py 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 100% rename from ptpython/python_input.py rename to src/ptpython/python_input.py diff --git a/ptpython/repl.py b/src/ptpython/repl.py similarity index 100% rename from ptpython/repl.py rename to src/ptpython/repl.py 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 100% rename from ptpython/style.py rename to src/ptpython/style.py 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 From 030790f8fb8da7736cc91a76712c99f230d1ebe1 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 22:02:03 +0000 Subject: [PATCH 68/83] Add typos to workflow. --- .github/workflows/test.yaml | 6 ++++++ pyproject.toml | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 457a4e48..6d2877b3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,13 +21,19 @@ jobs: run: | uvx --with . mypy src/ptpython - name: Code formatting + if: ${{ matrix.python-version == '3.13' }} run: | uvx ruff check . uvx ruff format --check . + - name: Typos + if: ${{ matrix.python-version == '3.13' }} + run: | + 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 diff --git a/pyproject.toml b/pyproject.toml index 680d7087..72259863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,10 @@ lint.ignore = [ 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"] From fb4949ad52ce7d603ab5bb52fba572c6dfdaad0b Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 10 Apr 2025 22:21:44 +0000 Subject: [PATCH 69/83] Typecheck examples. --- .github/workflows/test.yaml | 3 ++- examples/ssh-and-telnet-embed.py | 6 +++-- src/ptpython/repl.py | 43 ++++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6d2877b3..d53bfcc1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Type Checking run: | - uvx --with . mypy src/ptpython + uvx --with . mypy src/ptpython/ + uvx --with . mypy examples/ - name: Code formatting if: ${{ matrix.python-version == '3.13' }} run: | 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/src/ptpython/repl.py b/src/ptpython/repl.py index ba6717fb..469ed694 100644 --- a/src/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 @@ -505,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( globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None, @@ -515,7 +553,7 @@ def embed( startup_paths: Sequence[str | Path] | None = None, patch_stdout: 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`. :: @@ -577,3 +615,4 @@ async def coroutine() -> None: else: with patch_context: repl.run() + return None From 836431ff6775aac2c2e3aafa3295b259ebe99d0a Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Tue, 15 Apr 2025 09:24:02 +0000 Subject: [PATCH 70/83] Release 3.0.30 --- CHANGELOG | 11 +++++++++++ pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bef7d07f..7706260d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ 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 ------------------ diff --git a/pyproject.toml b/pyproject.toml index 72259863..00e2d5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ptpython" -version = "3.0.29" +version = "3.0.30" description = "Python REPL build on top of prompt_toolkit" readme = "README.rst" authors = [{ name = "Jonathan Slenders" }] From c22b59d21147d36560a2ff802c8ba1876797e208 Mon Sep 17 00:00:00 2001 From: PEMessage <1165739182@qq.com> Date: Thu, 26 Jun 2025 00:20:27 +0800 Subject: [PATCH 71/83] Fix repl.min_brightness and repl.max_brightness config not work in history --- src/ptpython/history_browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ptpython/history_browser.py b/src/ptpython/history_browser.py index 72bc576d..101a6c5c 100644 --- a/src/ptpython/history_browser.py +++ b/src/ptpython/history_browser.py @@ -643,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), ) From 3767d50ff330f7847cc1695ab41de6b80c444fff Mon Sep 17 00:00:00 2001 From: Shengchen Zhang Date: Fri, 27 Jun 2025 20:47:18 +0800 Subject: [PATCH 72/83] Expose `raw` parameter from prompt-toolkit to allow escape sequences to print normally. --- src/ptpython/repl.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ptpython/repl.py b/src/ptpython/repl.py index 469ed694..90772689 100644 --- a/src/ptpython/repl.py +++ b/src/ptpython/repl.py @@ -525,6 +525,7 @@ def embed( title: str | None = ..., startup_paths: Sequence[str | Path] | None = ..., patch_stdout: bool = ..., + patch_stdout_raw: bool = ..., return_asyncio_coroutine: Literal[False] = ..., ) -> None: ... @@ -539,6 +540,7 @@ def embed( 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]: ... @@ -552,6 +554,7 @@ 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 | Coroutine[Any, Any, None]: """ @@ -567,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: @@ -602,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: From a14c329147f498fd0ce28240a16de8620d454180 Mon Sep 17 00:00:00 2001 From: Shengchen Zhang Date: Tue, 8 Jul 2025 16:44:27 +0100 Subject: [PATCH 73/83] Update dead image links in README.rst --- README.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From c49bf9b05a204fd8ed296698c5a6fe60b712dc1f Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 27 Aug 2025 11:30:30 +0000 Subject: [PATCH 74/83] Fix positioning of exit confirmation (compatibilitiy with latest prompt_toolkit). --- src/ptpython/layout.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ptpython/layout.py b/src/ptpython/layout.py index 9768598e..8d030dd1 100644 --- a/src/ptpython/layout.py +++ b/src/ptpython/layout.py @@ -694,7 +694,8 @@ def menu_position() -> int | None: ), Float( left=2, - bottom=1, + top=2, + height=3, content=self.exit_confirmation, ), Float( From b3959fe513d8b3fa0306ce0599347b779d736c07 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 27 Aug 2025 11:36:57 +0000 Subject: [PATCH 75/83] Upgrade ruff check to Python 3.8. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 00e2d5f8..9912c736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ strict_optional = true [tool.ruff] -target-version = "py37" +target-version = "py38" lint.select = [ "E", # pycodestyle errors "W", # pycodestyle warnings From 0066c7ee392d46103b8bd2968c7a645a4c04f02d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Wed, 27 Aug 2025 15:29:12 +0000 Subject: [PATCH 76/83] Release 3.0.31 --- CHANGELOG | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7706260d..8a07ede2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +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 (compatibilitiy with latest prompt_toolkit). + + 3.0.30: 2025-04-15 ------------------ diff --git a/pyproject.toml b/pyproject.toml index 9912c736..f1f3a853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ptpython" -version = "3.0.30" +version = "3.0.31" description = "Python REPL build on top of prompt_toolkit" readme = "README.rst" authors = [{ name = "Jonathan Slenders" }] From d29f20d996e301db1ce12cfd77cfa50a9c128155 Mon Sep 17 00:00:00 2001 From: tapple-cisco <167885465+tapple-cisco@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:01:39 -0700 Subject: [PATCH 77/83] Note in config example where the file goes on Windows --- examples/ptpython_config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index bfd3914e..4efd5d97 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -4,6 +4,7 @@ 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 From 86c6d1168a8f020f442309cd5bc3023ed6f0764d Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 8 Nov 2025 22:09:54 +0000 Subject: [PATCH 78/83] Use ANSI colors for the default theme. --- src/ptpython/python_input.py | 2 +- src/ptpython/style.py | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/ptpython/python_input.py b/src/ptpython/python_input.py index b1773643..7e218eb9 100644 --- a/src/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/src/ptpython/style.py b/src/ptpython/style.py index c5a04e58..85219717 100644 --- a/src/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 = { From ed597e1945535e4d84a2c628ba6e470917e883dc Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Mon, 17 Nov 2025 13:59:26 +0000 Subject: [PATCH 79/83] Fix string escaping error in config example. --- examples/ptpython_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py index 4efd5d97..fe8a9ae8 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -1,4 +1,4 @@ -""" +r""" Configuration example for ``ptpython``. Copy this file to $XDG_CONFIG_HOME/ptpython/config.py From 73a12553640280d01d576c080c589ec0fd099f05 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Thu, 20 Nov 2025 21:03:24 +0000 Subject: [PATCH 80/83] Release 3.0.32. --- CHANGELOG | 12 +++++++++++- pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8a07ede2..838303f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,13 +1,23 @@ 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 (compatibilitiy with latest prompt_toolkit). +- Fix positioning of exit confirmation (compatibility with latest prompt_toolkit). 3.0.30: 2025-04-15 diff --git a/pyproject.toml b/pyproject.toml index f1f3a853..cb6d1ed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ptpython" -version = "3.0.31" +version = "3.0.32" description = "Python REPL build on top of prompt_toolkit" readme = "README.rst" authors = [{ name = "Jonathan Slenders" }] From 8fa5ddb3eb7842fd54484bac74b091c00fb804d5 Mon Sep 17 00:00:00 2001 From: Prescott Murphy Date: Mon, 22 Jul 2024 18:59:29 -0400 Subject: [PATCH 81/83] Pass style with meta completion text --- src/ptpython/completer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ptpython/completer.py b/src/ptpython/completer.py index e8bab285..736756ab 100644 --- a/src/ptpython/completer.py +++ b/src/ptpython/completer.py @@ -285,12 +285,15 @@ def get_completions( if jc.type == "param": suffix = "..." + style = _get_style_for_jedi_completion(jc) + display_meta = jc.type if style == "" else [(style, 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, ) From bed05014c34a01c41233481dea07213ac9dca80d Mon Sep 17 00:00:00 2001 From: Prescott Murphy Date: Mon, 22 Jul 2024 19:29:59 -0400 Subject: [PATCH 82/83] Use distinct style to make feature completely opt-in --- src/ptpython/completer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ptpython/completer.py b/src/ptpython/completer.py index 736756ab..40701cab 100644 --- a/src/ptpython/completer.py +++ b/src/ptpython/completer.py @@ -286,7 +286,9 @@ def get_completions( suffix = "..." style = _get_style_for_jedi_completion(jc) - display_meta = jc.type if style == "" else [(style, jc.type)] + display_meta = ( + jc.type if style == "" else [(f"{style}-meta", jc.type)] + ) yield Completion( jc.name_with_symbols, From dfe9dae32df17f6695aad6168ec271d02eb1232e Mon Sep 17 00:00:00 2001 From: Prescott Murphy Date: Thu, 20 Nov 2025 22:08:27 -0500 Subject: [PATCH 83/83] Add display meta styling classes to default style --- src/ptpython/style.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ptpython/style.py b/src/ptpython/style.py index 85219717..8242df31 100644 --- a/src/ptpython/style.py +++ b/src/ptpython/style.py @@ -137,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