From e0719babdc8744144449c81fac14ba7bbf21446f Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 27 Feb 2023 11:40:00 +0100 Subject: Merging upstream version 3.0.23. Signed-off-by: Daniel Baumann --- .github/workflows/test.yaml | 2 +- CHANGELOG | 11 ++++ README.rst | 2 +- docs/concurrency-challenges.rst | 2 +- examples/ptpython_config/config.py | 3 +- ptpython/__init__.py | 2 + ptpython/__main__.py | 2 + ptpython/completer.py | 51 +++++++++--------- 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 | 34 ++++++------ ptpython/ipython.py | 6 ++- ptpython/key_bindings.py | 10 ++-- ptpython/layout.py | 48 +++++++++-------- ptpython/lexer.py | 4 +- ptpython/prompt_style.py | 4 +- ptpython/python_input.py | 98 +++++++++++++++++++++++----------- ptpython/repl.py | 27 +++++----- ptpython/signatures.py | 24 ++++----- ptpython/style.py | 8 +-- ptpython/utils.py | 8 +-- ptpython/validator.py | 4 +- setup.py | 10 ++-- tests/run_tests.py | 2 + 27 files changed, 230 insertions(+), 152 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7ec8662..31837db 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 diff --git a/CHANGELOG b/CHANGELOG index 916a542..645ca60 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/README.rst b/README.rst index 15464ba..2db3f69 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()) diff --git a/docs/concurrency-challenges.rst b/docs/concurrency-challenges.rst index b56d969..0ff9c6c 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 bf9d05f..2f3f49d 100644 --- a/examples/ptpython_config/config.py +++ b/examples/ptpython_config/config.py @@ -3,6 +3,7 @@ Configuration example for ``ptpython``. Copy this file to $XDG_CONFIG_HOME/ptpython/config.py On Linux, this is: ~/.config/ptpython/config.py +On macOS, this is: ~/Library/Application Support/ptpython/config.py """ from prompt_toolkit.filters import ViInsertMode from prompt_toolkit.key_binding.key_processor import KeyPress @@ -49,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/__init__.py b/ptpython/__init__.py index 4908eba..63c6233 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 83340a7..c006261 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 9252106..f28d2b1 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 @@ class PythonCompleter(Completer): 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 @@ class PythonCompleter(Completer): 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 @@ class PythonCompleter(Completer): 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) @@ -189,7 +191,6 @@ class PythonCompleter(Completer): ): # 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 @@ -203,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__() @@ -242,7 +243,7 @@ class JediCompleter(Completer): # 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 @@ -303,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__() @@ -386,7 +387,7 @@ class DictionaryCompleter(Completer): 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. @@ -399,7 +400,6 @@ class DictionaryCompleter(Completer): 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. @@ -431,7 +431,7 @@ class DictionaryCompleter(Completer): 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 """ @@ -446,7 +446,7 @@ class DictionaryCompleter(Completer): 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. @@ -454,7 +454,6 @@ class DictionaryCompleter(Completer): result = self.eval_expression(document, temp_locals) if result is not None: - if isinstance( result, (list, tuple, dict, collections_abc.Mapping, collections_abc.Sequence), @@ -470,7 +469,7 @@ class DictionaryCompleter(Completer): self, document: Document, complete_event: CompleteEvent, - temp_locals: Dict[str, Any], + temp_locals: dict[str, Any], ) -> Iterable[Completion]: """ Complete dictionary keys. @@ -478,6 +477,7 @@ class DictionaryCompleter(Completer): 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 @@ -549,7 +549,7 @@ class DictionaryCompleter(Completer): self, document: Document, complete_event: CompleteEvent, - temp_locals: Dict[str, Any], + temp_locals: dict[str, Any], ) -> Iterable[Completion]: """ Complete attribute names. @@ -568,9 +568,9 @@ class DictionaryCompleter(Completer): 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 @@ -581,13 +581,13 @@ class DictionaryCompleter(Completer): 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("_"): @@ -599,7 +599,7 @@ class DictionaryCompleter(Completer): 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 @@ -617,7 +617,6 @@ class HidePrivateCompleter(Completer): 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 @@ -653,7 +652,7 @@ except ImportError: # Python 2. 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 4c36217..0347ade 100644 --- a/ptpython/contrib/asyncssh_repl.py +++ b/ptpython/contrib/asyncssh_repl.py @@ -6,6 +6,8 @@ Note that the code in this file is Python 3 only. However, we should make sure not to use Python 3-only syntax, because this package should be installable in Python 2 as well! """ +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 21d7063..b660a0a 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 edffa44..1b4074d 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -21,6 +21,8 @@ environment variables: 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 @@ __all__ = ["create_parser", "get_config_and_history_file", "run"] 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 63dd740..14ab64b 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -7,6 +7,8 @@ way we don't block the UI of for instance ``turtle`` and other Tk libraries. 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 be85edf..a2079fd 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 @@ __all__ = ["HasSignature", "ShowSidebar", "ShowSignature", "ShowDocstring"] 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 08725ee..eea81c2 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -4,6 +4,8 @@ Utility to easily select lines from the history and execute them again. `create_history_application` creates an `Application` instance that runs will run as a sub application of the Repl/PythonInput. """ +from __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 @@ class HistoryMapping: 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 @@ class HistoryMapping: 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 @@ class HistoryMapping: 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,14 +594,12 @@ def create_key_bindings( 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`. - 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/ipython.py b/ptpython/ipython.py index db2a204..fb4b5ed 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,8 @@ also the power of for instance all the %-magic functions that IPython has to offer. """ +from __future__ import annotations + from typing import Iterable from warnings import warn @@ -62,12 +64,12 @@ class IPythonPrompt(PromptStyle): 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 147a321..d7bb575 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. """ @@ -157,7 +159,7 @@ def load_python_bindings(python_input: "PythonInput") -> KeyBindings: 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 @@ -218,7 +220,7 @@ def load_python_bindings(python_input: "PythonInput") -> KeyBindings: 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 load_sidebar_bindings(python_input: "PythonInput") -> KeyBindings: 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 365f381..d15e52e 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", " "), @@ -149,7 +151,7 @@ def python_sidebar(python_input: "PythonInput") -> Window: 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. @@ -172,7 +174,7 @@ def python_sidebar(python_input: "PythonInput") -> Window: ) -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 python_sidebar_navigation(python_input: "PythonInput") -> Window: ) -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 python_sidebar_help(python_input: "PythonInput") -> Container: ) -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 @@ class PythonPromptMargin(PromptMargin): 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 status_bar(python_input: "PythonInput") -> Container: ) -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 get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples: 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 get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples: 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 show_sidebar_button_info(python_input: "PythonInput") -> Container: 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 create_exit_confirmation( ) -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 meta_enter_message(python_input: "PythonInput") -> Container: 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 @@ class PtPythonLayout: 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 62e470f..81924c9 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 e7334af..96b738f 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 c561117..da19076 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 @@ -34,6 +34,12 @@ from prompt_toolkit.completion import ( 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 @@ -84,6 +90,11 @@ from .style import generate_style, get_all_code_styles, get_all_ui_styles 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 +112,7 @@ _T = TypeVar("_T", bound="_SupportsLessThan") 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,26 +205,25 @@ 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: - self.get_globals: _GetNamespace = get_globals or (lambda: {}) self.get_locals: _GetNamespace = get_locals or self.get_globals @@ -310,7 +320,7 @@ class PythonInput: 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.) @@ -321,11 +331,23 @@ class PythonInput: 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. # Styles selectable from the menu. - self.all_prompt_styles: Dict[str, PromptStyle] = { + self.all_prompt_styles: dict[str, PromptStyle] = { "ipython": IPythonPrompt(self), "classic": ClassicPrompt(), } @@ -339,7 +361,7 @@ class PythonInput: ].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" @@ -361,7 +383,7 @@ class PythonInput: 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.) @@ -400,9 +422,7 @@ class PythonInput: # Create an app if requested. If not, the global get_app() is returned # for self.app via property getter. if create_app: - self._app: Optional[Application[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: @@ -528,7 +548,7 @@ class PythonInput: 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. """ @@ -547,14 +567,14 @@ class PythonInput: 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), @@ -582,6 +602,16 @@ class PythonInput: "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.", @@ -731,10 +761,10 @@ class PythonInput: 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", @@ -826,10 +856,10 @@ class PythonInput: 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", @@ -863,7 +893,7 @@ class PythonInput: ] def _create_application( - self, input: Optional[Input], output: Optional[Output] + self, input: Input | None, output: Output | None ) -> Application[str]: """ Create an `Application` instance. @@ -894,6 +924,9 @@ class PythonInput: 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, ) @@ -953,7 +986,7 @@ class PythonInput: 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. @@ -1043,6 +1076,7 @@ class PythonInput: 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 3c729c0..02a5075 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -7,6 +7,8 @@ Utility for creating a Python repl. embed(globals(), locals(), vi_mode=False) """ +from __future__ import annotations + import asyncio import builtins import os @@ -53,7 +55,7 @@ except ImportError: __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,14 +91,15 @@ class PythonRepl(PythonInput): 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: # 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 @@ -299,7 +302,7 @@ class PythonRepl(PythonInput): 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: @@ -402,7 +405,7 @@ class PythonRepl(PythonInput): 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) @@ -523,7 +526,7 @@ class PythonRepl(PythonInput): flush_page() - def create_pager_prompt(self) -> PromptSession["PagerResult"]: + def create_pager_prompt(self) -> PromptSession[PagerResult]: """ Create pager --MORE-- prompt. """ @@ -572,8 +575,6 @@ class PythonRepl(PythonInput): include_default_pygments_style=False, output=output, ) - - output.write("%s\n" % e) output.flush() def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None: @@ -652,7 +653,7 @@ def run_config( # 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") @@ -671,10 +672,10 @@ def run_config( 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 e836d33..5a6f286 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -5,6 +5,8 @@ editing. 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 @@ class Signature: 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 @@ class Signature: docstring: str, signature: InspectSignature, index: int, - ) -> "Signature": + ) -> Signature: parameters = [] def get_annotation_name(annotation: object) -> str: @@ -123,9 +125,7 @@ class Signature: ) @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 @@ class Signature: 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 4b54d0c..199d5ab 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 @@ from pygments.styles import get_all_styles, get_style_by_name __all__ = ["get_all_code_styles", "get_all_ui_styles", "generate_style"] -def get_all_code_styles() -> Dict[str, BaseStyle]: +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 ef96ca4..5348899 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 ffac583..3b36d27 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 2725dac..18d2911 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f: 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, @@ -21,14 +21,14 @@ setup( "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.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 2f94516..0de3743 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 -- cgit v1.2.3