summaryrefslogtreecommitdiffstats
path: root/ptpython/python_input.py
diff options
context:
space:
mode:
Diffstat (limited to 'ptpython/python_input.py')
-rw-r--r--ptpython/python_input.py1054
1 files changed, 1054 insertions, 0 deletions
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