1"""
2Application for reading Python input.
3This can be used for creation of Python REPLs.
4"""
5import __future__
6
7from asyncio import get_event_loop
8from functools import partial
9from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, TypeVar
10
11from prompt_toolkit.application import Application, get_app
12from prompt_toolkit.auto_suggest import (
13    AutoSuggestFromHistory,
14    ConditionalAutoSuggest,
15    ThreadedAutoSuggest,
16)
17from prompt_toolkit.buffer import Buffer
18from prompt_toolkit.completion import (
19    Completer,
20    ConditionalCompleter,
21    DynamicCompleter,
22    FuzzyCompleter,
23    ThreadedCompleter,
24    merge_completers,
25)
26from prompt_toolkit.document import Document
27from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
28from prompt_toolkit.filters import Condition
29from prompt_toolkit.formatted_text import AnyFormattedText
30from prompt_toolkit.history import (
31    FileHistory,
32    History,
33    InMemoryHistory,
34    ThreadedHistory,
35)
36from prompt_toolkit.input import Input
37from prompt_toolkit.key_binding import (
38    ConditionalKeyBindings,
39    KeyBindings,
40    merge_key_bindings,
41)
42from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings
43from prompt_toolkit.key_binding.bindings.open_in_editor import (
44    load_open_in_editor_bindings,
45)
46from prompt_toolkit.key_binding.vi_state import InputMode
47from prompt_toolkit.lexers import DynamicLexer, Lexer, SimpleLexer
48from prompt_toolkit.output import ColorDepth, Output
49from prompt_toolkit.styles import (
50    AdjustBrightnessStyleTransformation,
51    BaseStyle,
52    ConditionalStyleTransformation,
53    DynamicStyle,
54    SwapLightAndDarkStyleTransformation,
55    merge_style_transformations,
56)
57from prompt_toolkit.utils import is_windows
58from prompt_toolkit.validation import ConditionalValidator, Validator
59
60from .completer import CompletePrivateAttributes, HidePrivateCompleter, PythonCompleter
61from .history_browser import PythonHistory
62from .key_bindings import (
63    load_confirm_exit_bindings,
64    load_python_bindings,
65    load_sidebar_bindings,
66)
67from .layout import CompletionVisualisation, PtPythonLayout
68from .lexer import PtpythonLexer
69from .prompt_style import ClassicPrompt, IPythonPrompt, PromptStyle
70from .signatures import Signature, get_signatures_using_eval, get_signatures_using_jedi
71from .style import generate_style, get_all_code_styles, get_all_ui_styles
72from .utils import unindent_code
73from .validator import PythonValidator
74
75__all__ = ["PythonInput"]
76
77
78if TYPE_CHECKING:
79    from typing_extensions import Protocol
80
81    class _SupportsLessThan(Protocol):
82        # Taken from typeshed. _T is used by "sorted", which needs anything
83        # sortable.
84        def __lt__(self, __other: Any) -> bool:
85            ...
86
87
88_T = TypeVar("_T", bound="_SupportsLessThan")
89
90
91class OptionCategory:
92    def __init__(self, title: str, options: List["Option"]) -> None:
93        self.title = title
94        self.options = options
95
96
97class Option(Generic[_T]):
98    """
99    Ptpython configuration option that can be shown and modified from the
100    sidebar.
101
102    :param title: Text.
103    :param description: Text.
104    :param get_values: Callable that returns a dictionary mapping the
105            possible values to callbacks that activate these value.
106    :param get_current_value: Callable that returns the current, active value.
107    """
108
109    def __init__(
110        self,
111        title: str,
112        description: str,
113        get_current_value: Callable[[], _T],
114        # We accept `object` as return type for the select functions, because
115        # often they return an unused boolean. Maybe this can be improved.
116        get_values: Callable[[], Dict[_T, Callable[[], object]]],
117    ) -> None:
118        self.title = title
119        self.description = description
120        self.get_current_value = get_current_value
121        self.get_values = get_values
122
123    @property
124    def values(self) -> Dict[_T, Callable[[], object]]:
125        return self.get_values()
126
127    def activate_next(self, _previous: bool = False) -> None:
128        """
129        Activate next value.
130        """
131        current = self.get_current_value()
132        options = sorted(self.values.keys())
133
134        # Get current index.
135        try:
136            index = options.index(current)
137        except ValueError:
138            index = 0
139
140        # Go to previous/next index.
141        if _previous:
142            index -= 1
143        else:
144            index += 1
145
146        # Call handler for this option.
147        next_option = options[index % len(options)]
148        self.values[next_option]()
149
150    def activate_previous(self) -> None:
151        """
152        Activate previous value.
153        """
154        self.activate_next(_previous=True)
155
156
157COLOR_DEPTHS = {
158    ColorDepth.DEPTH_1_BIT: "Monochrome",
159    ColorDepth.DEPTH_4_BIT: "ANSI Colors",
160    ColorDepth.DEPTH_8_BIT: "256 colors",
161    ColorDepth.DEPTH_24_BIT: "True color",
162}
163
164_Namespace = Dict[str, Any]
165_GetNamespace = Callable[[], _Namespace]
166
167
168class PythonInput:
169    """
170    Prompt for reading Python input.
171
172    ::
173
174        python_input = PythonInput(...)
175        python_code = python_input.app.run()
176
177    :param create_app: When `False`, don't create and manage a prompt_toolkit
178                       application. The default is `True` and should only be set
179                       to false if PythonInput is being embedded in a separate
180                       prompt_toolkit application.
181    """
182
183    def __init__(
184        self,
185        get_globals: Optional[_GetNamespace] = None,
186        get_locals: Optional[_GetNamespace] = None,
187        history_filename: Optional[str] = None,
188        vi_mode: bool = False,
189        color_depth: Optional[ColorDepth] = None,
190        # Input/output.
191        input: Optional[Input] = None,
192        output: Optional[Output] = None,
193        # For internal use.
194        extra_key_bindings: Optional[KeyBindings] = None,
195        create_app=True,
196        _completer: Optional[Completer] = None,
197        _validator: Optional[Validator] = None,
198        _lexer: Optional[Lexer] = None,
199        _extra_buffer_processors=None,
200        _extra_layout_body=None,
201        _extra_toolbars=None,
202        _input_buffer_height=None,
203    ) -> None:
204
205        self.get_globals: _GetNamespace = get_globals or (lambda: {})
206        self.get_locals: _GetNamespace = get_locals or self.get_globals
207
208        self.completer = _completer or PythonCompleter(
209            self.get_globals,
210            self.get_locals,
211            lambda: self.enable_dictionary_completion,
212        )
213
214        self._completer = HidePrivateCompleter(
215            # If fuzzy is enabled, first do fuzzy completion, but always add
216            # the non-fuzzy completions, if somehow the fuzzy completer didn't
217            # find them. (Due to the way the cursor position is moved in the
218            # fuzzy completer, some completions will not always be found by the
219            # fuzzy completer, but will be found with the normal completer.)
220            merge_completers(
221                [
222                    ConditionalCompleter(
223                        FuzzyCompleter(DynamicCompleter(lambda: self.completer)),
224                        Condition(lambda: self.enable_fuzzy_completion),
225                    ),
226                    DynamicCompleter(lambda: self.completer),
227                ],
228                deduplicate=True,
229            ),
230            lambda: self.complete_private_attributes,
231        )
232        self._validator = _validator or PythonValidator(self.get_compiler_flags)
233        self._lexer = PtpythonLexer(_lexer)
234
235        self.history: History
236        if history_filename:
237            self.history = ThreadedHistory(FileHistory(history_filename))
238        else:
239            self.history = InMemoryHistory()
240
241        self._input_buffer_height = _input_buffer_height
242        self._extra_layout_body = _extra_layout_body or []
243        self._extra_toolbars = _extra_toolbars or []
244        self._extra_buffer_processors = _extra_buffer_processors or []
245
246        self.extra_key_bindings = extra_key_bindings or KeyBindings()
247
248        # Settings.
249        self.title: AnyFormattedText = ""
250        self.show_signature: bool = False
251        self.show_docstring: bool = False
252        self.show_meta_enter_message: bool = True
253        self.completion_visualisation: CompletionVisualisation = (
254            CompletionVisualisation.MULTI_COLUMN
255        )
256        self.completion_menu_scroll_offset: int = 1
257
258        self.show_line_numbers: bool = False
259        self.show_status_bar: bool = True
260        self.wrap_lines: bool = True
261        self.complete_while_typing: bool = True
262        self.paste_mode: bool = (
263            False  # When True, don't insert whitespace after newline.
264        )
265        self.confirm_exit: bool = (
266            True  # Ask for confirmation when Control-D is pressed.
267        )
268        self.accept_input_on_enter: int = 2  # Accept when pressing Enter 'n' times.
269        # 'None' means that meta-enter is always required.
270        self.enable_open_in_editor: bool = True
271        self.enable_system_bindings: bool = True
272        self.enable_input_validation: bool = True
273        self.enable_auto_suggest: bool = False
274        self.enable_mouse_support: bool = False
275        self.enable_history_search: bool = False  # When True, like readline, going
276        # back in history will filter the
277        # history on the records starting
278        # with the current input.
279
280        self.enable_syntax_highlighting: bool = True
281        self.enable_fuzzy_completion: bool = False
282        self.enable_dictionary_completion: bool = False  # Also eval-based completion.
283        self.complete_private_attributes: CompletePrivateAttributes = (
284            CompletePrivateAttributes.ALWAYS
285        )
286        self.swap_light_and_dark: bool = False
287        self.highlight_matching_parenthesis: bool = False
288        self.show_sidebar: bool = False  # Currently show the sidebar.
289
290        # Pager.
291        self.enable_output_formatting: bool = False
292        self.enable_pager: bool = False
293
294        # When the sidebar is visible, also show the help text.
295        self.show_sidebar_help: bool = True
296
297        # Currently show 'Do you really want to exit?'
298        self.show_exit_confirmation: bool = False
299
300        # The title to be displayed in the terminal. (None or string.)
301        self.terminal_title: Optional[str] = None
302
303        self.exit_message: str = "Do you really want to exit?"
304        self.insert_blank_line_after_output: bool = True  # (For the REPL.)
305        self.insert_blank_line_after_input: bool = False  # (For the REPL.)
306
307        # The buffers.
308        self.default_buffer = self._create_buffer()
309        self.search_buffer: Buffer = Buffer()
310        self.docstring_buffer: Buffer = Buffer(read_only=True)
311
312        # Tokens to be shown at the prompt.
313        self.prompt_style: str = "classic"  # The currently active style.
314
315        # Styles selectable from the menu.
316        self.all_prompt_styles: Dict[str, PromptStyle] = {
317            "ipython": IPythonPrompt(self),
318            "classic": ClassicPrompt(),
319        }
320
321        self.get_input_prompt = lambda: self.all_prompt_styles[
322            self.prompt_style
323        ].in_prompt()
324
325        self.get_output_prompt = lambda: self.all_prompt_styles[
326            self.prompt_style
327        ].out_prompt()
328
329        #: Load styles.
330        self.code_styles: Dict[str, BaseStyle] = get_all_code_styles()
331        self.ui_styles = get_all_ui_styles()
332        self._current_code_style_name: str = "default"
333        self._current_ui_style_name: str = "default"
334
335        if is_windows():
336            self._current_code_style_name = "win32"
337
338        self._current_style = self._generate_style()
339        self.color_depth: ColorDepth = color_depth or ColorDepth.default()
340
341        self.max_brightness: float = 1.0
342        self.min_brightness: float = 0.0
343
344        # Options to be configurable from the sidebar.
345        self.options = self._create_options()
346        self.selected_option_index: int = 0
347
348        #: Incremeting integer counting the current statement.
349        self.current_statement_index: int = 1
350
351        # Code signatures. (This is set asynchronously after a timeout.)
352        self.signatures: List[Signature] = []
353
354        # Boolean indicating whether we have a signatures thread running.
355        # (Never run more than one at the same time.)
356        self._get_signatures_thread_running: bool = False
357
358        # Get into Vi navigation mode at startup
359        self.vi_start_in_navigation_mode: bool = False
360
361        # Preserve last used Vi input mode between main loop iterations
362        self.vi_keep_last_used_mode: bool = False
363
364        self.style_transformation = merge_style_transformations(
365            [
366                ConditionalStyleTransformation(
367                    SwapLightAndDarkStyleTransformation(),
368                    filter=Condition(lambda: self.swap_light_and_dark),
369                ),
370                AdjustBrightnessStyleTransformation(
371                    lambda: self.min_brightness, lambda: self.max_brightness
372                ),
373            ]
374        )
375        self.ptpython_layout = PtPythonLayout(
376            self,
377            lexer=DynamicLexer(
378                lambda: self._lexer
379                if self.enable_syntax_highlighting
380                else SimpleLexer()
381            ),
382            input_buffer_height=self._input_buffer_height,
383            extra_buffer_processors=self._extra_buffer_processors,
384            extra_body=self._extra_layout_body,
385            extra_toolbars=self._extra_toolbars,
386        )
387
388        # Create an app if requested. If not, the global get_app() is returned
389        # for self.app via property getter.
390        if create_app:
391            self._app: Optional[Application] = self._create_application(input, output)
392            # Setting vi_mode will not work unless the prompt_toolkit
393            # application has been created.
394            if vi_mode:
395                self.app.editing_mode = EditingMode.VI
396        else:
397            self._app = None
398
399    def _accept_handler(self, buff: Buffer) -> bool:
400        app = get_app()
401        app.exit(result=buff.text)
402        app.pre_run_callables.append(buff.reset)
403        return True  # Keep text, we call 'reset' later on.
404
405    @property
406    def option_count(self) -> int:
407        "Return the total amount of options. (In all categories together.)"
408        return sum(len(category.options) for category in self.options)
409
410    @property
411    def selected_option(self) -> Option:
412        "Return the currently selected option."
413        i = 0
414        for category in self.options:
415            for o in category.options:
416                if i == self.selected_option_index:
417                    return o
418                else:
419                    i += 1
420
421        raise ValueError("Nothing selected")
422
423    def get_compiler_flags(self) -> int:
424        """
425        Give the current compiler flags by looking for _Feature instances
426        in the globals.
427        """
428        flags = 0
429
430        for value in self.get_globals().values():
431            try:
432                if isinstance(value, __future__._Feature):
433                    f = value.compiler_flag
434                    flags |= f
435            except BaseException:
436                # get_compiler_flags should never raise to not run into an
437                # `Unhandled exception in event loop`
438
439                # See: https://github.com/prompt-toolkit/ptpython/issues/351
440                # An exception can be raised when some objects in the globals
441                # raise an exception in a custom `__getattribute__`.
442                pass
443
444        return flags
445
446    @property
447    def add_key_binding(self) -> Callable[[_T], _T]:
448        """
449        Shortcut for adding new key bindings.
450        (Mostly useful for a config.py file, that receives
451        a PythonInput/Repl instance as input.)
452
453        ::
454
455            @python_input.add_key_binding(Keys.ControlX, filter=...)
456            def handler(event):
457                ...
458        """
459
460        def add_binding_decorator(*k, **kw):
461            return self.extra_key_bindings.add(*k, **kw)
462
463        return add_binding_decorator
464
465    def install_code_colorscheme(self, name: str, style: BaseStyle) -> None:
466        """
467        Install a new code color scheme.
468        """
469        self.code_styles[name] = style
470
471    def use_code_colorscheme(self, name: str) -> None:
472        """
473        Apply new colorscheme. (By name.)
474        """
475        assert name in self.code_styles
476
477        self._current_code_style_name = name
478        self._current_style = self._generate_style()
479
480    def install_ui_colorscheme(self, name: str, style: BaseStyle) -> None:
481        """
482        Install a new UI color scheme.
483        """
484        self.ui_styles[name] = style
485
486    def use_ui_colorscheme(self, name: str) -> None:
487        """
488        Apply new colorscheme. (By name.)
489        """
490        assert name in self.ui_styles
491
492        self._current_ui_style_name = name
493        self._current_style = self._generate_style()
494
495    def _use_color_depth(self, depth: ColorDepth) -> None:
496        self.color_depth = depth
497
498    def _set_min_brightness(self, value: float) -> None:
499        self.min_brightness = value
500        self.max_brightness = max(self.max_brightness, value)
501
502    def _set_max_brightness(self, value: float) -> None:
503        self.max_brightness = value
504        self.min_brightness = min(self.min_brightness, value)
505
506    def _generate_style(self) -> BaseStyle:
507        """
508        Create new Style instance.
509        (We don't want to do this on every key press, because each time the
510        renderer receives a new style class, he will redraw everything.)
511        """
512        return generate_style(
513            self.code_styles[self._current_code_style_name],
514            self.ui_styles[self._current_ui_style_name],
515        )
516
517    def _create_options(self) -> List[OptionCategory]:
518        """
519        Create a list of `Option` instances for the options sidebar.
520        """
521
522        def enable(attribute: str, value: Any = True) -> bool:
523            setattr(self, attribute, value)
524
525            # Return `True`, to be able to chain this in the lambdas below.
526            return True
527
528        def disable(attribute: str) -> bool:
529            setattr(self, attribute, False)
530            return True
531
532        def simple_option(
533            title: str, description: str, field_name: str, values: Optional[List] = None
534        ) -> Option:
535            "Create Simple on/of option."
536            values = values or ["off", "on"]
537
538            def get_current_value():
539                return values[bool(getattr(self, field_name))]
540
541            def get_values():
542                return {
543                    values[1]: lambda: enable(field_name),
544                    values[0]: lambda: disable(field_name),
545                }
546
547            return Option(
548                title=title,
549                description=description,
550                get_values=get_values,
551                get_current_value=get_current_value,
552            )
553
554        brightness_values = [1.0 / 20 * value for value in range(0, 21)]
555
556        return [
557            OptionCategory(
558                "Input",
559                [
560                    Option(
561                        title="Editing mode",
562                        description="Vi or emacs key bindings.",
563                        get_current_value=lambda: ["Emacs", "Vi"][self.vi_mode],
564                        get_values=lambda: {
565                            "Emacs": lambda: disable("vi_mode"),
566                            "Vi": lambda: enable("vi_mode"),
567                        },
568                    ),
569                    simple_option(
570                        title="Paste mode",
571                        description="When enabled, don't indent automatically.",
572                        field_name="paste_mode",
573                    ),
574                    Option(
575                        title="Complete while typing",
576                        description="Generate autocompletions automatically while typing. "
577                        'Don\'t require pressing TAB. (Not compatible with "History search".)',
578                        get_current_value=lambda: ["off", "on"][
579                            self.complete_while_typing
580                        ],
581                        get_values=lambda: {
582                            "on": lambda: enable("complete_while_typing")
583                            and disable("enable_history_search"),
584                            "off": lambda: disable("complete_while_typing"),
585                        },
586                    ),
587                    Option(
588                        title="Complete private attrs",
589                        description="Show or hide private attributes in the completions. "
590                        "'If no public' means: show private attributes only if no public "
591                        "matches are found or if an underscore was typed.",
592                        get_current_value=lambda: {
593                            CompletePrivateAttributes.NEVER: "Never",
594                            CompletePrivateAttributes.ALWAYS: "Always",
595                            CompletePrivateAttributes.IF_NO_PUBLIC: "If no public",
596                        }[self.complete_private_attributes],
597                        get_values=lambda: {
598                            "Never": lambda: enable(
599                                "complete_private_attributes",
600                                CompletePrivateAttributes.NEVER,
601                            ),
602                            "Always": lambda: enable(
603                                "complete_private_attributes",
604                                CompletePrivateAttributes.ALWAYS,
605                            ),
606                            "If no public": lambda: enable(
607                                "complete_private_attributes",
608                                CompletePrivateAttributes.IF_NO_PUBLIC,
609                            ),
610                        },
611                    ),
612                    Option(
613                        title="Enable fuzzy completion",
614                        description="Enable fuzzy completion.",
615                        get_current_value=lambda: ["off", "on"][
616                            self.enable_fuzzy_completion
617                        ],
618                        get_values=lambda: {
619                            "on": lambda: enable("enable_fuzzy_completion"),
620                            "off": lambda: disable("enable_fuzzy_completion"),
621                        },
622                    ),
623                    Option(
624                        title="Dictionary completion",
625                        description="Enable experimental dictionary/list completion.\n"
626                        'WARNING: this does "eval" on fragments of\n'
627                        "         your Python input and is\n"
628                        "         potentially unsafe.",
629                        get_current_value=lambda: ["off", "on"][
630                            self.enable_dictionary_completion
631                        ],
632                        get_values=lambda: {
633                            "on": lambda: enable("enable_dictionary_completion"),
634                            "off": lambda: disable("enable_dictionary_completion"),
635                        },
636                    ),
637                    Option(
638                        title="History search",
639                        description="When pressing the up-arrow, filter the history on input starting "
640                        'with the current text. (Not compatible with "Complete while typing".)',
641                        get_current_value=lambda: ["off", "on"][
642                            self.enable_history_search
643                        ],
644                        get_values=lambda: {
645                            "on": lambda: enable("enable_history_search")
646                            and disable("complete_while_typing"),
647                            "off": lambda: disable("enable_history_search"),
648                        },
649                    ),
650                    simple_option(
651                        title="Mouse support",
652                        description="Respond to mouse clicks and scrolling for positioning the cursor, "
653                        "selecting text and scrolling through windows.",
654                        field_name="enable_mouse_support",
655                    ),
656                    simple_option(
657                        title="Confirm on exit",
658                        description="Require confirmation when exiting.",
659                        field_name="confirm_exit",
660                    ),
661                    simple_option(
662                        title="Input validation",
663                        description="In case of syntax errors, move the cursor to the error "
664                        "instead of showing a traceback of a SyntaxError.",
665                        field_name="enable_input_validation",
666                    ),
667                    simple_option(
668                        title="Auto suggestion",
669                        description="Auto suggest inputs by looking at the history. "
670                        "Pressing right arrow or Ctrl-E will complete the entry.",
671                        field_name="enable_auto_suggest",
672                    ),
673                    Option(
674                        title="Accept input on enter",
675                        description="Amount of ENTER presses required to execute input when the cursor "
676                        "is at the end of the input. (Note that META+ENTER will always execute.)",
677                        get_current_value=lambda: str(
678                            self.accept_input_on_enter or "meta-enter"
679                        ),
680                        get_values=lambda: {
681                            "2": lambda: enable("accept_input_on_enter", 2),
682                            "3": lambda: enable("accept_input_on_enter", 3),
683                            "4": lambda: enable("accept_input_on_enter", 4),
684                            "meta-enter": lambda: enable("accept_input_on_enter", None),
685                        },
686                    ),
687                ],
688            ),
689            OptionCategory(
690                "Display",
691                [
692                    Option(
693                        title="Completions",
694                        description="Visualisation to use for displaying the completions. (Multiple columns, one column, a toolbar or nothing.)",
695                        get_current_value=lambda: self.completion_visualisation.value,
696                        get_values=lambda: {
697                            CompletionVisualisation.NONE.value: lambda: enable(
698                                "completion_visualisation", CompletionVisualisation.NONE
699                            ),
700                            CompletionVisualisation.POP_UP.value: lambda: enable(
701                                "completion_visualisation",
702                                CompletionVisualisation.POP_UP,
703                            ),
704                            CompletionVisualisation.MULTI_COLUMN.value: lambda: enable(
705                                "completion_visualisation",
706                                CompletionVisualisation.MULTI_COLUMN,
707                            ),
708                            CompletionVisualisation.TOOLBAR.value: lambda: enable(
709                                "completion_visualisation",
710                                CompletionVisualisation.TOOLBAR,
711                            ),
712                        },
713                    ),
714                    Option(
715                        title="Prompt",
716                        description="Visualisation of the prompt. ('>>>' or 'In [1]:')",
717                        get_current_value=lambda: self.prompt_style,
718                        get_values=lambda: dict(
719                            (s, partial(enable, "prompt_style", s))
720                            for s in self.all_prompt_styles
721                        ),
722                    ),
723                    simple_option(
724                        title="Blank line after input",
725                        description="Insert a blank line after the input.",
726                        field_name="insert_blank_line_after_input",
727                    ),
728                    simple_option(
729                        title="Blank line after output",
730                        description="Insert a blank line after the output.",
731                        field_name="insert_blank_line_after_output",
732                    ),
733                    simple_option(
734                        title="Show signature",
735                        description="Display function signatures.",
736                        field_name="show_signature",
737                    ),
738                    simple_option(
739                        title="Show docstring",
740                        description="Display function docstrings.",
741                        field_name="show_docstring",
742                    ),
743                    simple_option(
744                        title="Show line numbers",
745                        description="Show line numbers when the input consists of multiple lines.",
746                        field_name="show_line_numbers",
747                    ),
748                    simple_option(
749                        title="Show Meta+Enter message",
750                        description="Show the [Meta+Enter] message when this key combination is required to execute commands. "
751                        + "(This is the case when a simple [Enter] key press will insert a newline.",
752                        field_name="show_meta_enter_message",
753                    ),
754                    simple_option(
755                        title="Wrap lines",
756                        description="Wrap lines instead of scrolling horizontally.",
757                        field_name="wrap_lines",
758                    ),
759                    simple_option(
760                        title="Show status bar",
761                        description="Show the status bar at the bottom of the terminal.",
762                        field_name="show_status_bar",
763                    ),
764                    simple_option(
765                        title="Show sidebar help",
766                        description="When the sidebar is visible, also show this help text.",
767                        field_name="show_sidebar_help",
768                    ),
769                    simple_option(
770                        title="Highlight parenthesis",
771                        description="Highlight matching parenthesis, when the cursor is on or right after one.",
772                        field_name="highlight_matching_parenthesis",
773                    ),
774                    simple_option(
775                        title="Reformat output (black)",
776                        description="Reformat outputs using Black, if possible (experimental).",
777                        field_name="enable_output_formatting",
778                    ),
779                    simple_option(
780                        title="Enable pager for output",
781                        description="Use a pager for displaying outputs that don't "
782                        "fit on the screen.",
783                        field_name="enable_pager",
784                    ),
785                ],
786            ),
787            OptionCategory(
788                "Colors",
789                [
790                    simple_option(
791                        title="Syntax highlighting",
792                        description="Use colors for syntax highligthing",
793                        field_name="enable_syntax_highlighting",
794                    ),
795                    simple_option(
796                        title="Swap light/dark colors",
797                        description="Swap light and dark colors.",
798                        field_name="swap_light_and_dark",
799                    ),
800                    Option(
801                        title="Code",
802                        description="Color scheme to use for the Python code.",
803                        get_current_value=lambda: self._current_code_style_name,
804                        get_values=lambda: {
805                            name: partial(self.use_code_colorscheme, name)
806                            for name in self.code_styles
807                        },
808                    ),
809                    Option(
810                        title="User interface",
811                        description="Color scheme to use for the user interface.",
812                        get_current_value=lambda: self._current_ui_style_name,
813                        get_values=lambda: dict(
814                            (name, partial(self.use_ui_colorscheme, name))
815                            for name in self.ui_styles
816                        ),
817                    ),
818                    Option(
819                        title="Color depth",
820                        description="Monochrome (1 bit), 16 ANSI colors (4 bit),\n256 colors (8 bit), or 24 bit.",
821                        get_current_value=lambda: COLOR_DEPTHS[self.color_depth],
822                        get_values=lambda: {
823                            name: partial(self._use_color_depth, depth)
824                            for depth, name in COLOR_DEPTHS.items()
825                        },
826                    ),
827                    Option(
828                        title="Min brightness",
829                        description="Minimum brightness for the color scheme (default=0.0).",
830                        get_current_value=lambda: "%.2f" % self.min_brightness,
831                        get_values=lambda: {
832                            "%.2f" % value: partial(self._set_min_brightness, value)
833                            for value in brightness_values
834                        },
835                    ),
836                    Option(
837                        title="Max brightness",
838                        description="Maximum brightness for the color scheme (default=1.0).",
839                        get_current_value=lambda: "%.2f" % self.max_brightness,
840                        get_values=lambda: {
841                            "%.2f" % value: partial(self._set_max_brightness, value)
842                            for value in brightness_values
843                        },
844                    ),
845                ],
846            ),
847        ]
848
849    def _create_application(
850        self, input: Optional[Input], output: Optional[Output]
851    ) -> Application:
852        """
853        Create an `Application` instance.
854        """
855        return Application(
856            layout=self.ptpython_layout.layout,
857            key_bindings=merge_key_bindings(
858                [
859                    load_python_bindings(self),
860                    load_auto_suggest_bindings(),
861                    load_sidebar_bindings(self),
862                    load_confirm_exit_bindings(self),
863                    ConditionalKeyBindings(
864                        load_open_in_editor_bindings(),
865                        Condition(lambda: self.enable_open_in_editor),
866                    ),
867                    # Extra key bindings should not be active when the sidebar is visible.
868                    ConditionalKeyBindings(
869                        self.extra_key_bindings,
870                        Condition(lambda: not self.show_sidebar),
871                    ),
872                ]
873            ),
874            color_depth=lambda: self.color_depth,
875            paste_mode=Condition(lambda: self.paste_mode),
876            mouse_support=Condition(lambda: self.enable_mouse_support),
877            style=DynamicStyle(lambda: self._current_style),
878            style_transformation=self.style_transformation,
879            include_default_pygments_style=False,
880            reverse_vi_search_direction=True,
881            input=input,
882            output=output,
883        )
884
885    def _create_buffer(self) -> Buffer:
886        """
887        Create the `Buffer` for the Python input.
888        """
889        python_buffer = Buffer(
890            name=DEFAULT_BUFFER,
891            complete_while_typing=Condition(lambda: self.complete_while_typing),
892            enable_history_search=Condition(lambda: self.enable_history_search),
893            tempfile_suffix=".py",
894            history=self.history,
895            completer=ThreadedCompleter(self._completer),
896            validator=ConditionalValidator(
897                self._validator, Condition(lambda: self.enable_input_validation)
898            ),
899            auto_suggest=ConditionalAutoSuggest(
900                ThreadedAutoSuggest(AutoSuggestFromHistory()),
901                Condition(lambda: self.enable_auto_suggest),
902            ),
903            accept_handler=self._accept_handler,
904            on_text_changed=self._on_input_timeout,
905        )
906
907        return python_buffer
908
909    @property
910    def editing_mode(self) -> EditingMode:
911        return self.app.editing_mode
912
913    @editing_mode.setter
914    def editing_mode(self, value: EditingMode) -> None:
915        self.app.editing_mode = value
916
917    @property
918    def vi_mode(self) -> bool:
919        return self.editing_mode == EditingMode.VI
920
921    @vi_mode.setter
922    def vi_mode(self, value: bool) -> None:
923        if value:
924            self.editing_mode = EditingMode.VI
925        else:
926            self.editing_mode = EditingMode.EMACS
927
928    @property
929    def app(self) -> Application:
930        if self._app is None:
931            return get_app()
932        return self._app
933
934    def _on_input_timeout(self, buff: Buffer) -> None:
935        """
936        When there is no input activity,
937        in another thread, get the signature of the current code.
938        """
939
940        def get_signatures_in_executor(document: Document) -> List[Signature]:
941            # First, get signatures from Jedi. If we didn't found any and if
942            # "dictionary completion" (eval-based completion) is enabled, then
943            # get signatures using eval.
944            signatures = get_signatures_using_jedi(
945                document, self.get_locals(), self.get_globals()
946            )
947            if not signatures and self.enable_dictionary_completion:
948                signatures = get_signatures_using_eval(
949                    document, self.get_locals(), self.get_globals()
950                )
951
952            return signatures
953
954        app = self.app
955
956        async def on_timeout_task() -> None:
957            loop = get_event_loop()
958
959            # Never run multiple get-signature threads.
960            if self._get_signatures_thread_running:
961                return
962            self._get_signatures_thread_running = True
963
964            try:
965                while True:
966                    document = buff.document
967                    signatures = await loop.run_in_executor(
968                        None, get_signatures_in_executor, document
969                    )
970
971                    # If the text didn't change in the meantime, take these
972                    # signatures. Otherwise, try again.
973                    if buff.text == document.text:
974                        break
975            finally:
976                self._get_signatures_thread_running = False
977
978            # Set signatures and redraw.
979            self.signatures = signatures
980
981            # Set docstring in docstring buffer.
982            if signatures:
983                self.docstring_buffer.reset(
984                    document=Document(signatures[0].docstring, cursor_position=0)
985                )
986            else:
987                self.docstring_buffer.reset()
988
989            app.invalidate()
990
991        if app.is_running:
992            app.create_background_task(on_timeout_task())
993
994    def on_reset(self) -> None:
995        self.signatures = []
996
997    def enter_history(self) -> None:
998        """
999        Display the history.
1000        """
1001        app = self.app
1002        app.vi_state.input_mode = InputMode.NAVIGATION
1003
1004        history = PythonHistory(self, self.default_buffer.document)
1005
1006        import asyncio
1007
1008        from prompt_toolkit.application import in_terminal
1009
1010        async def do_in_terminal() -> None:
1011            async with in_terminal():
1012                result = await history.app.run_async()
1013                if result is not None:
1014                    self.default_buffer.text = result
1015
1016                app.vi_state.input_mode = InputMode.INSERT
1017
1018        asyncio.ensure_future(do_in_terminal())
1019
1020    def read(self) -> str:
1021        """
1022        Read the input.
1023
1024        This will run the Python input user interface in another thread, wait
1025        for input to be accepted and return that. By running the UI in another
1026        thread, we avoid issues regarding possibly nested event loops.
1027
1028        This can raise EOFError, when Control-D is pressed.
1029        """
1030        # Capture the current input_mode in order to restore it after reset,
1031        # for ViState.reset() sets it to InputMode.INSERT unconditionally and
1032        # doesn't accept any arguments.
1033        def pre_run(
1034            last_input_mode: InputMode = self.app.vi_state.input_mode,
1035        ) -> None:
1036            if self.vi_keep_last_used_mode:
1037                self.app.vi_state.input_mode = last_input_mode
1038
1039            if not self.vi_keep_last_used_mode and self.vi_start_in_navigation_mode:
1040                self.app.vi_state.input_mode = InputMode.NAVIGATION
1041
1042        # Run the UI.
1043        while True:
1044            try:
1045                result = self.app.run(pre_run=pre_run, in_thread=True)
1046
1047                if result.lstrip().startswith("\x1a"):
1048                    # When the input starts with Ctrl-Z, quit the REPL.
1049                    # (Important for Windows users.)
1050                    raise EOFError
1051
1052                # Remove leading whitespace.
1053                # (Users can add extra indentation, which happens for
1054                # instance because of copy/pasting code.)
1055                result = unindent_code(result)
1056
1057                if result and not result.isspace():
1058                    if self.insert_blank_line_after_input:
1059                        self.app.output.write("\n")
1060
1061                    return result
1062            except KeyboardInterrupt:
1063                # Abort - try again.
1064                self.default_buffer.document = Document()
1065