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