##// END OF EJS Templates
DOCS: Clarify emacs details wrt simple_prompt
Niall Dooley -
Show More
@@ -1,1013 +1,1016 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2
3 3 import os
4 4 import sys
5 5 import inspect
6 6 from warnings import warn
7 7 from typing import Union as UnionType, Optional
8 8
9 9 from IPython.core.async_helpers import get_asyncio_loop
10 10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
11 11 from IPython.utils.py3compat import input
12 12 from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
13 13 from IPython.utils.process import abbrev_cwd
14 14 from traitlets import (
15 15 Bool,
16 16 Unicode,
17 17 Dict,
18 18 Integer,
19 19 List,
20 20 observe,
21 21 Instance,
22 22 Type,
23 23 default,
24 24 Enum,
25 25 Union,
26 26 Any,
27 27 validate,
28 28 Float,
29 29 )
30 30
31 31 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
32 32 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
33 33 from prompt_toolkit.filters import HasFocus, Condition, IsDone
34 34 from prompt_toolkit.formatted_text import PygmentsTokens
35 35 from prompt_toolkit.history import History
36 36 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
37 37 from prompt_toolkit.output import ColorDepth
38 38 from prompt_toolkit.patch_stdout import patch_stdout
39 39 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
40 40 from prompt_toolkit.styles import DynamicStyle, merge_styles
41 41 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
42 42 from prompt_toolkit import __version__ as ptk_version
43 43
44 44 from pygments.styles import get_style_by_name
45 45 from pygments.style import Style
46 46 from pygments.token import Token
47 47
48 48 from .debugger import TerminalPdb, Pdb
49 49 from .magics import TerminalMagics
50 50 from .pt_inputhooks import get_inputhook_name_and_func
51 51 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
52 52 from .ptutils import IPythonPTCompleter, IPythonPTLexer
53 53 from .shortcuts import (
54 54 KEY_BINDINGS,
55 55 create_ipython_shortcuts,
56 56 create_identifier,
57 57 RuntimeBinding,
58 58 add_binding,
59 59 )
60 60 from .shortcuts.filters import KEYBINDING_FILTERS, filter_from_string
61 61 from .shortcuts.auto_suggest import (
62 62 NavigableAutoSuggestFromHistory,
63 63 AppendAutoSuggestionInAnyLine,
64 64 )
65 65
66 66 PTK3 = ptk_version.startswith('3.')
67 67
68 68
69 69 class _NoStyle(Style):
70 70 pass
71 71
72 72
73 73 _style_overrides_light_bg = {
74 74 Token.Prompt: '#ansibrightblue',
75 75 Token.PromptNum: '#ansiblue bold',
76 76 Token.OutPrompt: '#ansibrightred',
77 77 Token.OutPromptNum: '#ansired bold',
78 78 }
79 79
80 80 _style_overrides_linux = {
81 81 Token.Prompt: '#ansibrightgreen',
82 82 Token.PromptNum: '#ansigreen bold',
83 83 Token.OutPrompt: '#ansibrightred',
84 84 Token.OutPromptNum: '#ansired bold',
85 85 }
86 86
87 87
88 88 def _backward_compat_continuation_prompt_tokens(method, width: int, *, lineno: int):
89 89 """
90 90 Sagemath use custom prompt and we broke them in 8.19.
91 91 """
92 92 sig = inspect.signature(method)
93 93 if "lineno" in inspect.signature(method).parameters or any(
94 94 [p.kind == p.VAR_KEYWORD for p in sig.parameters.values()]
95 95 ):
96 96 return method(width, lineno=lineno)
97 97 else:
98 98 return method(width)
99 99
100 100
101 101 def get_default_editor():
102 102 try:
103 103 return os.environ['EDITOR']
104 104 except KeyError:
105 105 pass
106 106 except UnicodeError:
107 107 warn("$EDITOR environment variable is not pure ASCII. Using platform "
108 108 "default editor.")
109 109
110 110 if os.name == 'posix':
111 111 return 'vi' # the only one guaranteed to be there!
112 112 else:
113 113 return "notepad" # same in Windows!
114 114
115 115
116 116 # conservatively check for tty
117 117 # overridden streams can result in things like:
118 118 # - sys.stdin = None
119 119 # - no isatty method
120 120 for _name in ('stdin', 'stdout', 'stderr'):
121 121 _stream = getattr(sys, _name)
122 122 try:
123 123 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
124 124 _is_tty = False
125 125 break
126 126 except ValueError:
127 127 # stream is closed
128 128 _is_tty = False
129 129 break
130 130 else:
131 131 _is_tty = True
132 132
133 133
134 134 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
135 135
136 136 def black_reformat_handler(text_before_cursor):
137 137 """
138 138 We do not need to protect against error,
139 139 this is taken care at a higher level where any reformat error is ignored.
140 140 Indeed we may call reformatting on incomplete code.
141 141 """
142 142 import black
143 143
144 144 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
145 145 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
146 146 formatted_text = formatted_text[:-1]
147 147 return formatted_text
148 148
149 149
150 150 def yapf_reformat_handler(text_before_cursor):
151 151 from yapf.yapflib import file_resources
152 152 from yapf.yapflib import yapf_api
153 153
154 154 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
155 155 formatted_text, was_formatted = yapf_api.FormatCode(
156 156 text_before_cursor, style_config=style_config
157 157 )
158 158 if was_formatted:
159 159 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
160 160 formatted_text = formatted_text[:-1]
161 161 return formatted_text
162 162 else:
163 163 return text_before_cursor
164 164
165 165
166 166 class PtkHistoryAdapter(History):
167 167 """
168 168 Prompt toolkit has it's own way of handling history, Where it assumes it can
169 169 Push/pull from history.
170 170
171 171 """
172 172
173 173 def __init__(self, shell):
174 174 super().__init__()
175 175 self.shell = shell
176 176 self._refresh()
177 177
178 178 def append_string(self, string):
179 179 # we rely on sql for that.
180 180 self._loaded = False
181 181 self._refresh()
182 182
183 183 def _refresh(self):
184 184 if not self._loaded:
185 185 self._loaded_strings = list(self.load_history_strings())
186 186
187 187 def load_history_strings(self):
188 188 last_cell = ""
189 189 res = []
190 190 for __, ___, cell in self.shell.history_manager.get_tail(
191 191 self.shell.history_load_length, include_latest=True
192 192 ):
193 193 # Ignore blank lines and consecutive duplicates
194 194 cell = cell.rstrip()
195 195 if cell and (cell != last_cell):
196 196 res.append(cell)
197 197 last_cell = cell
198 198 yield from res[::-1]
199 199
200 200 def store_string(self, string: str) -> None:
201 201 pass
202 202
203 203 class TerminalInteractiveShell(InteractiveShell):
204 204 mime_renderers = Dict().tag(config=True)
205 205
206 206 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
207 207 'to reserve for the tab completion menu, '
208 208 'search history, ...etc, the height of '
209 209 'these menus will at most this value. '
210 210 'Increase it is you prefer long and skinny '
211 211 'menus, decrease for short and wide.'
212 212 ).tag(config=True)
213 213
214 214 pt_app: UnionType[PromptSession, None] = None
215 215 auto_suggest: UnionType[
216 216 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
217 217 ] = None
218 218 debugger_history = None
219 219
220 220 debugger_history_file = Unicode(
221 221 "~/.pdbhistory", help="File in which to store and read history"
222 222 ).tag(config=True)
223 223
224 224 simple_prompt = Bool(_use_simple_prompt,
225 225 help="""Use `raw_input` for the REPL, without completion and prompt colors.
226 226
227 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
228 IPython own testing machinery, and emacs inferior-python integration through elpy.
227 Useful when controlling IPython as a subprocess, and piping
228 STDIN/OUT/ERR. Known usage are: IPython's own testing machinery,
229 and emacs' inferior-python subprocess (assuming you have set
230 `python-shell-interpreter` to "ipython") available through the
231 built-in `M-x run-python` and third party packages such as elpy.
229 232
230 233 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
231 234 environment variable is set, or the current terminal is not a tty.
232 235 Thus the Default value reported in --help-all, or config will often
233 236 be incorrectly reported.
234 237 """,
235 238 ).tag(config=True)
236 239
237 240 @property
238 241 def debugger_cls(self):
239 242 return Pdb if self.simple_prompt else TerminalPdb
240 243
241 244 confirm_exit = Bool(True,
242 245 help="""
243 246 Set to confirm when you try to exit IPython with an EOF (Control-D
244 247 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
245 248 you can force a direct exit without any confirmation.""",
246 249 ).tag(config=True)
247 250
248 251 editing_mode = Unicode('emacs',
249 252 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
250 253 ).tag(config=True)
251 254
252 255 emacs_bindings_in_vi_insert_mode = Bool(
253 256 True,
254 257 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
255 258 ).tag(config=True)
256 259
257 260 modal_cursor = Bool(
258 261 True,
259 262 help="""
260 263 Cursor shape changes depending on vi mode: beam in vi insert mode,
261 264 block in nav mode, underscore in replace mode.""",
262 265 ).tag(config=True)
263 266
264 267 ttimeoutlen = Float(
265 268 0.01,
266 269 help="""The time in milliseconds that is waited for a key code
267 270 to complete.""",
268 271 ).tag(config=True)
269 272
270 273 timeoutlen = Float(
271 274 0.5,
272 275 help="""The time in milliseconds that is waited for a mapped key
273 276 sequence to complete.""",
274 277 ).tag(config=True)
275 278
276 279 autoformatter = Unicode(
277 280 None,
278 281 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
279 282 allow_none=True
280 283 ).tag(config=True)
281 284
282 285 auto_match = Bool(
283 286 False,
284 287 help="""
285 288 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
286 289 Brackets: (), [], {}
287 290 Quotes: '', \"\"
288 291 """,
289 292 ).tag(config=True)
290 293
291 294 mouse_support = Bool(False,
292 295 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
293 296 ).tag(config=True)
294 297
295 298 # We don't load the list of styles for the help string, because loading
296 299 # Pygments plugins takes time and can cause unexpected errors.
297 300 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
298 301 help="""The name or class of a Pygments style to use for syntax
299 302 highlighting. To see available styles, run `pygmentize -L styles`."""
300 303 ).tag(config=True)
301 304
302 305 @validate('editing_mode')
303 306 def _validate_editing_mode(self, proposal):
304 307 if proposal['value'].lower() == 'vim':
305 308 proposal['value']= 'vi'
306 309 elif proposal['value'].lower() == 'default':
307 310 proposal['value']= 'emacs'
308 311
309 312 if hasattr(EditingMode, proposal['value'].upper()):
310 313 return proposal['value'].lower()
311 314
312 315 return self.editing_mode
313 316
314 317 @observe('editing_mode')
315 318 def _editing_mode(self, change):
316 319 if self.pt_app:
317 320 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
318 321
319 322 def _set_formatter(self, formatter):
320 323 if formatter is None:
321 324 self.reformat_handler = lambda x:x
322 325 elif formatter == 'black':
323 326 self.reformat_handler = black_reformat_handler
324 327 elif formatter == "yapf":
325 328 self.reformat_handler = yapf_reformat_handler
326 329 else:
327 330 raise ValueError
328 331
329 332 @observe("autoformatter")
330 333 def _autoformatter_changed(self, change):
331 334 formatter = change.new
332 335 self._set_formatter(formatter)
333 336
334 337 @observe('highlighting_style')
335 338 @observe('colors')
336 339 def _highlighting_style_changed(self, change):
337 340 self.refresh_style()
338 341
339 342 def refresh_style(self):
340 343 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
341 344
342 345 highlighting_style_overrides = Dict(
343 346 help="Override highlighting format for specific tokens"
344 347 ).tag(config=True)
345 348
346 349 true_color = Bool(False,
347 350 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
348 351 If your terminal supports true color, the following command should
349 352 print ``TRUECOLOR`` in orange::
350 353
351 354 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
352 355 """,
353 356 ).tag(config=True)
354 357
355 358 editor = Unicode(get_default_editor(),
356 359 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
357 360 ).tag(config=True)
358 361
359 362 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
360 363
361 364 prompts = Instance(Prompts)
362 365
363 366 @default('prompts')
364 367 def _prompts_default(self):
365 368 return self.prompts_class(self)
366 369
367 370 # @observe('prompts')
368 371 # def _(self, change):
369 372 # self._update_layout()
370 373
371 374 @default('displayhook_class')
372 375 def _displayhook_class_default(self):
373 376 return RichPromptDisplayHook
374 377
375 378 term_title = Bool(True,
376 379 help="Automatically set the terminal title"
377 380 ).tag(config=True)
378 381
379 382 term_title_format = Unicode("IPython: {cwd}",
380 383 help="Customize the terminal title format. This is a python format string. " +
381 384 "Available substitutions are: {cwd}."
382 385 ).tag(config=True)
383 386
384 387 display_completions = Enum(('column', 'multicolumn','readlinelike'),
385 388 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
386 389 "'readlinelike'. These options are for `prompt_toolkit`, see "
387 390 "`prompt_toolkit` documentation for more information."
388 391 ),
389 392 default_value='multicolumn').tag(config=True)
390 393
391 394 highlight_matching_brackets = Bool(True,
392 395 help="Highlight matching brackets.",
393 396 ).tag(config=True)
394 397
395 398 extra_open_editor_shortcuts = Bool(False,
396 399 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
397 400 "This is in addition to the F2 binding, which is always enabled."
398 401 ).tag(config=True)
399 402
400 403 handle_return = Any(None,
401 404 help="Provide an alternative handler to be called when the user presses "
402 405 "Return. This is an advanced option intended for debugging, which "
403 406 "may be changed or removed in later releases."
404 407 ).tag(config=True)
405 408
406 409 enable_history_search = Bool(True,
407 410 help="Allows to enable/disable the prompt toolkit history search"
408 411 ).tag(config=True)
409 412
410 413 autosuggestions_provider = Unicode(
411 414 "NavigableAutoSuggestFromHistory",
412 415 help="Specifies from which source automatic suggestions are provided. "
413 416 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
414 417 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
415 418 " or ``None`` to disable automatic suggestions. "
416 419 "Default is `'NavigableAutoSuggestFromHistory`'.",
417 420 allow_none=True,
418 421 ).tag(config=True)
419 422
420 423 def _set_autosuggestions(self, provider):
421 424 # disconnect old handler
422 425 if self.auto_suggest and isinstance(
423 426 self.auto_suggest, NavigableAutoSuggestFromHistory
424 427 ):
425 428 self.auto_suggest.disconnect()
426 429 if provider is None:
427 430 self.auto_suggest = None
428 431 elif provider == "AutoSuggestFromHistory":
429 432 self.auto_suggest = AutoSuggestFromHistory()
430 433 elif provider == "NavigableAutoSuggestFromHistory":
431 434 self.auto_suggest = NavigableAutoSuggestFromHistory()
432 435 else:
433 436 raise ValueError("No valid provider.")
434 437 if self.pt_app:
435 438 self.pt_app.auto_suggest = self.auto_suggest
436 439
437 440 @observe("autosuggestions_provider")
438 441 def _autosuggestions_provider_changed(self, change):
439 442 provider = change.new
440 443 self._set_autosuggestions(provider)
441 444
442 445 shortcuts = List(
443 446 trait=Dict(
444 447 key_trait=Enum(
445 448 [
446 449 "command",
447 450 "match_keys",
448 451 "match_filter",
449 452 "new_keys",
450 453 "new_filter",
451 454 "create",
452 455 ]
453 456 ),
454 457 per_key_traits={
455 458 "command": Unicode(),
456 459 "match_keys": List(Unicode()),
457 460 "match_filter": Unicode(),
458 461 "new_keys": List(Unicode()),
459 462 "new_filter": Unicode(),
460 463 "create": Bool(False),
461 464 },
462 465 ),
463 466 help="""Add, disable or modifying shortcuts.
464 467
465 468 Each entry on the list should be a dictionary with ``command`` key
466 469 identifying the target function executed by the shortcut and at least
467 470 one of the following:
468 471
469 472 - ``match_keys``: list of keys used to match an existing shortcut,
470 473 - ``match_filter``: shortcut filter used to match an existing shortcut,
471 474 - ``new_keys``: list of keys to set,
472 475 - ``new_filter``: a new shortcut filter to set
473 476
474 477 The filters have to be composed of pre-defined verbs and joined by one
475 478 of the following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not).
476 479 The pre-defined verbs are:
477 480
478 481 {}
479 482
480 483
481 484 To disable a shortcut set ``new_keys`` to an empty list.
482 485 To add a shortcut add key ``create`` with value ``True``.
483 486
484 487 When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can
485 488 be omitted if the provided specification uniquely identifies a shortcut
486 489 to be modified/disabled. When modifying a shortcut ``new_filter`` or
487 490 ``new_keys`` can be omitted which will result in reuse of the existing
488 491 filter/keys.
489 492
490 493 Only shortcuts defined in IPython (and not default prompt-toolkit
491 494 shortcuts) can be modified or disabled. The full list of shortcuts,
492 495 command identifiers and filters is available under
493 496 :ref:`terminal-shortcuts-list`.
494 497 """.format(
495 498 "\n ".join([f"- `{k}`" for k in KEYBINDING_FILTERS])
496 499 ),
497 500 ).tag(config=True)
498 501
499 502 @observe("shortcuts")
500 503 def _shortcuts_changed(self, change):
501 504 if self.pt_app:
502 505 self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
503 506
504 507 def _merge_shortcuts(self, user_shortcuts):
505 508 # rebuild the bindings list from scratch
506 509 key_bindings = create_ipython_shortcuts(self)
507 510
508 511 # for now we only allow adding shortcuts for commands which are already
509 512 # registered; this is a security precaution.
510 513 known_commands = {
511 514 create_identifier(binding.command): binding.command
512 515 for binding in KEY_BINDINGS
513 516 }
514 517 shortcuts_to_skip = []
515 518 shortcuts_to_add = []
516 519
517 520 for shortcut in user_shortcuts:
518 521 command_id = shortcut["command"]
519 522 if command_id not in known_commands:
520 523 allowed_commands = "\n - ".join(known_commands)
521 524 raise ValueError(
522 525 f"{command_id} is not a known shortcut command."
523 526 f" Allowed commands are: \n - {allowed_commands}"
524 527 )
525 528 old_keys = shortcut.get("match_keys", None)
526 529 old_filter = (
527 530 filter_from_string(shortcut["match_filter"])
528 531 if "match_filter" in shortcut
529 532 else None
530 533 )
531 534 matching = [
532 535 binding
533 536 for binding in KEY_BINDINGS
534 537 if (
535 538 (old_filter is None or binding.filter == old_filter)
536 539 and (old_keys is None or [k for k in binding.keys] == old_keys)
537 540 and create_identifier(binding.command) == command_id
538 541 )
539 542 ]
540 543
541 544 new_keys = shortcut.get("new_keys", None)
542 545 new_filter = shortcut.get("new_filter", None)
543 546
544 547 command = known_commands[command_id]
545 548
546 549 creating_new = shortcut.get("create", False)
547 550 modifying_existing = not creating_new and (
548 551 new_keys is not None or new_filter
549 552 )
550 553
551 554 if creating_new and new_keys == []:
552 555 raise ValueError("Cannot add a shortcut without keys")
553 556
554 557 if modifying_existing:
555 558 specification = {
556 559 key: shortcut[key]
557 560 for key in ["command", "filter"]
558 561 if key in shortcut
559 562 }
560 563 if len(matching) == 0:
561 564 raise ValueError(
562 565 f"No shortcuts matching {specification} found in {KEY_BINDINGS}"
563 566 )
564 567 elif len(matching) > 1:
565 568 raise ValueError(
566 569 f"Multiple shortcuts matching {specification} found,"
567 570 f" please add keys/filter to select one of: {matching}"
568 571 )
569 572
570 573 matched = matching[0]
571 574 old_filter = matched.filter
572 575 old_keys = list(matched.keys)
573 576 shortcuts_to_skip.append(
574 577 RuntimeBinding(
575 578 command,
576 579 keys=old_keys,
577 580 filter=old_filter,
578 581 )
579 582 )
580 583
581 584 if new_keys != []:
582 585 shortcuts_to_add.append(
583 586 RuntimeBinding(
584 587 command,
585 588 keys=new_keys or old_keys,
586 589 filter=filter_from_string(new_filter)
587 590 if new_filter is not None
588 591 else (
589 592 old_filter
590 593 if old_filter is not None
591 594 else filter_from_string("always")
592 595 ),
593 596 )
594 597 )
595 598
596 599 # rebuild the bindings list from scratch
597 600 key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
598 601 for binding in shortcuts_to_add:
599 602 add_binding(key_bindings, binding)
600 603
601 604 return key_bindings
602 605
603 606 prompt_includes_vi_mode = Bool(True,
604 607 help="Display the current vi mode (when using vi editing mode)."
605 608 ).tag(config=True)
606 609
607 610 prompt_line_number_format = Unicode(
608 611 "",
609 612 help="The format for line numbering, will be passed `line` (int, 1 based)"
610 613 " the current line number and `rel_line` the relative line number."
611 614 " for example to display both you can use the following template string :"
612 615 " c.TerminalInteractiveShell.prompt_line_number_format='{line: 4d}/{rel_line:+03d} | '"
613 616 " This will display the current line number, with leading space and a width of at least 4"
614 617 " character, as well as the relative line number 0 padded and always with a + or - sign."
615 618 " Note that when using Emacs mode the prompt of the first line may not update.",
616 619 ).tag(config=True)
617 620
618 621 @observe('term_title')
619 622 def init_term_title(self, change=None):
620 623 # Enable or disable the terminal title.
621 624 if self.term_title and _is_tty:
622 625 toggle_set_term_title(True)
623 626 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
624 627 else:
625 628 toggle_set_term_title(False)
626 629
627 630 def restore_term_title(self):
628 631 if self.term_title and _is_tty:
629 632 restore_term_title()
630 633
631 634 def init_display_formatter(self):
632 635 super(TerminalInteractiveShell, self).init_display_formatter()
633 636 # terminal only supports plain text
634 637 self.display_formatter.active_types = ["text/plain"]
635 638
636 639 def init_prompt_toolkit_cli(self):
637 640 if self.simple_prompt:
638 641 # Fall back to plain non-interactive output for tests.
639 642 # This is very limited.
640 643 def prompt():
641 644 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
642 645 lines = [input(prompt_text)]
643 646 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
644 647 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
645 648 lines.append( input(prompt_continuation) )
646 649 return '\n'.join(lines)
647 650 self.prompt_for_code = prompt
648 651 return
649 652
650 653 # Set up keyboard shortcuts
651 654 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
652 655
653 656 # Pre-populate history from IPython's history database
654 657 history = PtkHistoryAdapter(self)
655 658
656 659 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
657 660 self.style = DynamicStyle(lambda: self._style)
658 661
659 662 editing_mode = getattr(EditingMode, self.editing_mode.upper())
660 663
661 664 self._use_asyncio_inputhook = False
662 665 self.pt_app = PromptSession(
663 666 auto_suggest=self.auto_suggest,
664 667 editing_mode=editing_mode,
665 668 key_bindings=key_bindings,
666 669 history=history,
667 670 completer=IPythonPTCompleter(shell=self),
668 671 enable_history_search=self.enable_history_search,
669 672 style=self.style,
670 673 include_default_pygments_style=False,
671 674 mouse_support=self.mouse_support,
672 675 enable_open_in_editor=self.extra_open_editor_shortcuts,
673 676 color_depth=self.color_depth,
674 677 tempfile_suffix=".py",
675 678 **self._extra_prompt_options(),
676 679 )
677 680 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
678 681 self.auto_suggest.connect(self.pt_app)
679 682
680 683 def _make_style_from_name_or_cls(self, name_or_cls):
681 684 """
682 685 Small wrapper that make an IPython compatible style from a style name
683 686
684 687 We need that to add style for prompt ... etc.
685 688 """
686 689 style_overrides = {}
687 690 if name_or_cls == 'legacy':
688 691 legacy = self.colors.lower()
689 692 if legacy == 'linux':
690 693 style_cls = get_style_by_name('monokai')
691 694 style_overrides = _style_overrides_linux
692 695 elif legacy == 'lightbg':
693 696 style_overrides = _style_overrides_light_bg
694 697 style_cls = get_style_by_name('pastie')
695 698 elif legacy == 'neutral':
696 699 # The default theme needs to be visible on both a dark background
697 700 # and a light background, because we can't tell what the terminal
698 701 # looks like. These tweaks to the default theme help with that.
699 702 style_cls = get_style_by_name('default')
700 703 style_overrides.update({
701 704 Token.Number: '#ansigreen',
702 705 Token.Operator: 'noinherit',
703 706 Token.String: '#ansiyellow',
704 707 Token.Name.Function: '#ansiblue',
705 708 Token.Name.Class: 'bold #ansiblue',
706 709 Token.Name.Namespace: 'bold #ansiblue',
707 710 Token.Name.Variable.Magic: '#ansiblue',
708 711 Token.Prompt: '#ansigreen',
709 712 Token.PromptNum: '#ansibrightgreen bold',
710 713 Token.OutPrompt: '#ansired',
711 714 Token.OutPromptNum: '#ansibrightred bold',
712 715 })
713 716
714 717 # Hack: Due to limited color support on the Windows console
715 718 # the prompt colors will be wrong without this
716 719 if os.name == 'nt':
717 720 style_overrides.update({
718 721 Token.Prompt: '#ansidarkgreen',
719 722 Token.PromptNum: '#ansigreen bold',
720 723 Token.OutPrompt: '#ansidarkred',
721 724 Token.OutPromptNum: '#ansired bold',
722 725 })
723 726 elif legacy =='nocolor':
724 727 style_cls=_NoStyle
725 728 style_overrides = {}
726 729 else :
727 730 raise ValueError('Got unknown colors: ', legacy)
728 731 else :
729 732 if isinstance(name_or_cls, str):
730 733 style_cls = get_style_by_name(name_or_cls)
731 734 else:
732 735 style_cls = name_or_cls
733 736 style_overrides = {
734 737 Token.Prompt: '#ansigreen',
735 738 Token.PromptNum: '#ansibrightgreen bold',
736 739 Token.OutPrompt: '#ansired',
737 740 Token.OutPromptNum: '#ansibrightred bold',
738 741 }
739 742 style_overrides.update(self.highlighting_style_overrides)
740 743 style = merge_styles([
741 744 style_from_pygments_cls(style_cls),
742 745 style_from_pygments_dict(style_overrides),
743 746 ])
744 747
745 748 return style
746 749
747 750 @property
748 751 def pt_complete_style(self):
749 752 return {
750 753 'multicolumn': CompleteStyle.MULTI_COLUMN,
751 754 'column': CompleteStyle.COLUMN,
752 755 'readlinelike': CompleteStyle.READLINE_LIKE,
753 756 }[self.display_completions]
754 757
755 758 @property
756 759 def color_depth(self):
757 760 return (ColorDepth.TRUE_COLOR if self.true_color else None)
758 761
759 762 def _extra_prompt_options(self):
760 763 """
761 764 Return the current layout option for the current Terminal InteractiveShell
762 765 """
763 766 def get_message():
764 767 return PygmentsTokens(self.prompts.in_prompt_tokens())
765 768
766 769 if self.editing_mode == "emacs" and self.prompt_line_number_format == "":
767 770 # with emacs mode the prompt is (usually) static, so we call only
768 771 # the function once. With VI mode it can toggle between [ins] and
769 772 # [nor] so we can't precompute.
770 773 # here I'm going to favor the default keybinding which almost
771 774 # everybody uses to decrease CPU usage.
772 775 # if we have issues with users with custom Prompts we can see how to
773 776 # work around this.
774 777 get_message = get_message()
775 778
776 779 options = {
777 780 "complete_in_thread": False,
778 781 "lexer": IPythonPTLexer(),
779 782 "reserve_space_for_menu": self.space_for_menu,
780 783 "message": get_message,
781 784 "prompt_continuation": (
782 785 lambda width, lineno, is_soft_wrap: PygmentsTokens(
783 786 _backward_compat_continuation_prompt_tokens(
784 787 self.prompts.continuation_prompt_tokens, width, lineno=lineno
785 788 )
786 789 )
787 790 ),
788 791 "multiline": True,
789 792 "complete_style": self.pt_complete_style,
790 793 "input_processors": [
791 794 # Highlight matching brackets, but only when this setting is
792 795 # enabled, and only when the DEFAULT_BUFFER has the focus.
793 796 ConditionalProcessor(
794 797 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
795 798 filter=HasFocus(DEFAULT_BUFFER)
796 799 & ~IsDone()
797 800 & Condition(lambda: self.highlight_matching_brackets),
798 801 ),
799 802 # Show auto-suggestion in lines other than the last line.
800 803 ConditionalProcessor(
801 804 processor=AppendAutoSuggestionInAnyLine(),
802 805 filter=HasFocus(DEFAULT_BUFFER)
803 806 & ~IsDone()
804 807 & Condition(
805 808 lambda: isinstance(
806 809 self.auto_suggest, NavigableAutoSuggestFromHistory
807 810 )
808 811 ),
809 812 ),
810 813 ],
811 814 }
812 815 if not PTK3:
813 816 options['inputhook'] = self.inputhook
814 817
815 818 return options
816 819
817 820 def prompt_for_code(self):
818 821 if self.rl_next_input:
819 822 default = self.rl_next_input
820 823 self.rl_next_input = None
821 824 else:
822 825 default = ''
823 826
824 827 # In order to make sure that asyncio code written in the
825 828 # interactive shell doesn't interfere with the prompt, we run the
826 829 # prompt in a different event loop.
827 830 # If we don't do this, people could spawn coroutine with a
828 831 # while/true inside which will freeze the prompt.
829 832
830 833 with patch_stdout(raw=True):
831 834 if self._use_asyncio_inputhook:
832 835 # When we integrate the asyncio event loop, run the UI in the
833 836 # same event loop as the rest of the code. don't use an actual
834 837 # input hook. (Asyncio is not made for nesting event loops.)
835 838 asyncio_loop = get_asyncio_loop()
836 839 text = asyncio_loop.run_until_complete(
837 840 self.pt_app.prompt_async(
838 841 default=default, **self._extra_prompt_options()
839 842 )
840 843 )
841 844 else:
842 845 text = self.pt_app.prompt(
843 846 default=default,
844 847 inputhook=self._inputhook,
845 848 **self._extra_prompt_options(),
846 849 )
847 850
848 851 return text
849 852
850 853 def enable_win_unicode_console(self):
851 854 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
852 855 # console by default, so WUC shouldn't be needed.
853 856 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
854 857 DeprecationWarning,
855 858 stacklevel=2)
856 859
857 860 def init_io(self):
858 861 if sys.platform not in {'win32', 'cli'}:
859 862 return
860 863
861 864 import colorama
862 865 colorama.init()
863 866
864 867 def init_magics(self):
865 868 super(TerminalInteractiveShell, self).init_magics()
866 869 self.register_magics(TerminalMagics)
867 870
868 871 def init_alias(self):
869 872 # The parent class defines aliases that can be safely used with any
870 873 # frontend.
871 874 super(TerminalInteractiveShell, self).init_alias()
872 875
873 876 # Now define aliases that only make sense on the terminal, because they
874 877 # need direct access to the console in a way that we can't emulate in
875 878 # GUI or web frontend
876 879 if os.name == 'posix':
877 880 for cmd in ('clear', 'more', 'less', 'man'):
878 881 self.alias_manager.soft_define_alias(cmd, cmd)
879 882
880 883 def __init__(self, *args, **kwargs) -> None:
881 884 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
882 885 self._set_autosuggestions(self.autosuggestions_provider)
883 886 self.init_prompt_toolkit_cli()
884 887 self.init_term_title()
885 888 self.keep_running = True
886 889 self._set_formatter(self.autoformatter)
887 890
888 891 def ask_exit(self):
889 892 self.keep_running = False
890 893
891 894 rl_next_input = None
892 895
893 896 def interact(self):
894 897 self.keep_running = True
895 898 while self.keep_running:
896 899 print(self.separate_in, end='')
897 900
898 901 try:
899 902 code = self.prompt_for_code()
900 903 except EOFError:
901 904 if (not self.confirm_exit) \
902 905 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
903 906 self.ask_exit()
904 907
905 908 else:
906 909 if code:
907 910 self.run_cell(code, store_history=True)
908 911
909 912 def mainloop(self):
910 913 # An extra layer of protection in case someone mashing Ctrl-C breaks
911 914 # out of our internal code.
912 915 while True:
913 916 try:
914 917 self.interact()
915 918 break
916 919 except KeyboardInterrupt as e:
917 920 print("\n%s escaped interact()\n" % type(e).__name__)
918 921 finally:
919 922 # An interrupt during the eventloop will mess up the
920 923 # internal state of the prompt_toolkit library.
921 924 # Stopping the eventloop fixes this, see
922 925 # https://github.com/ipython/ipython/pull/9867
923 926 if hasattr(self, '_eventloop'):
924 927 self._eventloop.stop()
925 928
926 929 self.restore_term_title()
927 930
928 931 # try to call some at-exit operation optimistically as some things can't
929 932 # be done during interpreter shutdown. this is technically inaccurate as
930 933 # this make mainlool not re-callable, but that should be a rare if not
931 934 # in existent use case.
932 935
933 936 self._atexit_once()
934 937
935 938 _inputhook = None
936 939 def inputhook(self, context):
937 940 if self._inputhook is not None:
938 941 self._inputhook(context)
939 942
940 943 active_eventloop: Optional[str] = None
941 944
942 945 def enable_gui(self, gui: Optional[str] = None) -> None:
943 946 if self.simple_prompt is True and gui is not None:
944 947 print(
945 948 f'Cannot install event loop hook for "{gui}" when running with `--simple-prompt`.'
946 949 )
947 950 print(
948 951 "NOTE: Tk is supported natively; use Tk apps and Tk backends with `--simple-prompt`."
949 952 )
950 953 return
951 954
952 955 if self._inputhook is None and gui is None:
953 956 print("No event loop hook running.")
954 957 return
955 958
956 959 if self._inputhook is not None and gui is not None:
957 960 newev, newinhook = get_inputhook_name_and_func(gui)
958 961 if self._inputhook == newinhook:
959 962 # same inputhook, do nothing
960 963 self.log.info(
961 964 f"Shell is already running the {self.active_eventloop} eventloop. Doing nothing"
962 965 )
963 966 return
964 967 self.log.warning(
965 968 f"Shell is already running a different gui event loop for {self.active_eventloop}. "
966 969 "Call with no arguments to disable the current loop."
967 970 )
968 971 return
969 972 if self._inputhook is not None and gui is None:
970 973 self.active_eventloop = self._inputhook = None
971 974
972 975 if gui and (gui not in {None, "webagg"}):
973 976 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
974 977 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
975 978 else:
976 979 self.active_eventloop = self._inputhook = None
977 980
978 981 self._use_asyncio_inputhook = gui == "asyncio"
979 982
980 983 # Run !system commands directly, not through pipes, so terminal programs
981 984 # work correctly.
982 985 system = InteractiveShell.system_raw
983 986
984 987 def auto_rewrite_input(self, cmd):
985 988 """Overridden from the parent class to use fancy rewriting prompt"""
986 989 if not self.show_rewritten_input:
987 990 return
988 991
989 992 tokens = self.prompts.rewrite_prompt_tokens()
990 993 if self.pt_app:
991 994 print_formatted_text(PygmentsTokens(tokens), end='',
992 995 style=self.pt_app.app.style)
993 996 print(cmd)
994 997 else:
995 998 prompt = ''.join(s for t, s in tokens)
996 999 print(prompt, cmd, sep='')
997 1000
998 1001 _prompts_before = None
999 1002 def switch_doctest_mode(self, mode):
1000 1003 """Switch prompts to classic for %doctest_mode"""
1001 1004 if mode:
1002 1005 self._prompts_before = self.prompts
1003 1006 self.prompts = ClassicPrompts(self)
1004 1007 elif self._prompts_before:
1005 1008 self.prompts = self._prompts_before
1006 1009 self._prompts_before = None
1007 1010 # self._update_layout()
1008 1011
1009 1012
1010 1013 InteractiveShellABC.register(TerminalInteractiveShell)
1011 1014
1012 1015 if __name__ == '__main__':
1013 1016 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now