##// END OF EJS Templates
misc typing
Matthias Bussonnier -
Show More
@@ -1,789 +1,791 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
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 observe,
20 20 Instance,
21 21 Type,
22 22 default,
23 23 Enum,
24 24 Union,
25 25 Any,
26 26 validate,
27 27 Float,
28 28 )
29 29
30 30 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
31 31 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
32 32 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
33 33 from prompt_toolkit.formatted_text import PygmentsTokens
34 34 from prompt_toolkit.history import History
35 35 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
36 36 from prompt_toolkit.output import ColorDepth
37 37 from prompt_toolkit.patch_stdout import patch_stdout
38 38 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
39 39 from prompt_toolkit.styles import DynamicStyle, merge_styles
40 40 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
41 41 from prompt_toolkit import __version__ as ptk_version
42 42
43 43 from pygments.styles import get_style_by_name
44 44 from pygments.style import Style
45 45 from pygments.token import Token
46 46
47 47 from .debugger import TerminalPdb, Pdb
48 48 from .magics import TerminalMagics
49 49 from .pt_inputhooks import get_inputhook_name_and_func
50 50 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
51 51 from .ptutils import IPythonPTCompleter, IPythonPTLexer
52 52 from .shortcuts import create_ipython_shortcuts
53 53 from .shortcuts.auto_suggest import NavigableAutoSuggestFromHistory
54 54
55 55 PTK3 = ptk_version.startswith('3.')
56 56
57 57
58 58 class _NoStyle(Style): pass
59 59
60 60
61 61
62 62 _style_overrides_light_bg = {
63 63 Token.Prompt: '#ansibrightblue',
64 64 Token.PromptNum: '#ansiblue bold',
65 65 Token.OutPrompt: '#ansibrightred',
66 66 Token.OutPromptNum: '#ansired bold',
67 67 }
68 68
69 69 _style_overrides_linux = {
70 70 Token.Prompt: '#ansibrightgreen',
71 71 Token.PromptNum: '#ansigreen bold',
72 72 Token.OutPrompt: '#ansibrightred',
73 73 Token.OutPromptNum: '#ansired bold',
74 74 }
75 75
76 76 def get_default_editor():
77 77 try:
78 78 return os.environ['EDITOR']
79 79 except KeyError:
80 80 pass
81 81 except UnicodeError:
82 82 warn("$EDITOR environment variable is not pure ASCII. Using platform "
83 83 "default editor.")
84 84
85 85 if os.name == 'posix':
86 86 return 'vi' # the only one guaranteed to be there!
87 87 else:
88 88 return 'notepad' # same in Windows!
89 89
90 90 # conservatively check for tty
91 91 # overridden streams can result in things like:
92 92 # - sys.stdin = None
93 93 # - no isatty method
94 94 for _name in ('stdin', 'stdout', 'stderr'):
95 95 _stream = getattr(sys, _name)
96 96 try:
97 97 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
98 98 _is_tty = False
99 99 break
100 100 except ValueError:
101 101 # stream is closed
102 102 _is_tty = False
103 103 break
104 104 else:
105 105 _is_tty = True
106 106
107 107
108 108 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
109 109
110 110 def black_reformat_handler(text_before_cursor):
111 111 """
112 112 We do not need to protect against error,
113 113 this is taken care at a higher level where any reformat error is ignored.
114 114 Indeed we may call reformatting on incomplete code.
115 115 """
116 116 import black
117 117
118 118 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
119 119 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
120 120 formatted_text = formatted_text[:-1]
121 121 return formatted_text
122 122
123 123
124 124 def yapf_reformat_handler(text_before_cursor):
125 125 from yapf.yapflib import file_resources
126 126 from yapf.yapflib import yapf_api
127 127
128 128 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
129 129 formatted_text, was_formatted = yapf_api.FormatCode(
130 130 text_before_cursor, style_config=style_config
131 131 )
132 132 if was_formatted:
133 133 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
134 134 formatted_text = formatted_text[:-1]
135 135 return formatted_text
136 136 else:
137 137 return text_before_cursor
138 138
139 139
140 140 class PtkHistoryAdapter(History):
141 141 """
142 142 Prompt toolkit has it's own way of handling history, Where it assumes it can
143 143 Push/pull from history.
144 144
145 145 """
146 146
147 auto_suggest: UnionType[
148 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
149 ]
150
147 151 def __init__(self, shell):
148 152 super().__init__()
149 153 self.shell = shell
150 154 self._refresh()
151 155
152 156 def append_string(self, string):
153 157 # we rely on sql for that.
154 158 self._loaded = False
155 159 self._refresh()
156 160
157 161 def _refresh(self):
158 162 if not self._loaded:
159 163 self._loaded_strings = list(self.load_history_strings())
160 164
161 165 def load_history_strings(self):
162 166 last_cell = ""
163 167 res = []
164 168 for __, ___, cell in self.shell.history_manager.get_tail(
165 169 self.shell.history_load_length, include_latest=True
166 170 ):
167 171 # Ignore blank lines and consecutive duplicates
168 172 cell = cell.rstrip()
169 173 if cell and (cell != last_cell):
170 174 res.append(cell)
171 175 last_cell = cell
172 176 yield from res[::-1]
173 177
174 178 def store_string(self, string: str) -> None:
175 179 pass
176 180
177 181 class TerminalInteractiveShell(InteractiveShell):
178 182 mime_renderers = Dict().tag(config=True)
179 183
180 184 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
181 185 'to reserve for the tab completion menu, '
182 186 'search history, ...etc, the height of '
183 187 'these menus will at most this value. '
184 188 'Increase it is you prefer long and skinny '
185 189 'menus, decrease for short and wide.'
186 190 ).tag(config=True)
187 191
188 192 pt_app: UnionType[PromptSession, None] = None
189 193 debugger_history = None
190 194
191 195 debugger_history_file = Unicode(
192 196 "~/.pdbhistory", help="File in which to store and read history"
193 197 ).tag(config=True)
194 198
195 199 simple_prompt = Bool(_use_simple_prompt,
196 200 help="""Use `raw_input` for the REPL, without completion and prompt colors.
197 201
198 202 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
199 203 IPython own testing machinery, and emacs inferior-shell integration through elpy.
200 204
201 205 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
202 206 environment variable is set, or the current terminal is not a tty."""
203 207 ).tag(config=True)
204 208
205 209 @property
206 210 def debugger_cls(self):
207 211 return Pdb if self.simple_prompt else TerminalPdb
208 212
209 213 confirm_exit = Bool(True,
210 214 help="""
211 215 Set to confirm when you try to exit IPython with an EOF (Control-D
212 216 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
213 217 you can force a direct exit without any confirmation.""",
214 218 ).tag(config=True)
215 219
216 220 editing_mode = Unicode('emacs',
217 221 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
218 222 ).tag(config=True)
219 223
220 224 emacs_bindings_in_vi_insert_mode = Bool(
221 225 True,
222 226 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
223 227 ).tag(config=True)
224 228
225 229 modal_cursor = Bool(
226 230 True,
227 231 help="""
228 232 Cursor shape changes depending on vi mode: beam in vi insert mode,
229 233 block in nav mode, underscore in replace mode.""",
230 234 ).tag(config=True)
231 235
232 236 ttimeoutlen = Float(
233 237 0.01,
234 238 help="""The time in milliseconds that is waited for a key code
235 239 to complete.""",
236 240 ).tag(config=True)
237 241
238 242 timeoutlen = Float(
239 243 0.5,
240 244 help="""The time in milliseconds that is waited for a mapped key
241 245 sequence to complete.""",
242 246 ).tag(config=True)
243 247
244 248 autoformatter = Unicode(
245 249 None,
246 250 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
247 251 allow_none=True
248 252 ).tag(config=True)
249 253
250 254 auto_match = Bool(
251 255 False,
252 256 help="""
253 257 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
254 258 Brackets: (), [], {}
255 259 Quotes: '', \"\"
256 260 """,
257 261 ).tag(config=True)
258 262
259 263 mouse_support = Bool(False,
260 264 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
261 265 ).tag(config=True)
262 266
263 267 # We don't load the list of styles for the help string, because loading
264 268 # Pygments plugins takes time and can cause unexpected errors.
265 269 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
266 270 help="""The name or class of a Pygments style to use for syntax
267 271 highlighting. To see available styles, run `pygmentize -L styles`."""
268 272 ).tag(config=True)
269 273
270 274 @validate('editing_mode')
271 275 def _validate_editing_mode(self, proposal):
272 276 if proposal['value'].lower() == 'vim':
273 277 proposal['value']= 'vi'
274 278 elif proposal['value'].lower() == 'default':
275 279 proposal['value']= 'emacs'
276 280
277 281 if hasattr(EditingMode, proposal['value'].upper()):
278 282 return proposal['value'].lower()
279 283
280 284 return self.editing_mode
281 285
282 286
283 287 @observe('editing_mode')
284 288 def _editing_mode(self, change):
285 289 if self.pt_app:
286 290 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
287 291
288 292 def _set_formatter(self, formatter):
289 293 if formatter is None:
290 294 self.reformat_handler = lambda x:x
291 295 elif formatter == 'black':
292 296 self.reformat_handler = black_reformat_handler
293 297 elif formatter == "yapf":
294 298 self.reformat_handler = yapf_reformat_handler
295 299 else:
296 300 raise ValueError
297 301
298 302 @observe("autoformatter")
299 303 def _autoformatter_changed(self, change):
300 304 formatter = change.new
301 305 self._set_formatter(formatter)
302 306
303 307 @observe('highlighting_style')
304 308 @observe('colors')
305 309 def _highlighting_style_changed(self, change):
306 310 self.refresh_style()
307 311
308 312 def refresh_style(self):
309 313 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
310 314
311 315
312 316 highlighting_style_overrides = Dict(
313 317 help="Override highlighting format for specific tokens"
314 318 ).tag(config=True)
315 319
316 320 true_color = Bool(False,
317 321 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
318 322 If your terminal supports true color, the following command should
319 323 print ``TRUECOLOR`` in orange::
320 324
321 325 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
322 326 """,
323 327 ).tag(config=True)
324 328
325 329 editor = Unicode(get_default_editor(),
326 330 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
327 331 ).tag(config=True)
328 332
329 333 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
330 334
331 335 prompts = Instance(Prompts)
332 336
333 337 @default('prompts')
334 338 def _prompts_default(self):
335 339 return self.prompts_class(self)
336 340
337 341 # @observe('prompts')
338 342 # def _(self, change):
339 343 # self._update_layout()
340 344
341 345 @default('displayhook_class')
342 346 def _displayhook_class_default(self):
343 347 return RichPromptDisplayHook
344 348
345 349 term_title = Bool(True,
346 350 help="Automatically set the terminal title"
347 351 ).tag(config=True)
348 352
349 353 term_title_format = Unicode("IPython: {cwd}",
350 354 help="Customize the terminal title format. This is a python format string. " +
351 355 "Available substitutions are: {cwd}."
352 356 ).tag(config=True)
353 357
354 358 display_completions = Enum(('column', 'multicolumn','readlinelike'),
355 359 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
356 360 "'readlinelike'. These options are for `prompt_toolkit`, see "
357 361 "`prompt_toolkit` documentation for more information."
358 362 ),
359 363 default_value='multicolumn').tag(config=True)
360 364
361 365 highlight_matching_brackets = Bool(True,
362 366 help="Highlight matching brackets.",
363 367 ).tag(config=True)
364 368
365 369 extra_open_editor_shortcuts = Bool(False,
366 370 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
367 371 "This is in addition to the F2 binding, which is always enabled."
368 372 ).tag(config=True)
369 373
370 374 handle_return = Any(None,
371 375 help="Provide an alternative handler to be called when the user presses "
372 376 "Return. This is an advanced option intended for debugging, which "
373 377 "may be changed or removed in later releases."
374 378 ).tag(config=True)
375 379
376 380 enable_history_search = Bool(True,
377 381 help="Allows to enable/disable the prompt toolkit history search"
378 382 ).tag(config=True)
379 383
380 384 autosuggestions_provider = Unicode(
381 385 "NavigableAutoSuggestFromHistory",
382 386 help="Specifies from which source automatic suggestions are provided. "
383 387 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
384 388 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
385 389 " or ``None`` to disable automatic suggestions. "
386 390 "Default is `'NavigableAutoSuggestFromHistory`'.",
387 391 allow_none=True,
388 392 ).tag(config=True)
389 393
390 394 def _set_autosuggestions(self, provider):
391 395 # disconnect old handler
392 396 if self.auto_suggest and isinstance(
393 397 self.auto_suggest, NavigableAutoSuggestFromHistory
394 398 ):
395 399 self.auto_suggest.disconnect()
396 400 if provider is None:
397 401 self.auto_suggest = None
398 402 elif provider == "AutoSuggestFromHistory":
399 403 self.auto_suggest = AutoSuggestFromHistory()
400 404 elif provider == "NavigableAutoSuggestFromHistory":
401 405 self.auto_suggest = NavigableAutoSuggestFromHistory()
402 406 else:
403 407 raise ValueError("No valid provider.")
404 408 if self.pt_app:
405 409 self.pt_app.auto_suggest = self.auto_suggest
406 410
407 411 @observe("autosuggestions_provider")
408 412 def _autosuggestions_provider_changed(self, change):
409 413 provider = change.new
410 414 self._set_autosuggestions(provider)
411 415
412 416 prompt_includes_vi_mode = Bool(True,
413 417 help="Display the current vi mode (when using vi editing mode)."
414 418 ).tag(config=True)
415 419
416 420 @observe('term_title')
417 421 def init_term_title(self, change=None):
418 422 # Enable or disable the terminal title.
419 423 if self.term_title and _is_tty:
420 424 toggle_set_term_title(True)
421 425 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
422 426 else:
423 427 toggle_set_term_title(False)
424 428
425 429 def restore_term_title(self):
426 430 if self.term_title and _is_tty:
427 431 restore_term_title()
428 432
429 433 def init_display_formatter(self):
430 434 super(TerminalInteractiveShell, self).init_display_formatter()
431 435 # terminal only supports plain text
432 436 self.display_formatter.active_types = ["text/plain"]
433 437
434 438 def init_prompt_toolkit_cli(self):
435 439 if self.simple_prompt:
436 440 # Fall back to plain non-interactive output for tests.
437 441 # This is very limited.
438 442 def prompt():
439 443 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
440 444 lines = [input(prompt_text)]
441 445 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
442 446 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
443 447 lines.append( input(prompt_continuation) )
444 448 return '\n'.join(lines)
445 449 self.prompt_for_code = prompt
446 450 return
447 451
448 452 # Set up keyboard shortcuts
449 453 key_bindings = create_ipython_shortcuts(self)
450 454
451 455
452 456 # Pre-populate history from IPython's history database
453 457 history = PtkHistoryAdapter(self)
454 458
455 459 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
456 460 self.style = DynamicStyle(lambda: self._style)
457 461
458 462 editing_mode = getattr(EditingMode, self.editing_mode.upper())
459 463
460 464 self.pt_loop = asyncio.new_event_loop()
461 465 self.pt_app = PromptSession(
462 466 auto_suggest=self.auto_suggest,
463 467 editing_mode=editing_mode,
464 468 key_bindings=key_bindings,
465 469 history=history,
466 470 completer=IPythonPTCompleter(shell=self),
467 471 enable_history_search=self.enable_history_search,
468 472 style=self.style,
469 473 include_default_pygments_style=False,
470 474 mouse_support=self.mouse_support,
471 475 enable_open_in_editor=self.extra_open_editor_shortcuts,
472 476 color_depth=self.color_depth,
473 477 tempfile_suffix=".py",
474 478 **self._extra_prompt_options()
475 479 )
476 480 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
477 481 self.auto_suggest.connect(self.pt_app)
478 482
479 483 def _make_style_from_name_or_cls(self, name_or_cls):
480 484 """
481 485 Small wrapper that make an IPython compatible style from a style name
482 486
483 487 We need that to add style for prompt ... etc.
484 488 """
485 489 style_overrides = {}
486 490 if name_or_cls == 'legacy':
487 491 legacy = self.colors.lower()
488 492 if legacy == 'linux':
489 493 style_cls = get_style_by_name('monokai')
490 494 style_overrides = _style_overrides_linux
491 495 elif legacy == 'lightbg':
492 496 style_overrides = _style_overrides_light_bg
493 497 style_cls = get_style_by_name('pastie')
494 498 elif legacy == 'neutral':
495 499 # The default theme needs to be visible on both a dark background
496 500 # and a light background, because we can't tell what the terminal
497 501 # looks like. These tweaks to the default theme help with that.
498 502 style_cls = get_style_by_name('default')
499 503 style_overrides.update({
500 504 Token.Number: '#ansigreen',
501 505 Token.Operator: 'noinherit',
502 506 Token.String: '#ansiyellow',
503 507 Token.Name.Function: '#ansiblue',
504 508 Token.Name.Class: 'bold #ansiblue',
505 509 Token.Name.Namespace: 'bold #ansiblue',
506 510 Token.Name.Variable.Magic: '#ansiblue',
507 511 Token.Prompt: '#ansigreen',
508 512 Token.PromptNum: '#ansibrightgreen bold',
509 513 Token.OutPrompt: '#ansired',
510 514 Token.OutPromptNum: '#ansibrightred bold',
511 515 })
512 516
513 517 # Hack: Due to limited color support on the Windows console
514 518 # the prompt colors will be wrong without this
515 519 if os.name == 'nt':
516 520 style_overrides.update({
517 521 Token.Prompt: '#ansidarkgreen',
518 522 Token.PromptNum: '#ansigreen bold',
519 523 Token.OutPrompt: '#ansidarkred',
520 524 Token.OutPromptNum: '#ansired bold',
521 525 })
522 526 elif legacy =='nocolor':
523 527 style_cls=_NoStyle
524 528 style_overrides = {}
525 529 else :
526 530 raise ValueError('Got unknown colors: ', legacy)
527 531 else :
528 532 if isinstance(name_or_cls, str):
529 533 style_cls = get_style_by_name(name_or_cls)
530 534 else:
531 535 style_cls = name_or_cls
532 536 style_overrides = {
533 537 Token.Prompt: '#ansigreen',
534 538 Token.PromptNum: '#ansibrightgreen bold',
535 539 Token.OutPrompt: '#ansired',
536 540 Token.OutPromptNum: '#ansibrightred bold',
537 541 }
538 542 style_overrides.update(self.highlighting_style_overrides)
539 543 style = merge_styles([
540 544 style_from_pygments_cls(style_cls),
541 545 style_from_pygments_dict(style_overrides),
542 546 ])
543 547
544 548 return style
545 549
546 550 @property
547 551 def pt_complete_style(self):
548 552 return {
549 553 'multicolumn': CompleteStyle.MULTI_COLUMN,
550 554 'column': CompleteStyle.COLUMN,
551 555 'readlinelike': CompleteStyle.READLINE_LIKE,
552 556 }[self.display_completions]
553 557
554 558 @property
555 559 def color_depth(self):
556 560 return (ColorDepth.TRUE_COLOR if self.true_color else None)
557 561
558 562 def _extra_prompt_options(self):
559 563 """
560 564 Return the current layout option for the current Terminal InteractiveShell
561 565 """
562 566 def get_message():
563 567 return PygmentsTokens(self.prompts.in_prompt_tokens())
564 568
565 569 if self.editing_mode == 'emacs':
566 570 # with emacs mode the prompt is (usually) static, so we call only
567 571 # the function once. With VI mode it can toggle between [ins] and
568 572 # [nor] so we can't precompute.
569 573 # here I'm going to favor the default keybinding which almost
570 574 # everybody uses to decrease CPU usage.
571 575 # if we have issues with users with custom Prompts we can see how to
572 576 # work around this.
573 577 get_message = get_message()
574 578
575 579 options = {
576 580 'complete_in_thread': False,
577 581 'lexer':IPythonPTLexer(),
578 582 'reserve_space_for_menu':self.space_for_menu,
579 583 'message': get_message,
580 584 'prompt_continuation': (
581 585 lambda width, lineno, is_soft_wrap:
582 586 PygmentsTokens(self.prompts.continuation_prompt_tokens(width))),
583 587 'multiline': True,
584 588 'complete_style': self.pt_complete_style,
585 589
586 590 # Highlight matching brackets, but only when this setting is
587 591 # enabled, and only when the DEFAULT_BUFFER has the focus.
588 592 'input_processors': [ConditionalProcessor(
589 593 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
590 594 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
591 595 Condition(lambda: self.highlight_matching_brackets))],
592 596 }
593 597 if not PTK3:
594 598 options['inputhook'] = self.inputhook
595 599
596 600 return options
597 601
598 602 def prompt_for_code(self):
599 603 if self.rl_next_input:
600 604 default = self.rl_next_input
601 605 self.rl_next_input = None
602 606 else:
603 607 default = ''
604 608
605 609 # In order to make sure that asyncio code written in the
606 610 # interactive shell doesn't interfere with the prompt, we run the
607 611 # prompt in a different event loop.
608 612 # If we don't do this, people could spawn coroutine with a
609 613 # while/true inside which will freeze the prompt.
610 614
611 615 policy = asyncio.get_event_loop_policy()
612 616 old_loop = get_asyncio_loop()
613 617
614 618 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
615 619 # to get the current event loop.
616 620 # This will probably be replaced by an attribute or input argument,
617 621 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
618 622 if old_loop is not self.pt_loop:
619 623 policy.set_event_loop(self.pt_loop)
620 624 try:
621 625 with patch_stdout(raw=True):
622 626 text = self.pt_app.prompt(
623 627 default=default,
624 628 **self._extra_prompt_options())
625 629 finally:
626 630 # Restore the original event loop.
627 631 if old_loop is not None and old_loop is not self.pt_loop:
628 632 policy.set_event_loop(old_loop)
629 633
630 634 return text
631 635
632 636 def enable_win_unicode_console(self):
633 637 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
634 638 # console by default, so WUC shouldn't be needed.
635 639 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
636 640 DeprecationWarning,
637 641 stacklevel=2)
638 642
639 643 def init_io(self):
640 644 if sys.platform not in {'win32', 'cli'}:
641 645 return
642 646
643 647 import colorama
644 648 colorama.init()
645 649
646 650 def init_magics(self):
647 651 super(TerminalInteractiveShell, self).init_magics()
648 652 self.register_magics(TerminalMagics)
649 653
650 654 def init_alias(self):
651 655 # The parent class defines aliases that can be safely used with any
652 656 # frontend.
653 657 super(TerminalInteractiveShell, self).init_alias()
654 658
655 659 # Now define aliases that only make sense on the terminal, because they
656 660 # need direct access to the console in a way that we can't emulate in
657 661 # GUI or web frontend
658 662 if os.name == 'posix':
659 663 for cmd in ('clear', 'more', 'less', 'man'):
660 664 self.alias_manager.soft_define_alias(cmd, cmd)
661 665
662 666
663 def __init__(self, *args, **kwargs):
667 def __init__(self, *args, **kwargs) -> None:
664 668 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
665 self.auto_suggest: UnionType[
666 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
667 ] = None
669 self.auto_suggest = None
668 670 self._set_autosuggestions(self.autosuggestions_provider)
669 671 self.init_prompt_toolkit_cli()
670 672 self.init_term_title()
671 673 self.keep_running = True
672 674 self._set_formatter(self.autoformatter)
673 675
674 676
675 677 def ask_exit(self):
676 678 self.keep_running = False
677 679
678 680 rl_next_input = None
679 681
680 682 def interact(self):
681 683 self.keep_running = True
682 684 while self.keep_running:
683 685 print(self.separate_in, end='')
684 686
685 687 try:
686 688 code = self.prompt_for_code()
687 689 except EOFError:
688 690 if (not self.confirm_exit) \
689 691 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
690 692 self.ask_exit()
691 693
692 694 else:
693 695 if code:
694 696 self.run_cell(code, store_history=True)
695 697
696 698 def mainloop(self):
697 699 # An extra layer of protection in case someone mashing Ctrl-C breaks
698 700 # out of our internal code.
699 701 while True:
700 702 try:
701 703 self.interact()
702 704 break
703 705 except KeyboardInterrupt as e:
704 706 print("\n%s escaped interact()\n" % type(e).__name__)
705 707 finally:
706 708 # An interrupt during the eventloop will mess up the
707 709 # internal state of the prompt_toolkit library.
708 710 # Stopping the eventloop fixes this, see
709 711 # https://github.com/ipython/ipython/pull/9867
710 712 if hasattr(self, '_eventloop'):
711 713 self._eventloop.stop()
712 714
713 715 self.restore_term_title()
714 716
715 717 # try to call some at-exit operation optimistically as some things can't
716 718 # be done during interpreter shutdown. this is technically inaccurate as
717 719 # this make mainlool not re-callable, but that should be a rare if not
718 720 # in existent use case.
719 721
720 722 self._atexit_once()
721 723
722 724
723 725 _inputhook = None
724 726 def inputhook(self, context):
725 727 if self._inputhook is not None:
726 728 self._inputhook(context)
727 729
728 730 active_eventloop = None
729 731 def enable_gui(self, gui=None):
730 732 if gui and (gui not in {"inline", "webagg"}):
731 733 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
732 734 else:
733 735 self.active_eventloop = self._inputhook = None
734 736
735 737 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
736 738 # this inputhook.
737 739 if PTK3:
738 740 import asyncio
739 741 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
740 742
741 743 if gui == 'asyncio':
742 744 # When we integrate the asyncio event loop, run the UI in the
743 745 # same event loop as the rest of the code. don't use an actual
744 746 # input hook. (Asyncio is not made for nesting event loops.)
745 747 self.pt_loop = get_asyncio_loop()
746 748
747 749 elif self._inputhook:
748 750 # If an inputhook was set, create a new asyncio event loop with
749 751 # this inputhook for the prompt.
750 752 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
751 753 else:
752 754 # When there's no inputhook, run the prompt in a separate
753 755 # asyncio event loop.
754 756 self.pt_loop = asyncio.new_event_loop()
755 757
756 758 # Run !system commands directly, not through pipes, so terminal programs
757 759 # work correctly.
758 760 system = InteractiveShell.system_raw
759 761
760 762 def auto_rewrite_input(self, cmd):
761 763 """Overridden from the parent class to use fancy rewriting prompt"""
762 764 if not self.show_rewritten_input:
763 765 return
764 766
765 767 tokens = self.prompts.rewrite_prompt_tokens()
766 768 if self.pt_app:
767 769 print_formatted_text(PygmentsTokens(tokens), end='',
768 770 style=self.pt_app.app.style)
769 771 print(cmd)
770 772 else:
771 773 prompt = ''.join(s for t, s in tokens)
772 774 print(prompt, cmd, sep='')
773 775
774 776 _prompts_before = None
775 777 def switch_doctest_mode(self, mode):
776 778 """Switch prompts to classic for %doctest_mode"""
777 779 if mode:
778 780 self._prompts_before = self.prompts
779 781 self.prompts = ClassicPrompts(self)
780 782 elif self._prompts_before:
781 783 self.prompts = self._prompts_before
782 784 self._prompts_before = None
783 785 # self._update_layout()
784 786
785 787
786 788 InteractiveShellABC.register(TerminalInteractiveShell)
787 789
788 790 if __name__ == '__main__':
789 791 TerminalInteractiveShell.instance().interact()
@@ -1,308 +1,318 b''
1 1 import re
2 2 import tokenize
3 3 from io import StringIO
4 from typing import Callable, List, Optional, Union, Generator, Tuple
4 from typing import Callable, List, Optional, Union, Generator, Tuple, Sequence
5 5
6 6 from prompt_toolkit.buffer import Buffer
7 7 from prompt_toolkit.key_binding import KeyPressEvent
8 8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 10 from prompt_toolkit.document import Document
11 11 from prompt_toolkit.history import History
12 12 from prompt_toolkit.shortcuts import PromptSession
13 13
14 14 from IPython.utils.tokenutil import generate_tokens
15 15
16 16
17 17 def _get_query(document: Document):
18 18 return document.text.rsplit("\n", 1)[-1]
19 19
20 20
21 21 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
22 22 """
23 23 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
24 24 suggestion from history. To do so it remembers the current position, but it
25 25 state need to carefully be cleared on the right events.
26 26 """
27 27
28 28 def __init__(
29 29 self,
30 30 ):
31 31 self.skip_lines = 0
32 32 self._connected_apps = []
33 33
34 34 def reset_history_position(self, _: Buffer):
35 35 self.skip_lines = 0
36 36
37 37 def disconnect(self):
38 38 for pt_app in self._connected_apps:
39 39 text_insert_event = pt_app.default_buffer.on_text_insert
40 40 text_insert_event.remove_handler(self.reset_history_position)
41 41
42 42 def connect(self, pt_app: PromptSession):
43 43 self._connected_apps.append(pt_app)
44 44 # note: `on_text_changed` could be used for a bit different behaviour
45 45 # on character deletion (i.e. reseting history position on backspace)
46 46 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
47 47
48 48 def get_suggestion(
49 49 self, buffer: Buffer, document: Document
50 50 ) -> Optional[Suggestion]:
51 51 text = _get_query(document)
52 52
53 53 if text.strip():
54 54 for suggestion, _ in self._find_next_match(
55 55 text, self.skip_lines, buffer.history
56 56 ):
57 57 return Suggestion(suggestion)
58 58
59 59 return None
60 60
61 61 def _find_match(
62 62 self, text: str, skip_lines: float, history: History, previous: bool
63 63 ) -> Generator[Tuple[str, float], None, None]:
64 64 """
65 65 text: str
66
66 Text content to find a match for, the user cursor is most of the
67 time at the end of this text.
67 68 skip_lines: float
68 float is used as the base value is +inf
69 number of items to skip in the search, this is used to indicate how
70 far in the list the user has navigated by pressing up or down.
71 The float type is used as the base value is +inf
72 history : History
73 prompt_toolkit History instance to fetch previous entries from.
74 previous : bool
75 Direction of the search, whether we are looking previous match
76 (True), or next match (False).
69 77
70 78 Yields
71 79 ------
72 80 Tuple with:
73 81 str:
74 82 current suggestion.
75 83 float:
76 84 will actually yield only ints, which is passed back via skip_lines,
77 85 which may be a +inf (float)
78 86
79 87
80 88 """
81 89 line_number = -1
82 90 for string in reversed(list(history.get_strings())):
83 91 for line in reversed(string.splitlines()):
84 92 line_number += 1
85 93 if not previous and line_number < skip_lines:
86 94 continue
87 95 # do not return empty suggestions as these
88 96 # close the auto-suggestion overlay (and are useless)
89 97 if line.startswith(text) and len(line) > len(text):
90 98 yield line[len(text) :], line_number
91 99 if previous and line_number >= skip_lines:
92 100 return
93 101
94 def _find_next_match(self, text: str, skip_lines: float, history: History):
102 def _find_next_match(
103 self, text: str, skip_lines: float, history: History
104 ) -> Generator[Tuple[str, float], None, None]:
95 105 return self._find_match(text, skip_lines, history, previous=False)
96 106
97 107 def _find_previous_match(self, text: str, skip_lines: float, history: History):
98 108 return reversed(
99 109 list(self._find_match(text, skip_lines, history, previous=True))
100 110 )
101 111
102 def up(self, query: str, other_than: str, history: History):
112 def up(self, query: str, other_than: str, history: History) -> None:
103 113 for suggestion, line_number in self._find_next_match(
104 114 query, self.skip_lines, history
105 115 ):
106 116 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
107 117 # we want to switch from 'very.b' to 'very.a' because a) if the
108 118 # suggestion equals current text, prompt-toolkit aborts suggesting
109 119 # b) user likely would not be interested in 'very' anyways (they
110 120 # already typed it).
111 121 if query + suggestion != other_than:
112 122 self.skip_lines = line_number
113 123 break
114 124 else:
115 125 # no matches found, cycle back to beginning
116 126 self.skip_lines = 0
117 127
118 def down(self, query: str, other_than: str, history: History):
128 def down(self, query: str, other_than: str, history: History) -> None:
119 129 for suggestion, line_number in self._find_previous_match(
120 130 query, self.skip_lines, history
121 131 ):
122 132 if query + suggestion != other_than:
123 133 self.skip_lines = line_number
124 134 break
125 135 else:
126 136 # no matches found, cycle to end
127 137 for suggestion, line_number in self._find_previous_match(
128 138 query, float("Inf"), history
129 139 ):
130 140 if query + suggestion != other_than:
131 141 self.skip_lines = line_number
132 142 break
133 143
134 144
135 145 # Needed for to accept autosuggestions in vi insert mode
136 146 def accept_in_vi_insert_mode(event: KeyPressEvent):
137 147 """Apply autosuggestion if at end of line."""
138 148 buffer = event.current_buffer
139 149 d = buffer.document
140 150 after_cursor = d.text[d.cursor_position :]
141 151 lines = after_cursor.split("\n")
142 152 end_of_current_line = lines[0].strip()
143 153 suggestion = buffer.suggestion
144 154 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
145 155 buffer.insert_text(suggestion.text)
146 156 else:
147 157 nc.end_of_line(event)
148 158
149 159
150 160 def accept(event: KeyPressEvent):
151 161 """Accept autosuggestion"""
152 162 buffer = event.current_buffer
153 163 suggestion = buffer.suggestion
154 164 if suggestion:
155 165 buffer.insert_text(suggestion.text)
156 166 else:
157 167 nc.forward_char(event)
158 168
159 169
160 170 def accept_word(event: KeyPressEvent):
161 171 """Fill partial autosuggestion by word"""
162 172 buffer = event.current_buffer
163 173 suggestion = buffer.suggestion
164 174 if suggestion:
165 175 t = re.split(r"(\S+\s+)", suggestion.text)
166 176 buffer.insert_text(next((x for x in t if x), ""))
167 177 else:
168 178 nc.forward_word(event)
169 179
170 180
171 181 def accept_character(event: KeyPressEvent):
172 182 """Fill partial autosuggestion by character"""
173 183 b = event.current_buffer
174 184 suggestion = b.suggestion
175 185 if suggestion and suggestion.text:
176 186 b.insert_text(suggestion.text[0])
177 187
178 188
179 189 def accept_and_keep_cursor(event: KeyPressEvent):
180 190 """Accept autosuggestion and keep cursor in place"""
181 191 buffer = event.current_buffer
182 192 old_position = buffer.cursor_position
183 193 suggestion = buffer.suggestion
184 194 if suggestion:
185 195 buffer.insert_text(suggestion.text)
186 196 buffer.cursor_position = old_position
187 197
188 198
189 199 def accept_and_move_cursor_left(event: KeyPressEvent):
190 200 """Accept autosuggestion and move cursor left in place"""
191 201 accept_and_keep_cursor(event)
192 202 nc.backward_char(event)
193 203
194 204
195 205 def backspace_and_resume_hint(event: KeyPressEvent):
196 206 """Resume autosuggestions after deleting last character"""
197 207 current_buffer = event.current_buffer
198 208
199 209 def resume_hinting(buffer: Buffer):
200 210 if buffer.auto_suggest:
201 211 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
202 212 if suggestion:
203 213 buffer.suggestion = suggestion
204 214 current_buffer.on_text_changed.remove_handler(resume_hinting)
205 215
206 216 current_buffer.on_text_changed.add_handler(resume_hinting)
207 217 nc.backward_delete_char(event)
208 218
209 219
210 220 def accept_token(event: KeyPressEvent):
211 221 """Fill partial autosuggestion by token"""
212 222 b = event.current_buffer
213 223 suggestion = b.suggestion
214 224
215 225 if suggestion:
216 226 prefix = _get_query(b.document)
217 227 text = prefix + suggestion.text
218 228
219 229 tokens: List[Optional[str]] = [None, None, None]
220 230 substrings = [""]
221 231 i = 0
222 232
223 233 for token in generate_tokens(StringIO(text).readline):
224 234 if token.type == tokenize.NEWLINE:
225 235 index = len(text)
226 236 else:
227 237 index = text.index(token[1], len(substrings[-1]))
228 238 substrings.append(text[:index])
229 239 tokenized_so_far = substrings[-1]
230 240 if tokenized_so_far.startswith(prefix):
231 241 if i == 0 and len(tokenized_so_far) > len(prefix):
232 242 tokens[0] = tokenized_so_far[len(prefix) :]
233 243 substrings.append(tokenized_so_far)
234 244 i += 1
235 245 tokens[i] = token[1]
236 246 if i == 2:
237 247 break
238 248 i += 1
239 249
240 250 if tokens[0]:
241 251 to_insert: str
242 252 insert_text = substrings[-2]
243 253 if tokens[1] and len(tokens[1]) == 1:
244 254 insert_text = substrings[-1]
245 255 to_insert = insert_text[len(prefix) :]
246 256 b.insert_text(to_insert)
247 257 return
248 258
249 259 nc.forward_word(event)
250 260
251 261
252 262 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
253 263
254 264
255 265 def _swap_autosuggestion(
256 266 buffer: Buffer,
257 267 provider: NavigableAutoSuggestFromHistory,
258 268 direction_method: Callable,
259 269 ):
260 270 """
261 271 We skip most recent history entry (in either direction) if it equals the
262 272 current autosuggestion because if user cycles when auto-suggestion is shown
263 273 they most likely want something else than what was suggested (otherwise
264 274 they would have accepted the suggestion).
265 275 """
266 276 suggestion = buffer.suggestion
267 277 if not suggestion:
268 278 return
269 279
270 280 query = _get_query(buffer.document)
271 281 current = query + suggestion.text
272 282
273 283 direction_method(query=query, other_than=current, history=buffer.history)
274 284
275 285 new_suggestion = provider.get_suggestion(buffer, buffer.document)
276 286 buffer.suggestion = new_suggestion
277 287
278 288
279 289 def swap_autosuggestion_up(provider: Provider):
280 290 def swap_autosuggestion_up(event: KeyPressEvent):
281 291 """Get next autosuggestion from history."""
282 292 if not isinstance(provider, NavigableAutoSuggestFromHistory):
283 293 return
284 294
285 295 return _swap_autosuggestion(
286 296 buffer=event.current_buffer, provider=provider, direction_method=provider.up
287 297 )
288 298
289 299 swap_autosuggestion_up.__name__ = "swap_autosuggestion_up"
290 300 return swap_autosuggestion_up
291 301
292 302
293 303 def swap_autosuggestion_down(
294 304 provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
295 305 ):
296 306 def swap_autosuggestion_down(event: KeyPressEvent):
297 307 """Get previous autosuggestion from history."""
298 308 if not isinstance(provider, NavigableAutoSuggestFromHistory):
299 309 return
300 310
301 311 return _swap_autosuggestion(
302 312 buffer=event.current_buffer,
303 313 provider=provider,
304 314 direction_method=provider.down,
305 315 )
306 316
307 317 swap_autosuggestion_down.__name__ = "swap_autosuggestion_down"
308 318 return swap_autosuggestion_down
General Comments 0
You need to be logged in to leave comments. Login now