##// END OF EJS Templates
ENH: Don't show message when enable the current eventloop. (#14168)...
Matthias Bussonnier -
r28403:3df49467 merge
parent child Browse files
Show More
@@ -1,1002 +1,1010 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 from typing import Union as UnionType
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 634 self.pt_loop = asyncio.new_event_loop()
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 801 policy = asyncio.get_event_loop_policy()
802 802 old_loop = get_asyncio_loop()
803 803
804 804 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
805 805 # to get the current event loop.
806 806 # This will probably be replaced by an attribute or input argument,
807 807 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
808 808 if old_loop is not self.pt_loop:
809 809 policy.set_event_loop(self.pt_loop)
810 810 try:
811 811 with patch_stdout(raw=True):
812 812 text = self.pt_app.prompt(
813 813 default=default,
814 814 **self._extra_prompt_options())
815 815 finally:
816 816 # Restore the original event loop.
817 817 if old_loop is not None and old_loop is not self.pt_loop:
818 818 policy.set_event_loop(old_loop)
819 819
820 820 return text
821 821
822 822 def enable_win_unicode_console(self):
823 823 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
824 824 # console by default, so WUC shouldn't be needed.
825 825 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
826 826 DeprecationWarning,
827 827 stacklevel=2)
828 828
829 829 def init_io(self):
830 830 if sys.platform not in {'win32', 'cli'}:
831 831 return
832 832
833 833 import colorama
834 834 colorama.init()
835 835
836 836 def init_magics(self):
837 837 super(TerminalInteractiveShell, self).init_magics()
838 838 self.register_magics(TerminalMagics)
839 839
840 840 def init_alias(self):
841 841 # The parent class defines aliases that can be safely used with any
842 842 # frontend.
843 843 super(TerminalInteractiveShell, self).init_alias()
844 844
845 845 # Now define aliases that only make sense on the terminal, because they
846 846 # need direct access to the console in a way that we can't emulate in
847 847 # GUI or web frontend
848 848 if os.name == 'posix':
849 849 for cmd in ('clear', 'more', 'less', 'man'):
850 850 self.alias_manager.soft_define_alias(cmd, cmd)
851 851
852 852
853 853 def __init__(self, *args, **kwargs) -> None:
854 854 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
855 855 self._set_autosuggestions(self.autosuggestions_provider)
856 856 self.init_prompt_toolkit_cli()
857 857 self.init_term_title()
858 858 self.keep_running = True
859 859 self._set_formatter(self.autoformatter)
860 860
861 861
862 862 def ask_exit(self):
863 863 self.keep_running = False
864 864
865 865 rl_next_input = None
866 866
867 867 def interact(self):
868 868 self.keep_running = True
869 869 while self.keep_running:
870 870 print(self.separate_in, end='')
871 871
872 872 try:
873 873 code = self.prompt_for_code()
874 874 except EOFError:
875 875 if (not self.confirm_exit) \
876 876 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
877 877 self.ask_exit()
878 878
879 879 else:
880 880 if code:
881 881 self.run_cell(code, store_history=True)
882 882
883 883 def mainloop(self):
884 884 # An extra layer of protection in case someone mashing Ctrl-C breaks
885 885 # out of our internal code.
886 886 while True:
887 887 try:
888 888 self.interact()
889 889 break
890 890 except KeyboardInterrupt as e:
891 891 print("\n%s escaped interact()\n" % type(e).__name__)
892 892 finally:
893 893 # An interrupt during the eventloop will mess up the
894 894 # internal state of the prompt_toolkit library.
895 895 # Stopping the eventloop fixes this, see
896 896 # https://github.com/ipython/ipython/pull/9867
897 897 if hasattr(self, '_eventloop'):
898 898 self._eventloop.stop()
899 899
900 900 self.restore_term_title()
901 901
902 902 # try to call some at-exit operation optimistically as some things can't
903 903 # be done during interpreter shutdown. this is technically inaccurate as
904 904 # this make mainlool not re-callable, but that should be a rare if not
905 905 # in existent use case.
906 906
907 907 self._atexit_once()
908 908
909 909
910 910 _inputhook = None
911 911 def inputhook(self, context):
912 912 if self._inputhook is not None:
913 913 self._inputhook(context)
914 914
915 active_eventloop = None
916 def enable_gui(self, gui=None):
915 active_eventloop: Optional[str] = None
916
917 def enable_gui(self, gui: Optional[str] = None) -> None:
917 918 if self.simple_prompt is True and gui is not None:
918 919 print(
919 920 f'Cannot install event loop hook for "{gui}" when running with `--simple-prompt`.'
920 921 )
921 922 print(
922 923 "NOTE: Tk is supported natively; use Tk apps and Tk backends with `--simple-prompt`."
923 924 )
924 925 return
925 926
926 927 if self._inputhook is None and gui is None:
927 928 print("No event loop hook running.")
928 929 return
929 930
930 931 if self._inputhook is not None and gui is not None:
931 print(
932 f"Shell is already running a gui event loop for {self.active_eventloop}. "
932 newev, newinhook = get_inputhook_name_and_func(gui)
933 if self._inputhook == newinhook:
934 # same inputhook, do nothing
935 self.log.info(
936 f"Shell is already running the {self.active_eventloop} eventloop. Doing nothing"
937 )
938 return
939 self.log.warning(
940 f"Shell is already running a different gui event loop for {self.active_eventloop}. "
933 941 "Call with no arguments to disable the current loop."
934 942 )
935 943 return
936 944 if self._inputhook is not None and gui is None:
937 945 self.active_eventloop = self._inputhook = None
938 946
939 947 if gui and (gui not in {"inline", "webagg"}):
940 948 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
941 949 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
942 950 else:
943 951 self.active_eventloop = self._inputhook = None
944 952
945 953 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
946 954 # this inputhook.
947 955 if PTK3:
948 956 import asyncio
949 957 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
950 958
951 959 if gui == 'asyncio':
952 960 # When we integrate the asyncio event loop, run the UI in the
953 961 # same event loop as the rest of the code. don't use an actual
954 962 # input hook. (Asyncio is not made for nesting event loops.)
955 963 self.pt_loop = get_asyncio_loop()
956 964 print("Installed asyncio event loop hook.")
957 965
958 966 elif self._inputhook:
959 967 # If an inputhook was set, create a new asyncio event loop with
960 968 # this inputhook for the prompt.
961 969 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
962 970 print(f"Installed {self.active_eventloop} event loop hook.")
963 971 else:
964 972 # When there's no inputhook, run the prompt in a separate
965 973 # asyncio event loop.
966 974 self.pt_loop = asyncio.new_event_loop()
967 975 print("GUI event loop hook disabled.")
968 976
969 977 # Run !system commands directly, not through pipes, so terminal programs
970 978 # work correctly.
971 979 system = InteractiveShell.system_raw
972 980
973 981 def auto_rewrite_input(self, cmd):
974 982 """Overridden from the parent class to use fancy rewriting prompt"""
975 983 if not self.show_rewritten_input:
976 984 return
977 985
978 986 tokens = self.prompts.rewrite_prompt_tokens()
979 987 if self.pt_app:
980 988 print_formatted_text(PygmentsTokens(tokens), end='',
981 989 style=self.pt_app.app.style)
982 990 print(cmd)
983 991 else:
984 992 prompt = ''.join(s for t, s in tokens)
985 993 print(prompt, cmd, sep='')
986 994
987 995 _prompts_before = None
988 996 def switch_doctest_mode(self, mode):
989 997 """Switch prompts to classic for %doctest_mode"""
990 998 if mode:
991 999 self._prompts_before = self.prompts
992 1000 self.prompts = ClassicPrompts(self)
993 1001 elif self._prompts_before:
994 1002 self.prompts = self._prompts_before
995 1003 self._prompts_before = None
996 1004 # self._update_layout()
997 1005
998 1006
999 1007 InteractiveShellABC.register(TerminalInteractiveShell)
1000 1008
1001 1009 if __name__ == '__main__':
1002 1010 TerminalInteractiveShell.instance().interact()
@@ -1,138 +1,139 b''
1 1 import importlib
2 2 import os
3 from typing import Tuple, Callable
3 4
4 5 aliases = {
5 6 'qt4': 'qt',
6 7 'gtk2': 'gtk',
7 8 }
8 9
9 10 backends = [
10 11 "qt",
11 12 "qt5",
12 13 "qt6",
13 14 "gtk",
14 15 "gtk2",
15 16 "gtk3",
16 17 "gtk4",
17 18 "tk",
18 19 "wx",
19 20 "pyglet",
20 21 "glut",
21 22 "osx",
22 23 "asyncio",
23 24 ]
24 25
25 26 registered = {}
26 27
27 28 def register(name, inputhook):
28 29 """Register the function *inputhook* as an event loop integration."""
29 30 registered[name] = inputhook
30 31
31 32
32 33 class UnknownBackend(KeyError):
33 34 def __init__(self, name):
34 35 self.name = name
35 36
36 37 def __str__(self):
37 38 return ("No event loop integration for {!r}. "
38 39 "Supported event loops are: {}").format(self.name,
39 40 ', '.join(backends + sorted(registered)))
40 41
41 42
42 43 def set_qt_api(gui):
43 44 """Sets the `QT_API` environment variable if it isn't already set."""
44 45
45 46 qt_api = os.environ.get("QT_API", None)
46 47
47 48 from IPython.external.qt_loaders import (
48 49 QT_API_PYQT,
49 50 QT_API_PYQT5,
50 51 QT_API_PYQT6,
51 52 QT_API_PYSIDE,
52 53 QT_API_PYSIDE2,
53 54 QT_API_PYSIDE6,
54 55 QT_API_PYQTv1,
55 56 loaded_api,
56 57 )
57 58
58 59 loaded = loaded_api()
59 60
60 61 qt_env2gui = {
61 62 QT_API_PYSIDE: "qt4",
62 63 QT_API_PYQTv1: "qt4",
63 64 QT_API_PYQT: "qt4",
64 65 QT_API_PYSIDE2: "qt5",
65 66 QT_API_PYQT5: "qt5",
66 67 QT_API_PYSIDE6: "qt6",
67 68 QT_API_PYQT6: "qt6",
68 69 }
69 70 if loaded is not None and gui != "qt":
70 71 if qt_env2gui[loaded] != gui:
71 72 print(
72 73 f"Cannot switch Qt versions for this session; will use {qt_env2gui[loaded]}."
73 74 )
74 75 return qt_env2gui[loaded]
75 76
76 77 if qt_api is not None and gui != "qt":
77 78 if qt_env2gui[qt_api] != gui:
78 79 print(
79 80 f'Request for "{gui}" will be ignored because `QT_API` '
80 81 f'environment variable is set to "{qt_api}"'
81 82 )
82 83 return qt_env2gui[qt_api]
83 84 else:
84 85 if gui == "qt5":
85 86 try:
86 87 import PyQt5 # noqa
87 88
88 89 os.environ["QT_API"] = "pyqt5"
89 90 except ImportError:
90 91 try:
91 92 import PySide2 # noqa
92 93
93 94 os.environ["QT_API"] = "pyside2"
94 95 except ImportError:
95 96 os.environ["QT_API"] = "pyqt5"
96 97 elif gui == "qt6":
97 98 try:
98 99 import PyQt6 # noqa
99 100
100 101 os.environ["QT_API"] = "pyqt6"
101 102 except ImportError:
102 103 try:
103 104 import PySide6 # noqa
104 105
105 106 os.environ["QT_API"] = "pyside6"
106 107 except ImportError:
107 108 os.environ["QT_API"] = "pyqt6"
108 109 elif gui == "qt":
109 110 # Don't set QT_API; let IPython logic choose the version.
110 111 if "QT_API" in os.environ.keys():
111 112 del os.environ["QT_API"]
112 113 else:
113 114 print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".')
114 115 return
115 116
116 117 # Import it now so we can figure out which version it is.
117 118 from IPython.external.qt_for_kernel import QT_API
118 119
119 120 return qt_env2gui[QT_API]
120 121
121 122
122 def get_inputhook_name_and_func(gui):
123 def get_inputhook_name_and_func(gui: str) -> Tuple[str, Callable]:
123 124 if gui in registered:
124 125 return gui, registered[gui]
125 126
126 127 if gui not in backends:
127 128 raise UnknownBackend(gui)
128 129
129 130 if gui in aliases:
130 131 return get_inputhook_name_and_func(aliases[gui])
131 132
132 133 gui_mod = gui
133 134 if gui.startswith("qt"):
134 135 gui = set_qt_api(gui)
135 136 gui_mod = "qt"
136 137
137 138 mod = importlib.import_module("IPython.terminal.pt_inputhooks." + gui_mod)
138 139 return gui, mod.inputhook
General Comments 0
You need to be logged in to leave comments. Login now