From 227038a454943a075c51b60988c53f77a605fa10 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 26 Feb 2021 05:10:02 +0100 Subject: Adding upstream version 3.0.16. Signed-off-by: Daniel Baumann --- ptpython/python_input.py | 1054 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1054 insertions(+) create mode 100644 ptpython/python_input.py (limited to 'ptpython/python_input.py') diff --git a/ptpython/python_input.py b/ptpython/python_input.py new file mode 100644 index 0000000..e63cdf1 --- /dev/null +++ b/ptpython/python_input.py @@ -0,0 +1,1054 @@ +""" +Application for reading Python input. +This can be used for creation of Python REPLs. +""" +import __future__ + +import threading +from asyncio import get_event_loop +from functools import partial +from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, TypeVar + +from prompt_toolkit.application import Application, get_app +from prompt_toolkit.auto_suggest import ( + AutoSuggestFromHistory, + ConditionalAutoSuggest, + ThreadedAutoSuggest, +) +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.completion import ( + Completer, + ConditionalCompleter, + DynamicCompleter, + FuzzyCompleter, + ThreadedCompleter, + merge_completers, +) +from prompt_toolkit.document import Document +from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode +from prompt_toolkit.filters import Condition +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.history import ( + FileHistory, + History, + InMemoryHistory, + ThreadedHistory, +) +from prompt_toolkit.input import Input +from prompt_toolkit.key_binding import ( + ConditionalKeyBindings, + KeyBindings, + merge_key_bindings, +) +from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings +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.lexers import DynamicLexer, Lexer, SimpleLexer +from prompt_toolkit.output import ColorDepth, Output +from prompt_toolkit.styles import ( + AdjustBrightnessStyleTransformation, + BaseStyle, + ConditionalStyleTransformation, + DynamicStyle, + SwapLightAndDarkStyleTransformation, + merge_style_transformations, +) +from prompt_toolkit.utils import is_windows +from prompt_toolkit.validation import ConditionalValidator, Validator + +from .completer import CompletePrivateAttributes, HidePrivateCompleter, PythonCompleter +from .history_browser import PythonHistory +from .key_bindings import ( + load_confirm_exit_bindings, + load_python_bindings, + load_sidebar_bindings, +) +from .layout import CompletionVisualisation, PtPythonLayout +from .lexer import PtpythonLexer +from .prompt_style import ClassicPrompt, IPythonPrompt, PromptStyle +from .signatures import Signature, get_signatures_using_eval, get_signatures_using_jedi +from .style import generate_style, get_all_code_styles, get_all_ui_styles +from .utils import unindent_code +from .validator import PythonValidator + +__all__ = ["PythonInput"] + + +if TYPE_CHECKING: + from typing_extensions import Protocol + + class _SupportsLessThan(Protocol): + # Taken from typeshed. _T is used by "sorted", which needs anything + # sortable. + def __lt__(self, __other: Any) -> bool: + ... + + +_T = TypeVar("_T", bound="_SupportsLessThan") + + +class OptionCategory: + def __init__(self, title: str, options: List["Option"]) -> None: + self.title = title + self.options = options + + +class Option(Generic[_T]): + """ + Ptpython configuration option that can be shown and modified from the + sidebar. + + :param title: Text. + :param description: Text. + :param get_values: Callable that returns a dictionary mapping the + possible values to callbacks that activate these value. + :param get_current_value: Callable that returns the current, active value. + """ + + def __init__( + self, + title: str, + description: str, + 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]]], + ) -> None: + self.title = title + self.description = description + self.get_current_value = get_current_value + self.get_values = get_values + + @property + def values(self) -> Dict[_T, Callable[[], object]]: + return self.get_values() + + def activate_next(self, _previous: bool = False) -> None: + """ + Activate next value. + """ + current = self.get_current_value() + options = sorted(self.values.keys()) + + # Get current index. + try: + index = options.index(current) + except ValueError: + index = 0 + + # Go to previous/next index. + if _previous: + index -= 1 + else: + index += 1 + + # Call handler for this option. + next_option = options[index % len(options)] + self.values[next_option]() + + def activate_previous(self) -> None: + """ + Activate previous value. + """ + self.activate_next(_previous=True) + + +COLOR_DEPTHS = { + ColorDepth.DEPTH_1_BIT: "Monochrome", + ColorDepth.DEPTH_4_BIT: "ANSI Colors", + ColorDepth.DEPTH_8_BIT: "256 colors", + ColorDepth.DEPTH_24_BIT: "True color", +} + +_Namespace = Dict[str, Any] +_GetNamespace = Callable[[], _Namespace] + + +class PythonInput: + """ + Prompt for reading Python input. + + :: + + python_input = PythonInput(...) + python_code = python_input.app.run() + """ + + def __init__( + self, + get_globals: Optional[_GetNamespace] = None, + get_locals: Optional[_GetNamespace] = None, + history_filename: Optional[str] = None, + vi_mode: bool = False, + color_depth: Optional[ColorDepth] = None, + # Input/output. + input: Optional[Input] = None, + output: Optional[Output] = None, + # For internal use. + extra_key_bindings: Optional[KeyBindings] = None, + _completer: Optional[Completer] = None, + _validator: Optional[Validator] = None, + _lexer: Optional[Lexer] = None, + _extra_buffer_processors=None, + _extra_layout_body=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 + + self.completer = _completer or PythonCompleter( + self.get_globals, + self.get_locals, + lambda: self.enable_dictionary_completion, + ) + + self._completer = HidePrivateCompleter( + # If fuzzy is enabled, first do fuzzy completion, but always add + # the non-fuzzy completions, if somehow the fuzzy completer didn't + # find them. (Due to the way the cursor position is moved in the + # fuzzy completer, some completions will not always be found by the + # fuzzy completer, but will be found with the normal completer.) + merge_completers( + [ + ConditionalCompleter( + FuzzyCompleter(DynamicCompleter(lambda: self.completer)), + Condition(lambda: self.enable_fuzzy_completion), + ), + DynamicCompleter(lambda: self.completer), + ], + deduplicate=True, + ), + lambda: self.complete_private_attributes, + ) + self._validator = _validator or PythonValidator(self.get_compiler_flags) + self._lexer = PtpythonLexer(_lexer) + + self.history: History + if history_filename: + self.history = ThreadedHistory(FileHistory(history_filename)) + else: + self.history = InMemoryHistory() + + self._input_buffer_height = _input_buffer_height + self._extra_layout_body = _extra_layout_body or [] + self._extra_toolbars = _extra_toolbars or [] + self._extra_buffer_processors = _extra_buffer_processors or [] + + self.extra_key_bindings = extra_key_bindings or KeyBindings() + + # Settings. + self.title: AnyFormattedText = "" + self.show_signature: bool = False + self.show_docstring: bool = False + self.show_meta_enter_message: bool = True + self.completion_visualisation: CompletionVisualisation = ( + CompletionVisualisation.MULTI_COLUMN + ) + self.completion_menu_scroll_offset: int = 1 + + self.show_line_numbers: bool = False + self.show_status_bar: bool = True + self.wrap_lines: bool = True + self.complete_while_typing: bool = True + self.paste_mode: bool = ( + False # When True, don't insert whitespace after newline. + ) + self.confirm_exit: bool = ( + True # Ask for confirmation when Control-D is pressed. + ) + self.accept_input_on_enter: int = 2 # Accept when pressing Enter 'n' times. + # 'None' means that meta-enter is always required. + self.enable_open_in_editor: bool = True + self.enable_system_bindings: bool = True + self.enable_input_validation: bool = True + self.enable_auto_suggest: bool = False + self.enable_mouse_support: bool = False + self.enable_history_search: bool = False # When True, like readline, going + # back in history will filter the + # history on the records starting + # with the current input. + + self.enable_syntax_highlighting: bool = True + self.enable_fuzzy_completion: bool = False + self.enable_dictionary_completion: bool = False # Also eval-based completion. + self.complete_private_attributes: CompletePrivateAttributes = ( + CompletePrivateAttributes.ALWAYS + ) + self.swap_light_and_dark: bool = False + self.highlight_matching_parenthesis: bool = False + self.show_sidebar: bool = False # Currently show the sidebar. + + # Pager. + self.enable_output_formatting: bool = False + self.enable_pager: bool = False + + # When the sidebar is visible, also show the help text. + self.show_sidebar_help: bool = True + + # Currently show 'Do you really want to exit?' + self.show_exit_confirmation: bool = False + + # The title to be displayed in the terminal. (None or string.) + self.terminal_title: Optional[str] = None + + self.exit_message: str = "Do you really want to exit?" + self.insert_blank_line_after_output: bool = True # (For the REPL.) + self.insert_blank_line_after_input: bool = False # (For the REPL.) + + # The buffers. + self.default_buffer = self._create_buffer() + self.search_buffer: Buffer = Buffer() + self.docstring_buffer: Buffer = Buffer(read_only=True) + + # 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] = { + "ipython": IPythonPrompt(self), + "classic": ClassicPrompt(), + } + + self.get_input_prompt = lambda: self.all_prompt_styles[ + self.prompt_style + ].in_prompt() + + self.get_output_prompt = lambda: self.all_prompt_styles[ + self.prompt_style + ].out_prompt() + + #: Load styles. + self.code_styles: Dict[str, BaseStyle] = get_all_code_styles() + self.ui_styles = get_all_ui_styles() + self._current_code_style_name: str = "default" + self._current_ui_style_name: str = "default" + + if is_windows(): + self._current_code_style_name = "win32" + + self._current_style = self._generate_style() + self.color_depth: ColorDepth = color_depth or ColorDepth.default() + + self.max_brightness: float = 1.0 + self.min_brightness: float = 0.0 + + # Options to be configurable from the sidebar. + self.options = self._create_options() + self.selected_option_index: int = 0 + + #: Incremeting integer counting the current statement. + self.current_statement_index: int = 1 + + # Code signatures. (This is set asynchronously after a timeout.) + self.signatures: List[Signature] = [] + + # Boolean indicating whether we have a signatures thread running. + # (Never run more than one at the same time.) + self._get_signatures_thread_running: bool = False + + # Get into Vi navigation mode at startup + self.vi_start_in_navigation_mode: bool = False + + # Preserve last used Vi input mode between main loop iterations + self.vi_keep_last_used_mode: bool = False + + self.style_transformation = merge_style_transformations( + [ + ConditionalStyleTransformation( + SwapLightAndDarkStyleTransformation(), + filter=Condition(lambda: self.swap_light_and_dark), + ), + AdjustBrightnessStyleTransformation( + lambda: self.min_brightness, lambda: self.max_brightness + ), + ] + ) + self.ptpython_layout = PtPythonLayout( + self, + lexer=DynamicLexer( + lambda: self._lexer + if self.enable_syntax_highlighting + else SimpleLexer() + ), + input_buffer_height=self._input_buffer_height, + extra_buffer_processors=self._extra_buffer_processors, + extra_body=self._extra_layout_body, + extra_toolbars=self._extra_toolbars, + ) + + self.app = self._create_application(input, output) + + if vi_mode: + self.app.editing_mode = EditingMode.VI + + def _accept_handler(self, buff: Buffer) -> bool: + app = get_app() + app.exit(result=buff.text) + app.pre_run_callables.append(buff.reset) + return True # Keep text, we call 'reset' later on. + + @property + def option_count(self) -> int: + " Return the total amount of options. (In all categories together.) " + return sum(len(category.options) for category in self.options) + + @property + def selected_option(self) -> Option: + " Return the currently selected option. " + i = 0 + for category in self.options: + for o in category.options: + if i == self.selected_option_index: + return o + else: + i += 1 + + raise ValueError("Nothing selected") + + def get_compiler_flags(self) -> int: + """ + Give the current compiler flags by looking for _Feature instances + in the globals. + """ + flags = 0 + + for value in self.get_globals().values(): + try: + if isinstance(value, __future__._Feature): + f = value.compiler_flag + flags |= f + except BaseException: + # get_compiler_flags should never raise to not run into an + # `Unhandled exception in event loop` + + # See: https://github.com/prompt-toolkit/ptpython/issues/351 + # An exception can be raised when some objects in the globals + # raise an exception in a custom `__getattribute__`. + pass + + return flags + + @property + def add_key_binding(self) -> Callable[[_T], _T]: + """ + Shortcut for adding new key bindings. + (Mostly useful for a config.py file, that receives + a PythonInput/Repl instance as input.) + + :: + + @python_input.add_key_binding(Keys.ControlX, filter=...) + def handler(event): + ... + """ + + def add_binding_decorator(*k, **kw): + return self.extra_key_bindings.add(*k, **kw) + + return add_binding_decorator + + def install_code_colorscheme(self, name: str, style: BaseStyle) -> None: + """ + Install a new code color scheme. + """ + self.code_styles[name] = style + + def use_code_colorscheme(self, name: str) -> None: + """ + Apply new colorscheme. (By name.) + """ + assert name in self.code_styles + + self._current_code_style_name = name + self._current_style = self._generate_style() + + def install_ui_colorscheme(self, name: str, style: BaseStyle) -> None: + """ + Install a new UI color scheme. + """ + self.ui_styles[name] = style + + def use_ui_colorscheme(self, name: str) -> None: + """ + Apply new colorscheme. (By name.) + """ + assert name in self.ui_styles + + self._current_ui_style_name = name + self._current_style = self._generate_style() + + def _use_color_depth(self, depth: ColorDepth) -> None: + self.color_depth = depth + + def _set_min_brightness(self, value: float) -> None: + self.min_brightness = value + self.max_brightness = max(self.max_brightness, value) + + def _set_max_brightness(self, value: float) -> None: + self.max_brightness = value + self.min_brightness = min(self.min_brightness, value) + + def _generate_style(self) -> BaseStyle: + """ + Create new Style instance. + (We don't want to do this on every key press, because each time the + renderer receives a new style class, he will redraw everything.) + """ + return generate_style( + self.code_styles[self._current_code_style_name], + self.ui_styles[self._current_ui_style_name], + ) + + def _create_options(self) -> List[OptionCategory]: + """ + Create a list of `Option` instances for the options sidebar. + """ + + def enable(attribute: str, value: Any = True) -> bool: + setattr(self, attribute, value) + + # Return `True`, to be able to chain this in the lambdas below. + return True + + def disable(attribute: str) -> bool: + setattr(self, attribute, False) + return True + + def simple_option( + title: str, description: str, field_name: str, values: Optional[List] = None + ) -> Option: + " Create Simple on/of option. " + values = values or ["off", "on"] + + def get_current_value(): + return values[bool(getattr(self, field_name))] + + def get_values(): + return { + values[1]: lambda: enable(field_name), + values[0]: lambda: disable(field_name), + } + + return Option( + title=title, + description=description, + get_values=get_values, + get_current_value=get_current_value, + ) + + brightness_values = [1.0 / 20 * value for value in range(0, 21)] + + return [ + OptionCategory( + "Input", + [ + Option( + title="Editing mode", + description="Vi or emacs key bindings.", + get_current_value=lambda: ["Emacs", "Vi"][self.vi_mode], + get_values=lambda: { + "Emacs": lambda: disable("vi_mode"), + "Vi": lambda: enable("vi_mode"), + }, + ), + simple_option( + title="Paste mode", + description="When enabled, don't indent automatically.", + field_name="paste_mode", + ), + Option( + title="Complete while typing", + description="Generate autocompletions automatically while typing. " + 'Don\'t require pressing TAB. (Not compatible with "History search".)', + get_current_value=lambda: ["off", "on"][ + self.complete_while_typing + ], + get_values=lambda: { + "on": lambda: enable("complete_while_typing") + and disable("enable_history_search"), + "off": lambda: disable("complete_while_typing"), + }, + ), + Option( + title="Complete private attrs", + description="Show or hide private attributes in the completions. " + "'If no public' means: show private attributes only if no public " + "matches are found or if an underscore was typed.", + get_current_value=lambda: { + CompletePrivateAttributes.NEVER: "Never", + CompletePrivateAttributes.ALWAYS: "Always", + CompletePrivateAttributes.IF_NO_PUBLIC: "If no public", + }[self.complete_private_attributes], + get_values=lambda: { + "Never": lambda: enable( + "complete_private_attributes", + CompletePrivateAttributes.NEVER, + ), + "Always": lambda: enable( + "complete_private_attributes", + CompletePrivateAttributes.ALWAYS, + ), + "If no public": lambda: enable( + "complete_private_attributes", + CompletePrivateAttributes.IF_NO_PUBLIC, + ), + }, + ), + Option( + title="Enable fuzzy completion", + description="Enable fuzzy completion.", + get_current_value=lambda: ["off", "on"][ + self.enable_fuzzy_completion + ], + get_values=lambda: { + "on": lambda: enable("enable_fuzzy_completion"), + "off": lambda: disable("enable_fuzzy_completion"), + }, + ), + Option( + title="Dictionary completion", + description="Enable experimental dictionary/list completion.\n" + 'WARNING: this does "eval" on fragments of\n' + " your Python input and is\n" + " potentially unsafe.", + get_current_value=lambda: ["off", "on"][ + self.enable_dictionary_completion + ], + get_values=lambda: { + "on": lambda: enable("enable_dictionary_completion"), + "off": lambda: disable("enable_dictionary_completion"), + }, + ), + Option( + title="History search", + description="When pressing the up-arrow, filter the history on input starting " + 'with the current text. (Not compatible with "Complete while typing".)', + get_current_value=lambda: ["off", "on"][ + self.enable_history_search + ], + get_values=lambda: { + "on": lambda: enable("enable_history_search") + and disable("complete_while_typing"), + "off": lambda: disable("enable_history_search"), + }, + ), + simple_option( + title="Mouse support", + description="Respond to mouse clicks and scrolling for positioning the cursor, " + "selecting text and scrolling through windows.", + field_name="enable_mouse_support", + ), + simple_option( + title="Confirm on exit", + description="Require confirmation when exiting.", + field_name="confirm_exit", + ), + simple_option( + title="Input validation", + description="In case of syntax errors, move the cursor to the error " + "instead of showing a traceback of a SyntaxError.", + field_name="enable_input_validation", + ), + simple_option( + title="Auto suggestion", + description="Auto suggest inputs by looking at the history. " + "Pressing right arrow or Ctrl-E will complete the entry.", + field_name="enable_auto_suggest", + ), + Option( + title="Accept input on enter", + description="Amount of ENTER presses required to execute input when the cursor " + "is at the end of the input. (Note that META+ENTER will always execute.)", + get_current_value=lambda: str( + self.accept_input_on_enter or "meta-enter" + ), + get_values=lambda: { + "2": lambda: enable("accept_input_on_enter", 2), + "3": lambda: enable("accept_input_on_enter", 3), + "4": lambda: enable("accept_input_on_enter", 4), + "meta-enter": lambda: enable("accept_input_on_enter", None), + }, + ), + ], + ), + OptionCategory( + "Display", + [ + Option( + title="Completions", + description="Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)", + get_current_value=lambda: self.completion_visualisation.value, + get_values=lambda: { + CompletionVisualisation.NONE.value: lambda: enable( + "completion_visualisation", CompletionVisualisation.NONE + ), + CompletionVisualisation.POP_UP.value: lambda: enable( + "completion_visualisation", + CompletionVisualisation.POP_UP, + ), + CompletionVisualisation.MULTI_COLUMN.value: lambda: enable( + "completion_visualisation", + CompletionVisualisation.MULTI_COLUMN, + ), + CompletionVisualisation.TOOLBAR.value: lambda: enable( + "completion_visualisation", + CompletionVisualisation.TOOLBAR, + ), + }, + ), + Option( + 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)) + for s in self.all_prompt_styles + ), + ), + simple_option( + title="Blank line after input", + description="Insert a blank line after the input.", + field_name="insert_blank_line_after_input", + ), + simple_option( + title="Blank line after output", + description="Insert a blank line after the output.", + field_name="insert_blank_line_after_output", + ), + simple_option( + title="Show signature", + description="Display function signatures.", + field_name="show_signature", + ), + simple_option( + title="Show docstring", + description="Display function docstrings.", + field_name="show_docstring", + ), + simple_option( + title="Show line numbers", + description="Show line numbers when the input consists of multiple lines.", + field_name="show_line_numbers", + ), + simple_option( + title="Show Meta+Enter message", + description="Show the [Meta+Enter] message when this key combination is required to execute commands. " + + "(This is the case when a simple [Enter] key press will insert a newline.", + field_name="show_meta_enter_message", + ), + simple_option( + title="Wrap lines", + description="Wrap lines instead of scrolling horizontally.", + field_name="wrap_lines", + ), + simple_option( + title="Show status bar", + description="Show the status bar at the bottom of the terminal.", + field_name="show_status_bar", + ), + simple_option( + title="Show sidebar help", + description="When the sidebar is visible, also show this help text.", + field_name="show_sidebar_help", + ), + simple_option( + title="Highlight parenthesis", + description="Highlight matching parenthesis, when the cursor is on or right after one.", + field_name="highlight_matching_parenthesis", + ), + simple_option( + title="Reformat output (black)", + description="Reformat outputs using Black, if possible (experimental).", + field_name="enable_output_formatting", + ), + simple_option( + title="Enable pager for output", + description="Use a pager for displaying outputs that don't " + "fit on the screen.", + field_name="enable_pager", + ), + ], + ), + OptionCategory( + "Colors", + [ + simple_option( + title="Syntax highlighting", + description="Use colors for syntax highligthing", + field_name="enable_syntax_highlighting", + ), + simple_option( + title="Swap light/dark colors", + description="Swap light and dark colors.", + field_name="swap_light_and_dark", + ), + Option( + title="Code", + description="Color scheme to use for the Python code.", + get_current_value=lambda: self._current_code_style_name, + get_values=lambda: { + name: partial(self.use_code_colorscheme, name) + for name in self.code_styles + }, + ), + Option( + 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)) + for name in self.ui_styles + ), + ), + Option( + title="Color depth", + description="Monochrome (1 bit), 16 ANSI colors (4 bit),\n256 colors (8 bit), or 24 bit.", + get_current_value=lambda: COLOR_DEPTHS[self.color_depth], + get_values=lambda: { + name: partial(self._use_color_depth, depth) + for depth, name in COLOR_DEPTHS.items() + }, + ), + Option( + title="Min brightness", + description="Minimum brightness for the color scheme (default=0.0).", + get_current_value=lambda: "%.2f" % self.min_brightness, + get_values=lambda: { + "%.2f" % value: partial(self._set_min_brightness, value) + for value in brightness_values + }, + ), + Option( + title="Max brightness", + description="Maximum brightness for the color scheme (default=1.0).", + get_current_value=lambda: "%.2f" % self.max_brightness, + get_values=lambda: { + "%.2f" % value: partial(self._set_max_brightness, value) + for value in brightness_values + }, + ), + ], + ), + ] + + def _create_application( + self, input: Optional[Input], output: Optional[Output] + ) -> Application: + """ + Create an `Application` instance. + """ + return Application( + layout=self.ptpython_layout.layout, + key_bindings=merge_key_bindings( + [ + load_python_bindings(self), + load_auto_suggest_bindings(), + load_sidebar_bindings(self), + load_confirm_exit_bindings(self), + ConditionalKeyBindings( + load_open_in_editor_bindings(), + Condition(lambda: self.enable_open_in_editor), + ), + # Extra key bindings should not be active when the sidebar is visible. + ConditionalKeyBindings( + self.extra_key_bindings, + Condition(lambda: not self.show_sidebar), + ), + ] + ), + color_depth=lambda: self.color_depth, + paste_mode=Condition(lambda: self.paste_mode), + mouse_support=Condition(lambda: self.enable_mouse_support), + style=DynamicStyle(lambda: self._current_style), + style_transformation=self.style_transformation, + include_default_pygments_style=False, + reverse_vi_search_direction=True, + input=input, + output=output, + ) + + def _create_buffer(self) -> Buffer: + """ + Create the `Buffer` for the Python input. + """ + python_buffer = Buffer( + name=DEFAULT_BUFFER, + complete_while_typing=Condition(lambda: self.complete_while_typing), + enable_history_search=Condition(lambda: self.enable_history_search), + tempfile_suffix=".py", + history=self.history, + completer=ThreadedCompleter(self._completer), + validator=ConditionalValidator( + self._validator, Condition(lambda: self.enable_input_validation) + ), + auto_suggest=ConditionalAutoSuggest( + ThreadedAutoSuggest(AutoSuggestFromHistory()), + Condition(lambda: self.enable_auto_suggest), + ), + accept_handler=self._accept_handler, + on_text_changed=self._on_input_timeout, + ) + + return python_buffer + + @property + def editing_mode(self) -> EditingMode: + return self.app.editing_mode + + @editing_mode.setter + def editing_mode(self, value: EditingMode) -> None: + self.app.editing_mode = value + + @property + def vi_mode(self) -> bool: + return self.editing_mode == EditingMode.VI + + @vi_mode.setter + def vi_mode(self, value: bool) -> None: + if value: + self.editing_mode = EditingMode.VI + else: + self.editing_mode = EditingMode.EMACS + + def _on_input_timeout(self, buff: Buffer, loop=None) -> None: + """ + When there is no input activity, + in another thread, get the signature of the current code. + """ + app = self.app + + # Never run multiple get-signature threads. + if self._get_signatures_thread_running: + return + self._get_signatures_thread_running = True + + document = buff.document + + loop = loop or get_event_loop() + + def run(): + # 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. + signatures = get_signatures_using_jedi( + document, self.get_locals(), self.get_globals() + ) + if not signatures and self.enable_dictionary_completion: + signatures = get_signatures_using_eval( + document, self.get_locals(), self.get_globals() + ) + + self._get_signatures_thread_running = False + + # Set signatures and redraw if the text didn't change in the + # meantime. Otherwise request new signatures. + if buff.text == document.text: + self.signatures = signatures + + # Set docstring in docstring buffer. + if signatures: + self.docstring_buffer.reset( + document=Document(signatures[0].docstring, cursor_position=0) + ) + else: + self.docstring_buffer.reset() + + app.invalidate() + else: + self._on_input_timeout(buff, loop=loop) + + loop.run_in_executor(None, run) + + def on_reset(self) -> None: + self.signatures = [] + + def enter_history(self) -> None: + """ + Display the history. + """ + app = get_app() + app.vi_state.input_mode = InputMode.NAVIGATION + + history = PythonHistory(self, self.default_buffer.document) + + import asyncio + + from prompt_toolkit.application import in_terminal + + async def do_in_terminal() -> None: + async with in_terminal(): + result = await history.app.run_async() + if result is not None: + self.default_buffer.text = result + + app.vi_state.input_mode = InputMode.INSERT + + asyncio.ensure_future(do_in_terminal()) + + def read(self) -> str: + """ + Read the input. + + This will run the Python input user interface in another thread, wait + for input to be accepted and return that. By running the UI in another + thread, we avoid issues regarding possibly nested event loops. + + 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. + def pre_run( + last_input_mode: InputMode = self.app.vi_state.input_mode, + ) -> None: + if self.vi_keep_last_used_mode: + self.app.vi_state.input_mode = last_input_mode + + if not self.vi_keep_last_used_mode and self.vi_start_in_navigation_mode: + self.app.vi_state.input_mode = InputMode.NAVIGATION + + # Run the UI. + result: str = "" + exception: Optional[BaseException] = None + + def in_thread() -> None: + nonlocal result, exception + try: + while True: + try: + result = self.app.run(pre_run=pre_run) + + if result.lstrip().startswith("\x1a"): + # When the input starts with Ctrl-Z, quit the REPL. + # (Important for Windows users.) + raise EOFError + + # Remove leading whitespace. + # (Users can add extra indentation, which happens for + # instance because of copy/pasting code.) + result = unindent_code(result) + + if result and not result.isspace(): + return + except KeyboardInterrupt: + # Abort - try again. + self.default_buffer.document = Document() + except BaseException as e: + exception = e + return + + finally: + if self.insert_blank_line_after_input: + self.app.output.write("\n") + + thread = threading.Thread(target=in_thread) + thread.start() + thread.join() + + if exception is not None: + raise exception + return result -- cgit v1.2.3