##// END OF EJS Templates
attempt fix
Matthias Bussonnier -
Show More
@@ -1,987 +1,998
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 prompt_line_number_format = Unicode(
592 "",
593 help="The format for line numbering, will be passed `line` (int, 1 based)"
594 " the current line number and `rel_line` the relative line number."
595 " for example to display both you can use the following template string :"
596 " c.TerminalInteractiveShell.prompt_line_number_format='{line: 4d}/{rel_line:+03d} | '"
597 " This will display the current line number, with leading space and a width of at least 4"
598 " character, as well as the relative line number 0 padded and always with a + or - sign."
599 " Note that when using Emacs mode the prompt of the first line may not update.",
600 ).tag(config=True)
601
591 602 @observe('term_title')
592 603 def init_term_title(self, change=None):
593 604 # Enable or disable the terminal title.
594 605 if self.term_title and _is_tty:
595 606 toggle_set_term_title(True)
596 607 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
597 608 else:
598 609 toggle_set_term_title(False)
599 610
600 611 def restore_term_title(self):
601 612 if self.term_title and _is_tty:
602 613 restore_term_title()
603 614
604 615 def init_display_formatter(self):
605 616 super(TerminalInteractiveShell, self).init_display_formatter()
606 617 # terminal only supports plain text
607 618 self.display_formatter.active_types = ["text/plain"]
608 619
609 620 def init_prompt_toolkit_cli(self):
610 621 if self.simple_prompt:
611 622 # Fall back to plain non-interactive output for tests.
612 623 # This is very limited.
613 624 def prompt():
614 625 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
615 626 lines = [input(prompt_text)]
616 627 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
617 628 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
618 629 lines.append( input(prompt_continuation) )
619 630 return '\n'.join(lines)
620 631 self.prompt_for_code = prompt
621 632 return
622 633
623 634 # Set up keyboard shortcuts
624 635 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
625 636
626 637 # Pre-populate history from IPython's history database
627 638 history = PtkHistoryAdapter(self)
628 639
629 640 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
630 641 self.style = DynamicStyle(lambda: self._style)
631 642
632 643 editing_mode = getattr(EditingMode, self.editing_mode.upper())
633 644
634 645 self._use_asyncio_inputhook = False
635 646 self.pt_app = PromptSession(
636 647 auto_suggest=self.auto_suggest,
637 648 editing_mode=editing_mode,
638 649 key_bindings=key_bindings,
639 650 history=history,
640 651 completer=IPythonPTCompleter(shell=self),
641 652 enable_history_search=self.enable_history_search,
642 653 style=self.style,
643 654 include_default_pygments_style=False,
644 655 mouse_support=self.mouse_support,
645 656 enable_open_in_editor=self.extra_open_editor_shortcuts,
646 657 color_depth=self.color_depth,
647 658 tempfile_suffix=".py",
648 659 **self._extra_prompt_options(),
649 660 )
650 661 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
651 662 self.auto_suggest.connect(self.pt_app)
652 663
653 664 def _make_style_from_name_or_cls(self, name_or_cls):
654 665 """
655 666 Small wrapper that make an IPython compatible style from a style name
656 667
657 668 We need that to add style for prompt ... etc.
658 669 """
659 670 style_overrides = {}
660 671 if name_or_cls == 'legacy':
661 672 legacy = self.colors.lower()
662 673 if legacy == 'linux':
663 674 style_cls = get_style_by_name('monokai')
664 675 style_overrides = _style_overrides_linux
665 676 elif legacy == 'lightbg':
666 677 style_overrides = _style_overrides_light_bg
667 678 style_cls = get_style_by_name('pastie')
668 679 elif legacy == 'neutral':
669 680 # The default theme needs to be visible on both a dark background
670 681 # and a light background, because we can't tell what the terminal
671 682 # looks like. These tweaks to the default theme help with that.
672 683 style_cls = get_style_by_name('default')
673 684 style_overrides.update({
674 685 Token.Number: '#ansigreen',
675 686 Token.Operator: 'noinherit',
676 687 Token.String: '#ansiyellow',
677 688 Token.Name.Function: '#ansiblue',
678 689 Token.Name.Class: 'bold #ansiblue',
679 690 Token.Name.Namespace: 'bold #ansiblue',
680 691 Token.Name.Variable.Magic: '#ansiblue',
681 692 Token.Prompt: '#ansigreen',
682 693 Token.PromptNum: '#ansibrightgreen bold',
683 694 Token.OutPrompt: '#ansired',
684 695 Token.OutPromptNum: '#ansibrightred bold',
685 696 })
686 697
687 698 # Hack: Due to limited color support on the Windows console
688 699 # the prompt colors will be wrong without this
689 700 if os.name == 'nt':
690 701 style_overrides.update({
691 702 Token.Prompt: '#ansidarkgreen',
692 703 Token.PromptNum: '#ansigreen bold',
693 704 Token.OutPrompt: '#ansidarkred',
694 705 Token.OutPromptNum: '#ansired bold',
695 706 })
696 707 elif legacy =='nocolor':
697 708 style_cls=_NoStyle
698 709 style_overrides = {}
699 710 else :
700 711 raise ValueError('Got unknown colors: ', legacy)
701 712 else :
702 713 if isinstance(name_or_cls, str):
703 714 style_cls = get_style_by_name(name_or_cls)
704 715 else:
705 716 style_cls = name_or_cls
706 717 style_overrides = {
707 718 Token.Prompt: '#ansigreen',
708 719 Token.PromptNum: '#ansibrightgreen bold',
709 720 Token.OutPrompt: '#ansired',
710 721 Token.OutPromptNum: '#ansibrightred bold',
711 722 }
712 723 style_overrides.update(self.highlighting_style_overrides)
713 724 style = merge_styles([
714 725 style_from_pygments_cls(style_cls),
715 726 style_from_pygments_dict(style_overrides),
716 727 ])
717 728
718 729 return style
719 730
720 731 @property
721 732 def pt_complete_style(self):
722 733 return {
723 734 'multicolumn': CompleteStyle.MULTI_COLUMN,
724 735 'column': CompleteStyle.COLUMN,
725 736 'readlinelike': CompleteStyle.READLINE_LIKE,
726 737 }[self.display_completions]
727 738
728 739 @property
729 740 def color_depth(self):
730 741 return (ColorDepth.TRUE_COLOR if self.true_color else None)
731 742
732 743 def _extra_prompt_options(self):
733 744 """
734 745 Return the current layout option for the current Terminal InteractiveShell
735 746 """
736 747 def get_message():
737 748 return PygmentsTokens(self.prompts.in_prompt_tokens())
738 749
739 750 if self.editing_mode == 'emacs':
740 751 # with emacs mode the prompt is (usually) static, so we call only
741 752 # the function once. With VI mode it can toggle between [ins] and
742 753 # [nor] so we can't precompute.
743 754 # here I'm going to favor the default keybinding which almost
744 755 # everybody uses to decrease CPU usage.
745 756 # if we have issues with users with custom Prompts we can see how to
746 757 # work around this.
747 758 get_message = get_message()
748 759
749 760 options = {
750 761 "complete_in_thread": False,
751 762 "lexer": IPythonPTLexer(),
752 763 "reserve_space_for_menu": self.space_for_menu,
753 764 "message": get_message,
754 765 "prompt_continuation": (
755 766 lambda width, lineno, is_soft_wrap: PygmentsTokens(
756 767 self.prompts.continuation_prompt_tokens(width, lineno=lineno)
757 768 )
758 769 ),
759 770 "multiline": True,
760 771 "complete_style": self.pt_complete_style,
761 772 "input_processors": [
762 773 # Highlight matching brackets, but only when this setting is
763 774 # enabled, and only when the DEFAULT_BUFFER has the focus.
764 775 ConditionalProcessor(
765 776 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
766 777 filter=HasFocus(DEFAULT_BUFFER)
767 778 & ~IsDone()
768 779 & Condition(lambda: self.highlight_matching_brackets),
769 780 ),
770 781 # Show auto-suggestion in lines other than the last line.
771 782 ConditionalProcessor(
772 783 processor=AppendAutoSuggestionInAnyLine(),
773 784 filter=HasFocus(DEFAULT_BUFFER)
774 785 & ~IsDone()
775 786 & Condition(
776 787 lambda: isinstance(
777 788 self.auto_suggest, NavigableAutoSuggestFromHistory
778 789 )
779 790 ),
780 791 ),
781 792 ],
782 793 }
783 794 if not PTK3:
784 795 options['inputhook'] = self.inputhook
785 796
786 797 return options
787 798
788 799 def prompt_for_code(self):
789 800 if self.rl_next_input:
790 801 default = self.rl_next_input
791 802 self.rl_next_input = None
792 803 else:
793 804 default = ''
794 805
795 806 # In order to make sure that asyncio code written in the
796 807 # interactive shell doesn't interfere with the prompt, we run the
797 808 # prompt in a different event loop.
798 809 # If we don't do this, people could spawn coroutine with a
799 810 # while/true inside which will freeze the prompt.
800 811
801 812 with patch_stdout(raw=True):
802 813 if self._use_asyncio_inputhook:
803 814 # When we integrate the asyncio event loop, run the UI in the
804 815 # same event loop as the rest of the code. don't use an actual
805 816 # input hook. (Asyncio is not made for nesting event loops.)
806 817 asyncio_loop = get_asyncio_loop()
807 818 text = asyncio_loop.run_until_complete(
808 819 self.pt_app.prompt_async(
809 820 default=default, **self._extra_prompt_options()
810 821 )
811 822 )
812 823 else:
813 824 text = self.pt_app.prompt(
814 825 default=default,
815 826 inputhook=self._inputhook,
816 827 **self._extra_prompt_options(),
817 828 )
818 829
819 830 return text
820 831
821 832 def enable_win_unicode_console(self):
822 833 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
823 834 # console by default, so WUC shouldn't be needed.
824 835 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
825 836 DeprecationWarning,
826 837 stacklevel=2)
827 838
828 839 def init_io(self):
829 840 if sys.platform not in {'win32', 'cli'}:
830 841 return
831 842
832 843 import colorama
833 844 colorama.init()
834 845
835 846 def init_magics(self):
836 847 super(TerminalInteractiveShell, self).init_magics()
837 848 self.register_magics(TerminalMagics)
838 849
839 850 def init_alias(self):
840 851 # The parent class defines aliases that can be safely used with any
841 852 # frontend.
842 853 super(TerminalInteractiveShell, self).init_alias()
843 854
844 855 # Now define aliases that only make sense on the terminal, because they
845 856 # need direct access to the console in a way that we can't emulate in
846 857 # GUI or web frontend
847 858 if os.name == 'posix':
848 859 for cmd in ('clear', 'more', 'less', 'man'):
849 860 self.alias_manager.soft_define_alias(cmd, cmd)
850 861
851 862
852 863 def __init__(self, *args, **kwargs) -> None:
853 864 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
854 865 self._set_autosuggestions(self.autosuggestions_provider)
855 866 self.init_prompt_toolkit_cli()
856 867 self.init_term_title()
857 868 self.keep_running = True
858 869 self._set_formatter(self.autoformatter)
859 870
860 871
861 872 def ask_exit(self):
862 873 self.keep_running = False
863 874
864 875 rl_next_input = None
865 876
866 877 def interact(self):
867 878 self.keep_running = True
868 879 while self.keep_running:
869 880 print(self.separate_in, end='')
870 881
871 882 try:
872 883 code = self.prompt_for_code()
873 884 except EOFError:
874 885 if (not self.confirm_exit) \
875 886 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
876 887 self.ask_exit()
877 888
878 889 else:
879 890 if code:
880 891 self.run_cell(code, store_history=True)
881 892
882 893 def mainloop(self):
883 894 # An extra layer of protection in case someone mashing Ctrl-C breaks
884 895 # out of our internal code.
885 896 while True:
886 897 try:
887 898 self.interact()
888 899 break
889 900 except KeyboardInterrupt as e:
890 901 print("\n%s escaped interact()\n" % type(e).__name__)
891 902 finally:
892 903 # An interrupt during the eventloop will mess up the
893 904 # internal state of the prompt_toolkit library.
894 905 # Stopping the eventloop fixes this, see
895 906 # https://github.com/ipython/ipython/pull/9867
896 907 if hasattr(self, '_eventloop'):
897 908 self._eventloop.stop()
898 909
899 910 self.restore_term_title()
900 911
901 912 # try to call some at-exit operation optimistically as some things can't
902 913 # be done during interpreter shutdown. this is technically inaccurate as
903 914 # this make mainlool not re-callable, but that should be a rare if not
904 915 # in existent use case.
905 916
906 917 self._atexit_once()
907 918
908 919
909 920 _inputhook = None
910 921 def inputhook(self, context):
911 922 if self._inputhook is not None:
912 923 self._inputhook(context)
913 924
914 925 active_eventloop: Optional[str] = None
915 926
916 927 def enable_gui(self, gui: Optional[str] = None) -> None:
917 928 if self.simple_prompt is True and gui is not None:
918 929 print(
919 930 f'Cannot install event loop hook for "{gui}" when running with `--simple-prompt`.'
920 931 )
921 932 print(
922 933 "NOTE: Tk is supported natively; use Tk apps and Tk backends with `--simple-prompt`."
923 934 )
924 935 return
925 936
926 937 if self._inputhook is None and gui is None:
927 938 print("No event loop hook running.")
928 939 return
929 940
930 941 if self._inputhook is not None and gui is not None:
931 942 newev, newinhook = get_inputhook_name_and_func(gui)
932 943 if self._inputhook == newinhook:
933 944 # same inputhook, do nothing
934 945 self.log.info(
935 946 f"Shell is already running the {self.active_eventloop} eventloop. Doing nothing"
936 947 )
937 948 return
938 949 self.log.warning(
939 950 f"Shell is already running a different gui event loop for {self.active_eventloop}. "
940 951 "Call with no arguments to disable the current loop."
941 952 )
942 953 return
943 954 if self._inputhook is not None and gui is None:
944 955 self.active_eventloop = self._inputhook = None
945 956
946 957 if gui and (gui not in {"inline", "webagg"}):
947 958 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
948 959 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
949 960 else:
950 961 self.active_eventloop = self._inputhook = None
951 962
952 963 self._use_asyncio_inputhook = gui == "asyncio"
953 964
954 965 # Run !system commands directly, not through pipes, so terminal programs
955 966 # work correctly.
956 967 system = InteractiveShell.system_raw
957 968
958 969 def auto_rewrite_input(self, cmd):
959 970 """Overridden from the parent class to use fancy rewriting prompt"""
960 971 if not self.show_rewritten_input:
961 972 return
962 973
963 974 tokens = self.prompts.rewrite_prompt_tokens()
964 975 if self.pt_app:
965 976 print_formatted_text(PygmentsTokens(tokens), end='',
966 977 style=self.pt_app.app.style)
967 978 print(cmd)
968 979 else:
969 980 prompt = ''.join(s for t, s in tokens)
970 981 print(prompt, cmd, sep='')
971 982
972 983 _prompts_before = None
973 984 def switch_doctest_mode(self, mode):
974 985 """Switch prompts to classic for %doctest_mode"""
975 986 if mode:
976 987 self._prompts_before = self.prompts
977 988 self.prompts = ClassicPrompts(self)
978 989 elif self._prompts_before:
979 990 self.prompts = self._prompts_before
980 991 self._prompts_before = None
981 992 # self._update_layout()
982 993
983 994
984 995 InteractiveShellABC.register(TerminalInteractiveShell)
985 996
986 997 if __name__ == '__main__':
987 998 TerminalInteractiveShell.instance().interact()
@@ -1,113 +1,127
1 1 """Terminal input and output prompts."""
2 2
3 3 from pygments.token import Token
4 4 import sys
5 5
6 6 from IPython.core.displayhook import DisplayHook
7 7
8 8 from prompt_toolkit.formatted_text import fragment_list_width, PygmentsTokens
9 9 from prompt_toolkit.shortcuts import print_formatted_text
10 10 from prompt_toolkit.enums import EditingMode
11 11
12 12
13 13 class Prompts(object):
14 14 def __init__(self, shell):
15 15 self.shell = shell
16 16
17 17 def vi_mode(self):
18 18 if (getattr(self.shell.pt_app, 'editing_mode', None) == EditingMode.VI
19 19 and self.shell.prompt_includes_vi_mode):
20 20 mode = str(self.shell.pt_app.app.vi_state.input_mode)
21 21 if mode.startswith('InputMode.'):
22 22 mode = mode[10:13].lower()
23 23 elif mode.startswith('vi-'):
24 24 mode = mode[3:6]
25 25 return '['+mode+'] '
26 26 return ''
27 27
28 def current_line(self) -> int:
29 if self.shell.pt_app is not None:
30 return self.shell.pt_app.default_buffer.document.cursor_position_row or 0
31 return 0
28 32
29 33 def in_prompt_tokens(self):
30 34 return [
31 35 (Token.Prompt, self.vi_mode()),
32 (Token.Prompt, "1 | "),
36 (
37 Token.Prompt,
38 self.shell.prompt_line_number_format.format(
39 line=1, rel_line=-self.current_line()
40 ),
41 ),
33 42 (Token.Prompt, "In ["),
34 43 (Token.PromptNum, str(self.shell.execution_count)),
35 44 (Token.Prompt, ']: '),
36 45 ]
37 46
38 47 def _width(self):
39 48 return fragment_list_width(self.in_prompt_tokens())
40 49
41 50 def continuation_prompt_tokens(self, width=None, *, lineno=None):
42 51 if width is None:
43 52 width = self._width()
44 prefix = " " * len(self.vi_mode()) + str(lineno + 1) + " | "
53 line = lineno + 1 if lineno is not None else 0
54 prefix = " " * len(
55 self.vi_mode()
56 ) + self.shell.prompt_line_number_format.format(
57 line=line, rel_line=line - self.current_line() - 1
58 )
45 59 return [
46 60 (
47 61 Token.Prompt,
48 62 prefix + (" " * (width - len(prefix) - 5)) + "...: ",
49 63 ),
50 64 ]
51 65
52 66 def rewrite_prompt_tokens(self):
53 67 width = self._width()
54 68 return [
55 69 (Token.Prompt, ('-' * (width - 2)) + '> '),
56 70 ]
57 71
58 72 def out_prompt_tokens(self):
59 73 return [
60 74 (Token.OutPrompt, 'Out['),
61 75 (Token.OutPromptNum, str(self.shell.execution_count)),
62 76 (Token.OutPrompt, ']: '),
63 77 ]
64 78
65 79 class ClassicPrompts(Prompts):
66 80 def in_prompt_tokens(self):
67 81 return [
68 82 (Token.Prompt, '>>> '),
69 83 ]
70 84
71 85 def continuation_prompt_tokens(self, width=None):
72 86 return [
73 87 (Token.Prompt, '... ')
74 88 ]
75 89
76 90 def rewrite_prompt_tokens(self):
77 91 return []
78 92
79 93 def out_prompt_tokens(self):
80 94 return []
81 95
82 96 class RichPromptDisplayHook(DisplayHook):
83 97 """Subclass of base display hook using coloured prompt"""
84 98 def write_output_prompt(self):
85 99 sys.stdout.write(self.shell.separate_out)
86 100 # If we're not displaying a prompt, it effectively ends with a newline,
87 101 # because the output will be left-aligned.
88 102 self.prompt_end_newline = True
89 103
90 104 if self.do_full_cache:
91 105 tokens = self.shell.prompts.out_prompt_tokens()
92 106 prompt_txt = ''.join(s for t, s in tokens)
93 107 if prompt_txt and not prompt_txt.endswith('\n'):
94 108 # Ask for a newline before multiline output
95 109 self.prompt_end_newline = False
96 110
97 111 if self.shell.pt_app:
98 112 print_formatted_text(PygmentsTokens(tokens),
99 113 style=self.shell.pt_app.app.style, end='',
100 114 )
101 115 else:
102 116 sys.stdout.write(prompt_txt)
103 117
104 118 def write_format_data(self, format_dict, md_dict=None) -> None:
105 119 if self.shell.mime_renderers:
106 120
107 121 for mime, handler in self.shell.mime_renderers.items():
108 122 if mime in format_dict:
109 123 handler(format_dict[mime], None)
110 124 return
111 125
112 126 super().write_format_data(format_dict, md_dict)
113 127
General Comments 0
You need to be logged in to leave comments. Login now