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