From 50dfafe3a01b634bfa80101fc199d3d451ff7d0f Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 2 Dec 2022 10:11:15 +0100 Subject: Adding upstream version 3.0.21. Signed-off-by: Daniel Baumann --- ptpython/completer.py | 29 +++++--- ptpython/entry_points/run_ptpython.py | 12 ++-- ptpython/eventloop.py | 14 ++-- ptpython/filters.py | 1 + ptpython/history_browser.py | 122 ++++++++++++++++++++++------------ ptpython/ipython.py | 46 ++++++++++--- ptpython/key_bindings.py | 61 +++++++++-------- ptpython/layout.py | 64 ++++++++++-------- ptpython/py.typed | 0 ptpython/python_input.py | 52 ++++++++++----- ptpython/repl.py | 15 +++-- ptpython/signatures.py | 9 ++- ptpython/utils.py | 36 ++++++++-- ptpython/validator.py | 9 ++- 14 files changed, 308 insertions(+), 162 deletions(-) create mode 100644 ptpython/py.typed (limited to 'ptpython') diff --git a/ptpython/completer.py b/ptpython/completer.py index 51a4086..2b6795d 100644 --- a/ptpython/completer.py +++ b/ptpython/completer.py @@ -4,7 +4,7 @@ import inspect import keyword import re from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple from prompt_toolkit.completion import ( CompleteEvent, @@ -21,6 +21,7 @@ from prompt_toolkit.formatted_text import fragment_list_to_text, to_formatted_te from ptpython.utils import get_jedi_script_from_document if TYPE_CHECKING: + import jedi.api.classes from prompt_toolkit.contrib.regular_languages.compiler import _CompiledGrammar __all__ = ["PythonCompleter", "CompletePrivateAttributes", "HidePrivateCompleter"] @@ -43,8 +44,8 @@ class PythonCompleter(Completer): def __init__( self, - get_globals: Callable[[], dict], - get_locals: Callable[[], dict], + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], enable_dictionary_completion: Callable[[], bool], ) -> None: super().__init__() @@ -200,7 +201,11 @@ class JediCompleter(Completer): Autocompleter that uses the Jedi library. """ - def __init__(self, get_globals, get_locals) -> None: + def __init__( + self, + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], + ) -> None: super().__init__() self.get_globals = get_globals @@ -296,7 +301,11 @@ class DictionaryCompleter(Completer): function calls, so it only triggers attribute access. """ - def __init__(self, get_globals, get_locals): + def __init__( + self, + get_globals: Callable[[], Dict[str, Any]], + get_locals: Callable[[], Dict[str, Any]], + ) -> None: super().__init__() self.get_globals = get_globals @@ -495,7 +504,7 @@ class DictionaryCompleter(Completer): else: break - for k in result: + for k, v in result.items(): if str(k).startswith(str(key_obj)): try: k_repr = self._do_repr(k) @@ -503,7 +512,7 @@ class DictionaryCompleter(Completer): k_repr + "]", -len(key), display=f"[{k_repr}]", - display_meta=abbr_meta(self._do_repr(result[k])), + display_meta=abbr_meta(self._do_repr(v)), ) except KeyError: # `result[k]` lookup failed. Trying to complete @@ -574,7 +583,7 @@ class DictionaryCompleter(Completer): underscore names to the end. """ - def sort_key(name: str): + def sort_key(name: str) -> Tuple[int, str]: if name.startswith("__"): return (2, name) # Double underscore comes latest. if name.startswith("_"): @@ -639,7 +648,9 @@ except ImportError: # Python 2. _builtin_names = [] -def _get_style_for_jedi_completion(jedi_completion) -> str: +def _get_style_for_jedi_completion( + jedi_completion: "jedi.api.classes.Completion", +) -> str: """ Return completion style to use for this name. """ diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py index 5ebe2b9..edffa44 100644 --- a/ptpython/entry_points/run_ptpython.py +++ b/ptpython/entry_points/run_ptpython.py @@ -26,16 +26,16 @@ import os import pathlib import sys from textwrap import dedent -from typing import Tuple +from typing import IO, Optional, Tuple import appdirs from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import print_formatted_text -from ptpython.repl import embed, enable_deprecation_warnings, run_config +from ptpython.repl import PythonRepl, embed, enable_deprecation_warnings, run_config try: - from importlib import metadata + from importlib import metadata # type: ignore except ImportError: import importlib_metadata as metadata # type: ignore @@ -44,7 +44,7 @@ __all__ = ["create_parser", "get_config_and_history_file", "run"] class _Parser(argparse.ArgumentParser): - def print_help(self): + def print_help(self, file: Optional[IO[str]] = None) -> None: super().print_help() print( dedent( @@ -84,7 +84,7 @@ def create_parser() -> _Parser: "-V", "--version", action="version", - version=metadata.version("ptpython"), # type: ignore + version=metadata.version("ptpython"), ) parser.add_argument("args", nargs="*", help="Script and arguments") return parser @@ -190,7 +190,7 @@ def run() -> None: enable_deprecation_warnings() # Apply config file - def configure(repl) -> None: + def configure(repl: PythonRepl) -> None: if os.path.exists(config_file): run_config(repl, config_file) diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py index c841972..63dd740 100644 --- a/ptpython/eventloop.py +++ b/ptpython/eventloop.py @@ -10,10 +10,12 @@ will fix it for Tk.) import sys import time +from prompt_toolkit.eventloop import InputHookContext + __all__ = ["inputhook"] -def _inputhook_tk(inputhook_context): +def _inputhook_tk(inputhook_context: InputHookContext) -> None: """ Inputhook for Tk. Run the Tk eventloop until prompt-toolkit needs to process the next input. @@ -23,9 +25,9 @@ def _inputhook_tk(inputhook_context): import _tkinter # Keep this imports inline! - root = tkinter._default_root + root = tkinter._default_root # type: ignore - def wait_using_filehandler(): + def wait_using_filehandler() -> None: """ Run the TK eventloop until the file handler that we got from the inputhook becomes readable. @@ -34,7 +36,7 @@ def _inputhook_tk(inputhook_context): # to process. stop = [False] - def done(*a): + def done(*a: object) -> None: stop[0] = True root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done) @@ -46,7 +48,7 @@ def _inputhook_tk(inputhook_context): root.deletefilehandler(inputhook_context.fileno()) - def wait_using_polling(): + def wait_using_polling() -> None: """ Windows TK doesn't support 'createfilehandler'. So, run the TK eventloop and poll until input is ready. @@ -65,7 +67,7 @@ def _inputhook_tk(inputhook_context): wait_using_polling() -def inputhook(inputhook_context): +def inputhook(inputhook_context: InputHookContext) -> None: # Only call the real input hook when the 'Tkinter' library was loaded. if "Tkinter" in sys.modules or "tkinter" in sys.modules: _inputhook_tk(inputhook_context) diff --git a/ptpython/filters.py b/ptpython/filters.py index 1adac13..be85edf 100644 --- a/ptpython/filters.py +++ b/ptpython/filters.py @@ -10,6 +10,7 @@ __all__ = ["HasSignature", "ShowSidebar", "ShowSignature", "ShowDocstring"] class PythonInputFilter(Filter): def __init__(self, python_input: "PythonInput") -> None: + super().__init__() self.python_input = python_input def __call__(self) -> bool: diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py index b7fe086..08725ee 100644 --- a/ptpython/history_browser.py +++ b/ptpython/history_browser.py @@ -5,6 +5,7 @@ Utility to easily select lines from the history and execute them again. run as a sub application of the Repl/PythonInput. """ from functools import partial +from typing import TYPE_CHECKING, Callable, List, Optional, Set from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app @@ -12,8 +13,11 @@ from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition, has_focus +from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.formatted_text.utils import fragment_list_to_text +from prompt_toolkit.history import History from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.layout.containers import ( ConditionalContainer, Container, @@ -24,13 +28,23 @@ from prompt_toolkit.layout.containers import ( VSplit, Window, WindowAlign, + WindowRenderInfo, +) +from prompt_toolkit.layout.controls import ( + BufferControl, + FormattedTextControl, + UIContent, ) -from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.dimension import Dimension as D from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.margins import Margin, ScrollbarMargin -from prompt_toolkit.layout.processors import Processor, Transformation +from prompt_toolkit.layout.processors import ( + Processor, + Transformation, + TransformationInput, +) from prompt_toolkit.lexers import PygmentsLexer +from prompt_toolkit.mouse_events import MouseEvent from prompt_toolkit.widgets import Frame from prompt_toolkit.widgets.toolbars import ArgToolbar, SearchToolbar from pygments.lexers import Python3Lexer as PythonLexer @@ -40,10 +54,15 @@ from ptpython.layout import get_inputmode_fragments from .utils import if_mousedown +if TYPE_CHECKING: + from .python_input import PythonInput + HISTORY_COUNT = 2000 __all__ = ["HistoryLayout", "PythonHistory"] +E = KeyPressEvent + HELP_TEXT = """ This interface is meant to select multiple lines from the history and execute them together. @@ -109,7 +128,7 @@ class HistoryLayout: application. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: search_toolbar = SearchToolbar() self.help_buffer_control = BufferControl( @@ -201,19 +220,19 @@ class HistoryLayout: self.layout = Layout(self.root_container, history_window) -def _get_top_toolbar_fragments(): +def _get_top_toolbar_fragments() -> StyleAndTextTuples: return [("class:status-bar.title", "History browser - Insert from history")] -def _get_bottom_toolbar_fragments(history): +def _get_bottom_toolbar_fragments(history: "PythonHistory") -> StyleAndTextTuples: python_input = history.python_input @if_mousedown - def f1(mouse_event): + def f1(mouse_event: MouseEvent) -> None: _toggle_help(history) @if_mousedown - def tab(mouse_event): + def tab(mouse_event: MouseEvent) -> None: _select_other_window(history) return ( @@ -239,14 +258,16 @@ class HistoryMargin(Margin): This displays a green bar for the selected entries. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: self.history_buffer = history.history_buffer self.history_mapping = history.history_mapping - def get_width(self, ui_content): + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: return 2 - def create_margin(self, window_render_info, width, height): + def create_margin( + self, window_render_info: WindowRenderInfo, width: int, height: int + ) -> StyleAndTextTuples: document = self.history_buffer.document lines_starting_new_entries = self.history_mapping.lines_starting_new_entries @@ -255,7 +276,7 @@ class HistoryMargin(Margin): current_lineno = document.cursor_position_row visible_line_to_input_line = window_render_info.visible_line_to_input_line - result = [] + result: StyleAndTextTuples = [] for y in range(height): line_number = visible_line_to_input_line.get(y) @@ -286,14 +307,16 @@ class ResultMargin(Margin): The margin to be shown in the result pane. """ - def __init__(self, history): + def __init__(self, history: "PythonHistory") -> None: self.history_mapping = history.history_mapping self.history_buffer = history.history_buffer - def get_width(self, ui_content): + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: return 2 - def create_margin(self, window_render_info, width, height): + def create_margin( + self, window_render_info: WindowRenderInfo, width: int, height: int + ) -> StyleAndTextTuples: document = self.history_buffer.document current_lineno = document.cursor_position_row @@ -303,7 +326,7 @@ class ResultMargin(Margin): visible_line_to_input_line = window_render_info.visible_line_to_input_line - result = [] + result: StyleAndTextTuples = [] for y in range(height): line_number = visible_line_to_input_line.get(y) @@ -324,7 +347,7 @@ class ResultMargin(Margin): return result - def invalidation_hash(self, document): + def invalidation_hash(self, document: Document) -> int: return document.cursor_position_row @@ -333,13 +356,15 @@ class GrayExistingText(Processor): Turn the existing input, before and after the inserted code gray. """ - def __init__(self, history_mapping): + def __init__(self, history_mapping: "HistoryMapping") -> None: self.history_mapping = history_mapping self._lines_before = len( history_mapping.original_document.text_before_cursor.splitlines() ) - def apply_transformation(self, transformation_input): + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: lineno = transformation_input.lineno fragments = transformation_input.fragments @@ -357,17 +382,22 @@ class HistoryMapping: Keep a list of all the lines from the history and the selected lines. """ - def __init__(self, history, python_history, original_document): + def __init__( + self, + history: "PythonHistory", + python_history: History, + original_document: Document, + ) -> None: self.history = history self.python_history = python_history self.original_document = original_document self.lines_starting_new_entries = set() - self.selected_lines = set() + self.selected_lines: Set[int] = set() # Process history. history_strings = python_history.get_strings() - history_lines = [] + history_lines: List[str] = [] for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]: self.lines_starting_new_entries.add(len(history_lines)) @@ -389,7 +419,7 @@ class HistoryMapping: else: self.result_line_offset = 0 - def get_new_document(self, cursor_pos=None): + def get_new_document(self, cursor_pos: Optional[int] = None) -> Document: """ Create a `Document` instance that contains the resulting text. """ @@ -413,13 +443,13 @@ class HistoryMapping: cursor_pos = len(text) return Document(text, cursor_pos) - def update_default_buffer(self): + def update_default_buffer(self) -> None: b = self.history.default_buffer b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True) -def _toggle_help(history): +def _toggle_help(history: "PythonHistory") -> None: "Display/hide help." help_buffer_control = history.history_layout.help_buffer_control @@ -429,7 +459,7 @@ def _toggle_help(history): history.app.layout.current_control = help_buffer_control -def _select_other_window(history): +def _select_other_window(history: "PythonHistory") -> None: "Toggle focus between left/right window." current_buffer = history.app.current_buffer layout = history.history_layout.layout @@ -441,7 +471,11 @@ def _select_other_window(history): layout.current_control = history.history_layout.history_buffer_control -def create_key_bindings(history, python_input, history_mapping): +def create_key_bindings( + history: "PythonHistory", + python_input: "PythonInput", + history_mapping: HistoryMapping, +) -> KeyBindings: """ Key bindings. """ @@ -449,7 +483,7 @@ def create_key_bindings(history, python_input, history_mapping): handle = bindings.add @handle(" ", filter=has_focus(history.history_buffer)) - def _(event): + def _(event: E) -> None: """ Space: select/deselect line from history pane. """ @@ -486,7 +520,7 @@ def create_key_bindings(history, python_input, history_mapping): @handle(" ", filter=has_focus(DEFAULT_BUFFER)) @handle("delete", filter=has_focus(DEFAULT_BUFFER)) @handle("c-h", filter=has_focus(DEFAULT_BUFFER)) - def _(event): + def _(event: E) -> None: """ Space: remove line from default pane. """ @@ -512,17 +546,17 @@ def create_key_bindings(history, python_input, history_mapping): @handle("c-x", filter=main_buffer_focussed, eager=True) # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding. @handle("c-w", filter=main_buffer_focussed) - def _(event): + def _(event: E) -> None: "Select other window." _select_other_window(history) @handle("f4") - def _(event): + def _(event: E) -> None: "Switch between Emacs/Vi mode." python_input.vi_mode = not python_input.vi_mode @handle("f1") - def _(event): + def _(event: E) -> None: "Display/hide help." _toggle_help(history) @@ -530,7 +564,7 @@ def create_key_bindings(history, python_input, history_mapping): @handle("c-c", filter=help_focussed) @handle("c-g", filter=help_focussed) @handle("escape", filter=help_focussed) - def _(event): + def _(event: E) -> None: "Leave help." event.app.layout.focus_previous() @@ -538,19 +572,19 @@ def create_key_bindings(history, python_input, history_mapping): @handle("f3", filter=main_buffer_focussed) @handle("c-c", filter=main_buffer_focussed) @handle("c-g", filter=main_buffer_focussed) - def _(event): + def _(event: E) -> None: "Cancel and go back." event.app.exit(result=None) @handle("enter", filter=main_buffer_focussed) - def _(event): + def _(event: E) -> None: "Accept input." event.app.exit(result=history.default_buffer.text) enable_system_bindings = Condition(lambda: python_input.enable_system_bindings) @handle("c-z", filter=enable_system_bindings) - def _(event): + def _(event: E) -> None: "Suspend to background." event.app.suspend_to_background() @@ -558,7 +592,9 @@ def create_key_bindings(history, python_input, history_mapping): class PythonHistory: - def __init__(self, python_input, original_document): + def __init__( + self, python_input: "PythonInput", original_document: Document + ) -> None: """ Create an `Application` for the history screen. This has to be run as a sub application of `python_input`. @@ -577,12 +613,14 @@ class PythonHistory: + document.get_start_of_line_position(), ) + def accept_handler(buffer: Buffer) -> bool: + get_app().exit(result=self.default_buffer.text) + return False + self.history_buffer = Buffer( document=document, on_cursor_position_changed=self._history_buffer_pos_changed, - accept_handler=( - lambda buff: get_app().exit(result=self.default_buffer.text) - ), + accept_handler=accept_handler, read_only=True, ) @@ -597,7 +635,7 @@ class PythonHistory: self.history_layout = HistoryLayout(self) - self.app = Application( + self.app: Application[str] = Application( layout=self.history_layout.layout, full_screen=True, style=python_input._current_style, @@ -605,7 +643,7 @@ class PythonHistory: key_bindings=create_key_bindings(self, python_input, history_mapping), ) - def _default_buffer_pos_changed(self, _): + def _default_buffer_pos_changed(self, _: Buffer) -> None: """When the cursor changes in the default buffer. Synchronize with history buffer.""" # Only when this buffer has the focus. @@ -629,7 +667,7 @@ class PythonHistory: ) ) - def _history_buffer_pos_changed(self, _): + def _history_buffer_pos_changed(self, _: Buffer) -> None: """When the cursor changes in the history buffer. Synchronize.""" # Only when this buffer has the focus. if self.app.current_buffer == self.history_buffer: diff --git a/ptpython/ipython.py b/ptpython/ipython.py index 9163334..db2a204 100644 --- a/ptpython/ipython.py +++ b/ptpython/ipython.py @@ -8,6 +8,7 @@ also the power of for instance all the %-magic functions that IPython has to offer. """ +from typing import Iterable from warnings import warn from IPython import utils as ipy_utils @@ -15,6 +16,7 @@ from IPython.core.inputsplitter import IPythonInputSplitter from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed from IPython.terminal.ipapp import load_default_config from prompt_toolkit.completion import ( + CompleteEvent, Completer, Completion, PathCompleter, @@ -25,15 +27,17 @@ from prompt_toolkit.contrib.regular_languages.compiler import compile from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer from prompt_toolkit.document import Document -from prompt_toolkit.formatted_text import PygmentsTokens +from prompt_toolkit.formatted_text import AnyFormattedText, PygmentsTokens from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer from prompt_toolkit.styles import Style from pygments.lexers import BashLexer, PythonLexer from ptpython.prompt_style import PromptStyle -from .python_input import PythonCompleter, PythonInput, PythonValidator +from .completer import PythonCompleter +from .python_input import PythonInput from .style import default_ui_style +from .validator import PythonValidator __all__ = ["embed"] @@ -46,13 +50,13 @@ class IPythonPrompt(PromptStyle): def __init__(self, prompts): self.prompts = prompts - def in_prompt(self): + def in_prompt(self) -> AnyFormattedText: return PygmentsTokens(self.prompts.in_prompt_tokens()) - def in2_prompt(self, width): + def in2_prompt(self, width: int) -> AnyFormattedText: return PygmentsTokens(self.prompts.continuation_prompt_tokens()) - def out_prompt(self): + def out_prompt(self) -> AnyFormattedText: return [] @@ -61,7 +65,7 @@ class IPythonValidator(PythonValidator): super(IPythonValidator, self).__init__(*args, **kwargs) self.isp = IPythonInputSplitter() - def validate(self, document): + def validate(self, document: Document) -> None: document = Document(text=self.isp.transform_cell(document.text)) super(IPythonValidator, self).validate(document) @@ -142,7 +146,9 @@ class MagicsCompleter(Completer): def __init__(self, magics_manager): self.magics_manager = magics_manager - def get_completions(self, document, complete_event): + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: text = document.text_before_cursor.lstrip() for m in sorted(self.magics_manager.magics["line"]): @@ -154,7 +160,9 @@ class AliasCompleter(Completer): def __init__(self, alias_manager): self.alias_manager = alias_manager - def get_completions(self, document, complete_event): + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: text = document.text_before_cursor.lstrip() # aliases = [a for a, _ in self.alias_manager.aliases] aliases = self.alias_manager.aliases @@ -240,7 +248,7 @@ class InteractiveShellEmbed(_InteractiveShellEmbed): self.python_input = python_input - def prompt_for_code(self): + def prompt_for_code(self) -> str: try: return self.python_input.app.run() except KeyboardInterrupt: @@ -269,6 +277,25 @@ def initialize_extensions(shell, extensions): shell.showtraceback() +def run_exec_lines(shell, exec_lines): + """ + Partial copy of run_exec_lines code from IPython.core.shellapp . + """ + try: + iter(exec_lines) + except TypeError: + pass + else: + try: + for line in exec_lines: + try: + shell.run_cell(line, store_history=False) + except: + shell.showtraceback() + except: + shell.showtraceback() + + def embed(**kwargs): """ Copied from `IPython/terminal/embed.py`, but using our `InteractiveShellEmbed` instead. @@ -282,6 +309,7 @@ def embed(**kwargs): kwargs["config"] = config shell = InteractiveShellEmbed.instance(**kwargs) initialize_extensions(shell, config["InteractiveShellApp"]["extensions"]) + run_exec_lines(shell, config["InteractiveShellApp"]["exec_lines"]) run_startup_scripts(shell) shell(header=header, stack_depth=2, compile_flags=compile_flags) diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py index ae23a3d..147a321 100644 --- a/ptpython/key_bindings.py +++ b/ptpython/key_bindings.py @@ -1,4 +1,7 @@ +from typing import TYPE_CHECKING + from prompt_toolkit.application import get_app +from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import ( @@ -11,19 +14,25 @@ from prompt_toolkit.filters import ( ) from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.named_commands import get_by_name +from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.keys import Keys from .utils import document_is_multiline_python +if TYPE_CHECKING: + from .python_input import PythonInput + __all__ = [ "load_python_bindings", "load_sidebar_bindings", "load_confirm_exit_bindings", ] +E = KeyPressEvent + @Condition -def tab_should_insert_whitespace(): +def tab_should_insert_whitespace() -> bool: """ When the 'tab' key is pressed with only whitespace character before the cursor, do autocompletion. Otherwise, insert indentation. @@ -38,7 +47,7 @@ def tab_should_insert_whitespace(): return bool(b.text and (not before_cursor or before_cursor.isspace())) -def load_python_bindings(python_input): +def load_python_bindings(python_input: "PythonInput") -> KeyBindings: """ Custom key bindings. """ @@ -48,14 +57,14 @@ def load_python_bindings(python_input): handle = bindings.add @handle("c-l") - def _(event): + def _(event: E) -> None: """ Clear whole screen and render again -- also when the sidebar is visible. """ event.app.renderer.clear() @handle("c-z") - def _(event): + def _(event: E) -> None: """ Suspend. """ @@ -67,7 +76,7 @@ def load_python_bindings(python_input): handle("c-w")(get_by_name("backward-kill-word")) @handle("f2") - def _(event): + def _(event: E) -> None: """ Show/hide sidebar. """ @@ -78,21 +87,21 @@ def load_python_bindings(python_input): event.app.layout.focus_last() @handle("f3") - def _(event): + def _(event: E) -> None: """ Select from the history. """ python_input.enter_history() @handle("f4") - def _(event): + def _(event: E) -> None: """ Toggle between Vi and Emacs mode. """ python_input.vi_mode = not python_input.vi_mode @handle("f6") - def _(event): + def _(event: E) -> None: """ Enable/Disable paste mode. """ @@ -101,14 +110,14 @@ def load_python_bindings(python_input): @handle( "tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace ) - def _(event): + def _(event: E) -> None: """ When tab should insert whitespace, do that instead of completion. """ event.app.current_buffer.insert_text(" ") @Condition - def is_multiline(): + def is_multiline() -> bool: return document_is_multiline_python(python_input.default_buffer.document) @handle( @@ -120,7 +129,7 @@ def load_python_bindings(python_input): & ~is_multiline, ) @handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode) - def _(event): + def _(event: E) -> None: """ Accept input (for single line input). """ @@ -143,7 +152,7 @@ def load_python_bindings(python_input): & has_focus(DEFAULT_BUFFER) & is_multiline, ) - def _(event): + def _(event: E) -> None: """ Behaviour of the Enter key. @@ -153,11 +162,11 @@ def load_python_bindings(python_input): b = event.current_buffer empty_lines_required = python_input.accept_input_on_enter or 10000 - def at_the_end(b): + def at_the_end(b: Buffer) -> bool: """we consider the cursor at the end when there is no text after the cursor, or only whitespace.""" text = b.document.text_after_cursor - return text == "" or (text.isspace() and not "\n" in text) + return text == "" or (text.isspace() and "\n" not in text) if python_input.paste_mode: # In paste mode, always insert text. @@ -187,7 +196,7 @@ def load_python_bindings(python_input): not get_app().current_buffer.text ), ) - def _(event): + def _(event: E) -> None: """ Override Control-D exit, to ask for confirmation. """ @@ -202,14 +211,14 @@ def load_python_bindings(python_input): event.app.exit(exception=EOFError) @handle("c-c", filter=has_focus(python_input.default_buffer)) - def _(event): + def _(event: E) -> None: "Abort when Control-C has been pressed." event.app.exit(exception=KeyboardInterrupt, style="class:aborting") return bindings -def load_sidebar_bindings(python_input): +def load_sidebar_bindings(python_input: "PythonInput") -> KeyBindings: """ Load bindings for the navigation in the sidebar. """ @@ -221,7 +230,7 @@ def load_sidebar_bindings(python_input): @handle("up", filter=sidebar_visible) @handle("c-p", filter=sidebar_visible) @handle("k", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Go to previous option." python_input.selected_option_index = ( python_input.selected_option_index - 1 @@ -230,7 +239,7 @@ def load_sidebar_bindings(python_input): @handle("down", filter=sidebar_visible) @handle("c-n", filter=sidebar_visible) @handle("j", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Go to next option." python_input.selected_option_index = ( python_input.selected_option_index + 1 @@ -239,14 +248,14 @@ def load_sidebar_bindings(python_input): @handle("right", filter=sidebar_visible) @handle("l", filter=sidebar_visible) @handle(" ", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Select next value for current option." option = python_input.selected_option option.activate_next() @handle("left", filter=sidebar_visible) @handle("h", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Select previous value for current option." option = python_input.selected_option option.activate_previous() @@ -256,7 +265,7 @@ def load_sidebar_bindings(python_input): @handle("c-d", filter=sidebar_visible) @handle("enter", filter=sidebar_visible) @handle("escape", filter=sidebar_visible) - def _(event): + def _(event: E) -> None: "Hide sidebar." python_input.show_sidebar = False event.app.layout.focus_last() @@ -264,7 +273,7 @@ def load_sidebar_bindings(python_input): return bindings -def load_confirm_exit_bindings(python_input): +def load_confirm_exit_bindings(python_input: "PythonInput") -> KeyBindings: """ Handle yes/no key presses when the exit confirmation is shown. """ @@ -277,14 +286,14 @@ def load_confirm_exit_bindings(python_input): @handle("Y", filter=confirmation_visible) @handle("enter", filter=confirmation_visible) @handle("c-d", filter=confirmation_visible) - def _(event): + def _(event: E) -> None: """ Really quit. """ event.app.exit(exception=EOFError, style="class:exiting") @handle(Keys.Any, filter=confirmation_visible) - def _(event): + def _(event: E) -> None: """ Cancel exit. """ @@ -294,7 +303,7 @@ def load_confirm_exit_bindings(python_input): return bindings -def auto_newline(buffer): +def auto_newline(buffer: Buffer) -> None: r""" Insert \n at the cursor position. Also add necessary padding. """ diff --git a/ptpython/layout.py b/ptpython/layout.py index dc6b19b..365f381 100644 --- a/ptpython/layout.py +++ b/ptpython/layout.py @@ -5,7 +5,7 @@ import platform import sys from enum import Enum from inspect import _ParameterKind as ParameterKind -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, List, Optional, Type from prompt_toolkit.application import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER @@ -15,10 +15,15 @@ from prompt_toolkit.filters import ( is_done, renderer_height_is_known, ) -from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text +from prompt_toolkit.formatted_text import ( + AnyFormattedText, + fragment_list_width, + to_formatted_text, +) from prompt_toolkit.formatted_text.base import StyleAndTextTuples from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.layout.containers import ( + AnyContainer, ConditionalContainer, Container, Float, @@ -40,9 +45,10 @@ from prompt_toolkit.layout.processors import ( HighlightIncrementalSearchProcessor, HighlightMatchingBracketProcessor, HighlightSelectionProcessor, + Processor, TabsProcessor, ) -from prompt_toolkit.lexers import SimpleLexer +from prompt_toolkit.lexers import Lexer, SimpleLexer from prompt_toolkit.mouse_events import MouseEvent from prompt_toolkit.selection import SelectionType from prompt_toolkit.widgets.toolbars import ( @@ -55,6 +61,7 @@ from prompt_toolkit.widgets.toolbars import ( from pygments.lexers import PythonLexer from .filters import HasSignature, ShowDocstring, ShowSidebar, ShowSignature +from .prompt_style import PromptStyle from .utils import if_mousedown if TYPE_CHECKING: @@ -98,7 +105,7 @@ def python_sidebar(python_input: "PythonInput") -> Window: def get_text_fragments() -> StyleAndTextTuples: tokens: StyleAndTextTuples = [] - def append_category(category: "OptionCategory") -> None: + def append_category(category: "OptionCategory[Any]") -> None: tokens.extend( [ ("class:sidebar", " "), @@ -150,10 +157,10 @@ def python_sidebar(python_input: "PythonInput") -> Window: return tokens class Control(FormattedTextControl): - def move_cursor_down(self): + def move_cursor_down(self) -> None: python_input.selected_option_index += 1 - def move_cursor_up(self): + def move_cursor_up(self) -> None: python_input.selected_option_index -= 1 return Window( @@ -165,12 +172,12 @@ def python_sidebar(python_input: "PythonInput") -> Window: ) -def python_sidebar_navigation(python_input): +def python_sidebar_navigation(python_input: "PythonInput") -> Window: """ Create the `Layout` showing the navigation information for the sidebar. """ - def get_text_fragments(): + def get_text_fragments() -> StyleAndTextTuples: # Show navigation info. return [ ("class:sidebar", " "), @@ -191,13 +198,13 @@ def python_sidebar_navigation(python_input): ) -def python_sidebar_help(python_input): +def python_sidebar_help(python_input: "PythonInput") -> Container: """ Create the `Layout` for the help text for the current item in the sidebar. """ token = "class:sidebar.helptext" - def get_current_description(): + def get_current_description() -> str: """ Return the description of the selected option. """ @@ -209,7 +216,7 @@ def python_sidebar_help(python_input): i += 1 return "" - def get_help_text(): + def get_help_text() -> StyleAndTextTuples: return [(token, get_current_description())] return ConditionalContainer( @@ -225,7 +232,7 @@ def python_sidebar_help(python_input): ) -def signature_toolbar(python_input): +def signature_toolbar(python_input: "PythonInput") -> Container: """ Return the `Layout` for the signature. """ @@ -311,21 +318,23 @@ class PythonPromptMargin(PromptMargin): It shows something like "In [1]:". """ - def __init__(self, python_input) -> None: + def __init__(self, python_input: "PythonInput") -> None: self.python_input = python_input - def get_prompt_style(): + def get_prompt_style() -> PromptStyle: return python_input.all_prompt_styles[python_input.prompt_style] def get_prompt() -> StyleAndTextTuples: return to_formatted_text(get_prompt_style().in_prompt()) - def get_continuation(width, line_number, is_soft_wrap): + def get_continuation( + width: int, line_number: int, is_soft_wrap: bool + ) -> StyleAndTextTuples: if python_input.show_line_numbers and not is_soft_wrap: text = ("%i " % (line_number + 1)).rjust(width) return [("class:line-number", text)] else: - return get_prompt_style().in2_prompt(width) + return to_formatted_text(get_prompt_style().in2_prompt(width)) super().__init__(get_prompt, get_continuation) @@ -510,7 +519,7 @@ def show_sidebar_button_info(python_input: "PythonInput") -> Container: def create_exit_confirmation( - python_input: "PythonInput", style="class:exit-confirmation" + python_input: "PythonInput", style: str = "class:exit-confirmation" ) -> Container: """ Create `Layout` for the exit message. @@ -567,22 +576,22 @@ class PtPythonLayout: def __init__( self, python_input: "PythonInput", - lexer=PythonLexer, - extra_body=None, - extra_toolbars=None, - extra_buffer_processors=None, + lexer: Lexer, + extra_body: Optional[AnyContainer] = None, + extra_toolbars: Optional[List[AnyContainer]] = None, + extra_buffer_processors: Optional[List[Processor]] = None, input_buffer_height: Optional[AnyDimension] = None, ) -> None: D = Dimension - extra_body = [extra_body] if extra_body else [] + extra_body_list: List[AnyContainer] = [extra_body] if extra_body else [] extra_toolbars = extra_toolbars or [] - extra_buffer_processors = extra_buffer_processors or [] + input_buffer_height = input_buffer_height or D(min=6) search_toolbar = SearchToolbar(python_input.search_buffer) - def create_python_input_window(): - def menu_position(): + def create_python_input_window() -> Window: + def menu_position() -> Optional[int]: """ When there is no autocompletion menu to be shown, and we have a signature, set the pop-up position at `bracket_start`. @@ -593,6 +602,7 @@ class PtPythonLayout: row, col = python_input.signatures[0].bracket_start index = b.document.translate_row_col_to_index(row - 1, col) return index + return None return Window( BufferControl( @@ -622,7 +632,7 @@ class PtPythonLayout: processor=AppendAutoSuggestion(), filter=~is_done ), ] - + extra_buffer_processors, + + (extra_buffer_processors or []), menu_position=menu_position, # Make sure that we always see the result of an reverse-i-search: preview_search=True, @@ -654,7 +664,7 @@ class PtPythonLayout: [ FloatContainer( content=HSplit( - [create_python_input_window()] + extra_body + [create_python_input_window()] + extra_body_list ), floats=[ Float( diff --git a/ptpython/py.typed b/ptpython/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ptpython/python_input.py b/ptpython/python_input.py index 1785f52..c561117 100644 --- a/ptpython/python_input.py +++ b/ptpython/python_input.py @@ -6,7 +6,18 @@ import __future__ from asyncio import get_event_loop from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generic, + List, + Mapping, + Optional, + Tuple, + TypeVar, +) from prompt_toolkit.application import Application, get_app from prompt_toolkit.auto_suggest import ( @@ -44,6 +55,7 @@ from prompt_toolkit.key_binding.bindings.open_in_editor import ( load_open_in_editor_bindings, ) from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.layout.containers import AnyContainer from prompt_toolkit.lexers import DynamicLexer, Lexer, SimpleLexer from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.styles import ( @@ -88,8 +100,8 @@ if TYPE_CHECKING: _T = TypeVar("_T", bound="_SupportsLessThan") -class OptionCategory: - def __init__(self, title: str, options: List["Option"]) -> None: +class OptionCategory(Generic[_T]): + def __init__(self, title: str, options: List["Option[_T]"]) -> None: self.title = title self.options = options @@ -113,7 +125,7 @@ class Option(Generic[_T]): get_current_value: Callable[[], _T], # We accept `object` as return type for the select functions, because # often they return an unused boolean. Maybe this can be improved. - get_values: Callable[[], Dict[_T, Callable[[], object]]], + get_values: Callable[[], Mapping[_T, Callable[[], object]]], ) -> None: self.title = title self.description = description @@ -121,7 +133,7 @@ class Option(Generic[_T]): self.get_values = get_values @property - def values(self) -> Dict[_T, Callable[[], object]]: + def values(self) -> Mapping[_T, Callable[[], object]]: return self.get_values() def activate_next(self, _previous: bool = False) -> None: @@ -192,12 +204,12 @@ class PythonInput: output: Optional[Output] = None, # For internal use. extra_key_bindings: Optional[KeyBindings] = None, - create_app=True, + create_app: bool = True, _completer: Optional[Completer] = None, _validator: Optional[Validator] = None, _lexer: Optional[Lexer] = None, _extra_buffer_processors=None, - _extra_layout_body=None, + _extra_layout_body: Optional[AnyContainer] = None, _extra_toolbars=None, _input_buffer_height=None, ) -> None: @@ -239,7 +251,7 @@ class PythonInput: self.history = InMemoryHistory() self._input_buffer_height = _input_buffer_height - self._extra_layout_body = _extra_layout_body or [] + self._extra_layout_body = _extra_layout_body self._extra_toolbars = _extra_toolbars or [] self._extra_buffer_processors = _extra_buffer_processors or [] @@ -388,7 +400,9 @@ class PythonInput: # Create an app if requested. If not, the global get_app() is returned # for self.app via property getter. if create_app: - self._app: Optional[Application] = self._create_application(input, output) + self._app: Optional[Application[str]] = self._create_application( + input, output + ) # Setting vi_mode will not work unless the prompt_toolkit # application has been created. if vi_mode: @@ -408,7 +422,7 @@ class PythonInput: return sum(len(category.options) for category in self.options) @property - def selected_option(self) -> Option: + def selected_option(self) -> Option[Any]: "Return the currently selected option." i = 0 for category in self.options: @@ -514,7 +528,7 @@ class PythonInput: self.ui_styles[self._current_ui_style_name], ) - def _create_options(self) -> List[OptionCategory]: + def _create_options(self) -> List[OptionCategory[Any]]: """ Create a list of `Option` instances for the options sidebar. """ @@ -530,15 +544,17 @@ class PythonInput: return True def simple_option( - title: str, description: str, field_name: str, values: Optional[List] = None - ) -> Option: + title: str, + description: str, + field_name: str, + values: Tuple[str, str] = ("off", "on"), + ) -> Option[str]: "Create Simple on/of option." - values = values or ["off", "on"] - def get_current_value(): + def get_current_value() -> str: return values[bool(getattr(self, field_name))] - def get_values(): + def get_values() -> Dict[str, Callable[[], bool]]: return { values[1]: lambda: enable(field_name), values[0]: lambda: disable(field_name), @@ -848,7 +864,7 @@ class PythonInput: def _create_application( self, input: Optional[Input], output: Optional[Output] - ) -> Application: + ) -> Application[str]: """ Create an `Application` instance. """ @@ -926,7 +942,7 @@ class PythonInput: self.editing_mode = EditingMode.EMACS @property - def app(self) -> Application: + def app(self) -> Application[str]: if self._app is None: return get_app() return self._app diff --git a/ptpython/repl.py b/ptpython/repl.py index b55b5d5..3c729c0 100644 --- a/ptpython/repl.py +++ b/ptpython/repl.py @@ -44,6 +44,7 @@ from pygments.token import Token from .python_input import PythonInput +PyCF_ALLOW_TOP_LEVEL_AWAIT: int try: from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT # type: ignore except ImportError: @@ -90,7 +91,7 @@ class PythonRepl(PythonInput): output = self.app.output output.write("WARNING | File not found: {}\n\n".format(path)) - def run_and_show_expression(self, expression): + def run_and_show_expression(self, expression: str) -> None: try: # Eval. try: @@ -135,7 +136,7 @@ class PythonRepl(PythonInput): text = self.read() except EOFError: return - except BaseException as e: + except BaseException: # Something went wrong while reading input. # (E.g., a bug in the completer that propagates. Don't # crash the REPL.) @@ -149,7 +150,7 @@ class PythonRepl(PythonInput): clear_title() self._remove_from_namespace() - async def run_and_show_expression_async(self, text): + async def run_and_show_expression_async(self, text: str): loop = asyncio.get_event_loop() try: @@ -349,7 +350,7 @@ class PythonRepl(PythonInput): if not hasattr(black, "Mode"): raise ImportError except ImportError: - pass # no Black package in your installation + pass # no Black package in your installation else: result_repr = black.format_str( result_repr, @@ -725,17 +726,17 @@ def embed( configure(repl) # Start repl. - patch_context: ContextManager = ( + patch_context: ContextManager[None] = ( patch_stdout_context() if patch_stdout else DummyContext() ) if return_asyncio_coroutine: - async def coroutine(): + async def coroutine() -> None: with patch_context: await repl.run_async() - return coroutine() + return coroutine() # type: ignore else: with patch_context: repl.run() diff --git a/ptpython/signatures.py b/ptpython/signatures.py index 228b99b..e836d33 100644 --- a/ptpython/signatures.py +++ b/ptpython/signatures.py @@ -8,13 +8,16 @@ can use `eval()` to evaluate the function object. import inspect from inspect import Signature as InspectSignature from inspect import _ParameterKind as ParameterKind -from typing import Any, Dict, List, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple from prompt_toolkit.document import Document from .completer import DictionaryCompleter from .utils import get_jedi_script_from_document +if TYPE_CHECKING: + import jedi.api.classes + __all__ = ["Signature", "get_signatures_using_jedi", "get_signatures_using_eval"] @@ -120,7 +123,9 @@ class Signature: ) @classmethod - def from_jedi_signature(cls, signature) -> "Signature": + def from_jedi_signature( + cls, signature: "jedi.api.classes.Signature" + ) -> "Signature": parameters = [] for p in signature.params: diff --git a/ptpython/utils.py b/ptpython/utils.py index 2fb24a4..ef96ca4 100644 --- a/ptpython/utils.py +++ b/ptpython/utils.py @@ -2,12 +2,31 @@ For internal use only. """ import re -from typing import Callable, Iterable, Type, TypeVar, cast - +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + Optional, + Type, + TypeVar, + cast, +) + +from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import to_formatted_text from prompt_toolkit.formatted_text.utils import fragment_list_to_text from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +if TYPE_CHECKING: + from jedi import Interpreter + + # See: prompt_toolkit/key_binding/key_bindings.py + # Annotating these return types as `object` is what works best, because + # `NotImplemented` is typed `Any`. + NotImplementedOrNone = object + __all__ = [ "has_unclosed_brackets", "get_jedi_script_from_document", @@ -45,7 +64,9 @@ def has_unclosed_brackets(text: str) -> bool: return False -def get_jedi_script_from_document(document, locals, globals): +def get_jedi_script_from_document( + document: Document, locals: Dict[str, Any], globals: Dict[str, Any] +) -> "Interpreter": import jedi # We keep this import in-line, to improve start-up time. # Importing Jedi is 'slow'. @@ -78,7 +99,7 @@ def get_jedi_script_from_document(document, locals, globals): _multiline_string_delims = re.compile("""[']{3}|["]{3}""") -def document_is_multiline_python(document): +def document_is_multiline_python(document: Document) -> bool: """ Determine whether this is a multiline Python document. """ @@ -133,7 +154,7 @@ def if_mousedown(handler: _T) -> _T: by the Window.) """ - def handle_if_mouse_down(mouse_event: MouseEvent): + def handle_if_mouse_down(mouse_event: MouseEvent) -> "NotImplementedOrNone": if mouse_event.event_type == MouseEventType.MOUSE_DOWN: return handler(mouse_event) else: @@ -142,7 +163,7 @@ def if_mousedown(handler: _T) -> _T: return cast(_T, handle_if_mouse_down) -_T_type = TypeVar("_T_type", bound=Type) +_T_type = TypeVar("_T_type", bound=type) def ptrepr_to_repr(cls: _T_type) -> _T_type: @@ -154,7 +175,8 @@ def ptrepr_to_repr(cls: _T_type) -> _T_type: "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method." ) - def __repr__(self) -> str: + def __repr__(self: object) -> str: + assert hasattr(cls, "__pt_repr__") return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self))) cls.__repr__ = __repr__ # type:ignore diff --git a/ptpython/validator.py b/ptpython/validator.py index 0f6a4ea..ffac583 100644 --- a/ptpython/validator.py +++ b/ptpython/validator.py @@ -1,3 +1,6 @@ +from typing import Callable, Optional + +from prompt_toolkit.document import Document from prompt_toolkit.validation import ValidationError, Validator from .utils import unindent_code @@ -13,10 +16,10 @@ class PythonValidator(Validator): active compiler flags. """ - def __init__(self, get_compiler_flags=None): + def __init__(self, get_compiler_flags: Optional[Callable[[], int]] = None) -> None: self.get_compiler_flags = get_compiler_flags - def validate(self, document): + def validate(self, document: Document) -> None: """ Check input for Python syntax errors. """ @@ -45,7 +48,7 @@ class PythonValidator(Validator): # fixed in Python 3.) # TODO: This is not correct if indentation was removed. index = document.translate_row_col_to_index( - e.lineno - 1, (e.offset or 1) - 1 + (e.lineno or 1) - 1, (e.offset or 1) - 1 ) raise ValidationError(index, f"Syntax Error: {e}") except TypeError as e: -- cgit v1.2.3