##// END OF EJS Templates
Fix configuration before initialization
krassowski -
Show More
@@ -1,963 +1,966 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 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 create_ipython_shortcuts,
55 55 create_identifier,
56 56 RuntimeBinding,
57 57 Binding,
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 user_shortcuts = change.new
485 if self.pt_app:
486 self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
487
488 def _merge_shortcuts(self, user_shortcuts):
486 489 # rebuild the bindings list from scratch
487 490 key_bindings = create_ipython_shortcuts(self)
488 491
489 492 # for now we only allow adding shortcuts for commands which are already
490 493 # registered; this is a security precaution.
491 494 known_commands = {
492 495 create_identifier(binding.handler): binding.handler
493 496 for binding in key_bindings.bindings
494 497 }
495 498 shortcuts_to_skip = []
496 499 shortcuts_to_add = []
497 500
498 501 for shortcut in user_shortcuts:
499 502 command_id = shortcut["command"]
500 503 if command_id not in known_commands:
501 504 allowed_commands = "\n - ".join(known_commands)
502 505 raise ValueError(
503 506 f"{command_id} is not a known shortcut command."
504 507 f" Allowed commands are: \n - {allowed_commands}"
505 508 )
506 509 old_keys = shortcut.get("match_keys", None)
507 510 old_filter = (
508 511 filter_from_string(shortcut["match_filter"])
509 512 if "match_filter" in shortcut
510 513 else None
511 514 )
512 515 matching = [
513 516 binding
514 517 for binding in key_bindings.bindings
515 518 if (
516 519 (old_filter is None or binding.filter == old_filter)
517 520 and (old_keys is None or [k for k in binding.keys] == old_keys)
518 521 and create_identifier(binding.handler) == command_id
519 522 )
520 523 ]
521 524
522 525 new_keys = shortcut.get("new_keys", None)
523 526 new_filter = shortcut.get("new_filter", None)
524 527
525 528 command = known_commands[command_id]
526 529
527 530 creating_new = shortcut.get("create", False)
528 531 modifying_existing = not creating_new and (
529 532 new_keys is not None or new_filter
530 533 )
531 534
532 535 if creating_new and new_keys == []:
533 536 raise ValueError("Cannot add a shortcut without keys")
534 537
535 538 if modifying_existing:
536 539 specification = {
537 540 key: shortcut[key]
538 541 for key in ["command", "filter"]
539 542 if key in shortcut
540 543 }
541 544 if len(matching) == 0:
542 545 raise ValueError(f"No shortcuts matching {specification} found")
543 546 elif len(matching) > 1:
544 547 raise ValueError(
545 548 f"Multiple shortcuts matching {specification} found,"
546 549 f" please add keys/filter to select one of: {matching}"
547 550 )
548 551
549 552 for matched in matching:
550 553 shortcuts_to_skip.append(
551 554 RuntimeBinding(
552 555 command,
553 556 keys=[k for k in matching[0].keys],
554 557 filter=matching[0].filter,
555 558 )
556 559 )
557 560
558 561 if new_keys != []:
559 562 shortcuts_to_add.append(
560 563 Binding(
561 564 command,
562 565 keys=new_keys,
563 566 condition=new_filter if new_filter is not None else "always",
564 567 )
565 568 )
566 569
567 570 # rebuild the bindings list from scratch
568 571 key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
569 572 for binding in shortcuts_to_add:
570 573 add_binding(key_bindings, binding)
571 self.pt_app.key_bindings = key_bindings
574
575 return key_bindings
572 576
573 577 prompt_includes_vi_mode = Bool(True,
574 578 help="Display the current vi mode (when using vi editing mode)."
575 579 ).tag(config=True)
576 580
577 581 @observe('term_title')
578 582 def init_term_title(self, change=None):
579 583 # Enable or disable the terminal title.
580 584 if self.term_title and _is_tty:
581 585 toggle_set_term_title(True)
582 586 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
583 587 else:
584 588 toggle_set_term_title(False)
585 589
586 590 def restore_term_title(self):
587 591 if self.term_title and _is_tty:
588 592 restore_term_title()
589 593
590 594 def init_display_formatter(self):
591 595 super(TerminalInteractiveShell, self).init_display_formatter()
592 596 # terminal only supports plain text
593 597 self.display_formatter.active_types = ["text/plain"]
594 598
595 599 def init_prompt_toolkit_cli(self):
596 600 if self.simple_prompt:
597 601 # Fall back to plain non-interactive output for tests.
598 602 # This is very limited.
599 603 def prompt():
600 604 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
601 605 lines = [input(prompt_text)]
602 606 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
603 607 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
604 608 lines.append( input(prompt_continuation) )
605 609 return '\n'.join(lines)
606 610 self.prompt_for_code = prompt
607 611 return
608 612
609 613 # Set up keyboard shortcuts
610 key_bindings = create_ipython_shortcuts(self)
611
614 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
612 615
613 616 # Pre-populate history from IPython's history database
614 617 history = PtkHistoryAdapter(self)
615 618
616 619 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
617 620 self.style = DynamicStyle(lambda: self._style)
618 621
619 622 editing_mode = getattr(EditingMode, self.editing_mode.upper())
620 623
621 624 self.pt_loop = asyncio.new_event_loop()
622 625 self.pt_app = PromptSession(
623 626 auto_suggest=self.auto_suggest,
624 627 editing_mode=editing_mode,
625 628 key_bindings=key_bindings,
626 629 history=history,
627 630 completer=IPythonPTCompleter(shell=self),
628 631 enable_history_search=self.enable_history_search,
629 632 style=self.style,
630 633 include_default_pygments_style=False,
631 634 mouse_support=self.mouse_support,
632 635 enable_open_in_editor=self.extra_open_editor_shortcuts,
633 636 color_depth=self.color_depth,
634 637 tempfile_suffix=".py",
635 638 **self._extra_prompt_options(),
636 639 )
637 640 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
638 641 self.auto_suggest.connect(self.pt_app)
639 642
640 643 def _make_style_from_name_or_cls(self, name_or_cls):
641 644 """
642 645 Small wrapper that make an IPython compatible style from a style name
643 646
644 647 We need that to add style for prompt ... etc.
645 648 """
646 649 style_overrides = {}
647 650 if name_or_cls == 'legacy':
648 651 legacy = self.colors.lower()
649 652 if legacy == 'linux':
650 653 style_cls = get_style_by_name('monokai')
651 654 style_overrides = _style_overrides_linux
652 655 elif legacy == 'lightbg':
653 656 style_overrides = _style_overrides_light_bg
654 657 style_cls = get_style_by_name('pastie')
655 658 elif legacy == 'neutral':
656 659 # The default theme needs to be visible on both a dark background
657 660 # and a light background, because we can't tell what the terminal
658 661 # looks like. These tweaks to the default theme help with that.
659 662 style_cls = get_style_by_name('default')
660 663 style_overrides.update({
661 664 Token.Number: '#ansigreen',
662 665 Token.Operator: 'noinherit',
663 666 Token.String: '#ansiyellow',
664 667 Token.Name.Function: '#ansiblue',
665 668 Token.Name.Class: 'bold #ansiblue',
666 669 Token.Name.Namespace: 'bold #ansiblue',
667 670 Token.Name.Variable.Magic: '#ansiblue',
668 671 Token.Prompt: '#ansigreen',
669 672 Token.PromptNum: '#ansibrightgreen bold',
670 673 Token.OutPrompt: '#ansired',
671 674 Token.OutPromptNum: '#ansibrightred bold',
672 675 })
673 676
674 677 # Hack: Due to limited color support on the Windows console
675 678 # the prompt colors will be wrong without this
676 679 if os.name == 'nt':
677 680 style_overrides.update({
678 681 Token.Prompt: '#ansidarkgreen',
679 682 Token.PromptNum: '#ansigreen bold',
680 683 Token.OutPrompt: '#ansidarkred',
681 684 Token.OutPromptNum: '#ansired bold',
682 685 })
683 686 elif legacy =='nocolor':
684 687 style_cls=_NoStyle
685 688 style_overrides = {}
686 689 else :
687 690 raise ValueError('Got unknown colors: ', legacy)
688 691 else :
689 692 if isinstance(name_or_cls, str):
690 693 style_cls = get_style_by_name(name_or_cls)
691 694 else:
692 695 style_cls = name_or_cls
693 696 style_overrides = {
694 697 Token.Prompt: '#ansigreen',
695 698 Token.PromptNum: '#ansibrightgreen bold',
696 699 Token.OutPrompt: '#ansired',
697 700 Token.OutPromptNum: '#ansibrightred bold',
698 701 }
699 702 style_overrides.update(self.highlighting_style_overrides)
700 703 style = merge_styles([
701 704 style_from_pygments_cls(style_cls),
702 705 style_from_pygments_dict(style_overrides),
703 706 ])
704 707
705 708 return style
706 709
707 710 @property
708 711 def pt_complete_style(self):
709 712 return {
710 713 'multicolumn': CompleteStyle.MULTI_COLUMN,
711 714 'column': CompleteStyle.COLUMN,
712 715 'readlinelike': CompleteStyle.READLINE_LIKE,
713 716 }[self.display_completions]
714 717
715 718 @property
716 719 def color_depth(self):
717 720 return (ColorDepth.TRUE_COLOR if self.true_color else None)
718 721
719 722 def _extra_prompt_options(self):
720 723 """
721 724 Return the current layout option for the current Terminal InteractiveShell
722 725 """
723 726 def get_message():
724 727 return PygmentsTokens(self.prompts.in_prompt_tokens())
725 728
726 729 if self.editing_mode == 'emacs':
727 730 # with emacs mode the prompt is (usually) static, so we call only
728 731 # the function once. With VI mode it can toggle between [ins] and
729 732 # [nor] so we can't precompute.
730 733 # here I'm going to favor the default keybinding which almost
731 734 # everybody uses to decrease CPU usage.
732 735 # if we have issues with users with custom Prompts we can see how to
733 736 # work around this.
734 737 get_message = get_message()
735 738
736 739 options = {
737 740 "complete_in_thread": False,
738 741 "lexer": IPythonPTLexer(),
739 742 "reserve_space_for_menu": self.space_for_menu,
740 743 "message": get_message,
741 744 "prompt_continuation": (
742 745 lambda width, lineno, is_soft_wrap: PygmentsTokens(
743 746 self.prompts.continuation_prompt_tokens(width)
744 747 )
745 748 ),
746 749 "multiline": True,
747 750 "complete_style": self.pt_complete_style,
748 751 "input_processors": [
749 752 # Highlight matching brackets, but only when this setting is
750 753 # enabled, and only when the DEFAULT_BUFFER has the focus.
751 754 ConditionalProcessor(
752 755 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
753 756 filter=HasFocus(DEFAULT_BUFFER)
754 757 & ~IsDone()
755 758 & Condition(lambda: self.highlight_matching_brackets),
756 759 ),
757 760 # Show auto-suggestion in lines other than the last line.
758 761 ConditionalProcessor(
759 762 processor=AppendAutoSuggestionInAnyLine(),
760 763 filter=HasFocus(DEFAULT_BUFFER)
761 764 & ~IsDone()
762 765 & Condition(
763 766 lambda: isinstance(
764 767 self.auto_suggest, NavigableAutoSuggestFromHistory
765 768 )
766 769 ),
767 770 ),
768 771 ],
769 772 }
770 773 if not PTK3:
771 774 options['inputhook'] = self.inputhook
772 775
773 776 return options
774 777
775 778 def prompt_for_code(self):
776 779 if self.rl_next_input:
777 780 default = self.rl_next_input
778 781 self.rl_next_input = None
779 782 else:
780 783 default = ''
781 784
782 785 # In order to make sure that asyncio code written in the
783 786 # interactive shell doesn't interfere with the prompt, we run the
784 787 # prompt in a different event loop.
785 788 # If we don't do this, people could spawn coroutine with a
786 789 # while/true inside which will freeze the prompt.
787 790
788 791 policy = asyncio.get_event_loop_policy()
789 792 old_loop = get_asyncio_loop()
790 793
791 794 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
792 795 # to get the current event loop.
793 796 # This will probably be replaced by an attribute or input argument,
794 797 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
795 798 if old_loop is not self.pt_loop:
796 799 policy.set_event_loop(self.pt_loop)
797 800 try:
798 801 with patch_stdout(raw=True):
799 802 text = self.pt_app.prompt(
800 803 default=default,
801 804 **self._extra_prompt_options())
802 805 finally:
803 806 # Restore the original event loop.
804 807 if old_loop is not None and old_loop is not self.pt_loop:
805 808 policy.set_event_loop(old_loop)
806 809
807 810 return text
808 811
809 812 def enable_win_unicode_console(self):
810 813 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
811 814 # console by default, so WUC shouldn't be needed.
812 815 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
813 816 DeprecationWarning,
814 817 stacklevel=2)
815 818
816 819 def init_io(self):
817 820 if sys.platform not in {'win32', 'cli'}:
818 821 return
819 822
820 823 import colorama
821 824 colorama.init()
822 825
823 826 def init_magics(self):
824 827 super(TerminalInteractiveShell, self).init_magics()
825 828 self.register_magics(TerminalMagics)
826 829
827 830 def init_alias(self):
828 831 # The parent class defines aliases that can be safely used with any
829 832 # frontend.
830 833 super(TerminalInteractiveShell, self).init_alias()
831 834
832 835 # Now define aliases that only make sense on the terminal, because they
833 836 # need direct access to the console in a way that we can't emulate in
834 837 # GUI or web frontend
835 838 if os.name == 'posix':
836 839 for cmd in ('clear', 'more', 'less', 'man'):
837 840 self.alias_manager.soft_define_alias(cmd, cmd)
838 841
839 842
840 843 def __init__(self, *args, **kwargs) -> None:
841 844 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
842 845 self._set_autosuggestions(self.autosuggestions_provider)
843 846 self.init_prompt_toolkit_cli()
844 847 self.init_term_title()
845 848 self.keep_running = True
846 849 self._set_formatter(self.autoformatter)
847 850
848 851
849 852 def ask_exit(self):
850 853 self.keep_running = False
851 854
852 855 rl_next_input = None
853 856
854 857 def interact(self):
855 858 self.keep_running = True
856 859 while self.keep_running:
857 860 print(self.separate_in, end='')
858 861
859 862 try:
860 863 code = self.prompt_for_code()
861 864 except EOFError:
862 865 if (not self.confirm_exit) \
863 866 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
864 867 self.ask_exit()
865 868
866 869 else:
867 870 if code:
868 871 self.run_cell(code, store_history=True)
869 872
870 873 def mainloop(self):
871 874 # An extra layer of protection in case someone mashing Ctrl-C breaks
872 875 # out of our internal code.
873 876 while True:
874 877 try:
875 878 self.interact()
876 879 break
877 880 except KeyboardInterrupt as e:
878 881 print("\n%s escaped interact()\n" % type(e).__name__)
879 882 finally:
880 883 # An interrupt during the eventloop will mess up the
881 884 # internal state of the prompt_toolkit library.
882 885 # Stopping the eventloop fixes this, see
883 886 # https://github.com/ipython/ipython/pull/9867
884 887 if hasattr(self, '_eventloop'):
885 888 self._eventloop.stop()
886 889
887 890 self.restore_term_title()
888 891
889 892 # try to call some at-exit operation optimistically as some things can't
890 893 # be done during interpreter shutdown. this is technically inaccurate as
891 894 # this make mainlool not re-callable, but that should be a rare if not
892 895 # in existent use case.
893 896
894 897 self._atexit_once()
895 898
896 899
897 900 _inputhook = None
898 901 def inputhook(self, context):
899 902 if self._inputhook is not None:
900 903 self._inputhook(context)
901 904
902 905 active_eventloop = None
903 906 def enable_gui(self, gui=None):
904 907 if gui and (gui not in {"inline", "webagg"}):
905 908 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
906 909 else:
907 910 self.active_eventloop = self._inputhook = None
908 911
909 912 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
910 913 # this inputhook.
911 914 if PTK3:
912 915 import asyncio
913 916 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
914 917
915 918 if gui == 'asyncio':
916 919 # When we integrate the asyncio event loop, run the UI in the
917 920 # same event loop as the rest of the code. don't use an actual
918 921 # input hook. (Asyncio is not made for nesting event loops.)
919 922 self.pt_loop = get_asyncio_loop()
920 923
921 924 elif self._inputhook:
922 925 # If an inputhook was set, create a new asyncio event loop with
923 926 # this inputhook for the prompt.
924 927 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
925 928 else:
926 929 # When there's no inputhook, run the prompt in a separate
927 930 # asyncio event loop.
928 931 self.pt_loop = asyncio.new_event_loop()
929 932
930 933 # Run !system commands directly, not through pipes, so terminal programs
931 934 # work correctly.
932 935 system = InteractiveShell.system_raw
933 936
934 937 def auto_rewrite_input(self, cmd):
935 938 """Overridden from the parent class to use fancy rewriting prompt"""
936 939 if not self.show_rewritten_input:
937 940 return
938 941
939 942 tokens = self.prompts.rewrite_prompt_tokens()
940 943 if self.pt_app:
941 944 print_formatted_text(PygmentsTokens(tokens), end='',
942 945 style=self.pt_app.app.style)
943 946 print(cmd)
944 947 else:
945 948 prompt = ''.join(s for t, s in tokens)
946 949 print(prompt, cmd, sep='')
947 950
948 951 _prompts_before = None
949 952 def switch_doctest_mode(self, mode):
950 953 """Switch prompts to classic for %doctest_mode"""
951 954 if mode:
952 955 self._prompts_before = self.prompts
953 956 self.prompts = ClassicPrompts(self)
954 957 elif self._prompts_before:
955 958 self.prompts = self._prompts_before
956 959 self._prompts_before = None
957 960 # self._update_layout()
958 961
959 962
960 963 InteractiveShellABC.register(TerminalInteractiveShell)
961 964
962 965 if __name__ == '__main__':
963 966 TerminalInteractiveShell.instance().interact()
@@ -1,422 +1,434 b''
1 1 import pytest
2 2 from IPython.terminal.shortcuts.auto_suggest import (
3 3 accept,
4 4 accept_in_vi_insert_mode,
5 5 accept_token,
6 6 accept_character,
7 7 accept_word,
8 8 accept_and_keep_cursor,
9 9 discard,
10 10 NavigableAutoSuggestFromHistory,
11 11 swap_autosuggestion_up,
12 12 swap_autosuggestion_down,
13 13 )
14 14 from IPython.terminal.shortcuts.auto_match import skip_over
15 15 from IPython.terminal.shortcuts import create_ipython_shortcuts
16 16
17 17 from prompt_toolkit.history import InMemoryHistory
18 18 from prompt_toolkit.buffer import Buffer
19 19 from prompt_toolkit.document import Document
20 20 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
21 21
22 22 from unittest.mock import patch, Mock
23 23
24 24
25 25 def make_event(text, cursor, suggestion):
26 26 event = Mock()
27 27 event.current_buffer = Mock()
28 28 event.current_buffer.suggestion = Mock()
29 29 event.current_buffer.text = text
30 30 event.current_buffer.cursor_position = cursor
31 31 event.current_buffer.suggestion.text = suggestion
32 32 event.current_buffer.document = Document(text=text, cursor_position=cursor)
33 33 return event
34 34
35 35
36 36 @pytest.mark.parametrize(
37 37 "text, suggestion, expected",
38 38 [
39 39 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"),
40 40 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"),
41 41 ],
42 42 )
43 43 def test_accept(text, suggestion, expected):
44 44 event = make_event(text, len(text), suggestion)
45 45 buffer = event.current_buffer
46 46 buffer.insert_text = Mock()
47 47 accept(event)
48 48 assert buffer.insert_text.called
49 49 assert buffer.insert_text.call_args[0] == (expected,)
50 50
51 51
52 52 @pytest.mark.parametrize(
53 53 "text, suggestion",
54 54 [
55 55 ("", "def out(tag: str, n=50):"),
56 56 ("def ", "out(tag: str, n=50):"),
57 57 ],
58 58 )
59 59 def test_discard(text, suggestion):
60 60 event = make_event(text, len(text), suggestion)
61 61 buffer = event.current_buffer
62 62 buffer.insert_text = Mock()
63 63 discard(event)
64 64 assert not buffer.insert_text.called
65 65 assert buffer.suggestion is None
66 66
67 67
68 68 @pytest.mark.parametrize(
69 69 "text, cursor, suggestion, called",
70 70 [
71 71 ("123456", 6, "123456789", True),
72 72 ("123456", 3, "123456789", False),
73 73 ("123456 \n789", 6, "123456789", True),
74 74 ],
75 75 )
76 76 def test_autosuggest_at_EOL(text, cursor, suggestion, called):
77 77 """
78 78 test that autosuggest is only applied at end of line.
79 79 """
80 80
81 81 event = make_event(text, cursor, suggestion)
82 82 event.current_buffer.insert_text = Mock()
83 83 accept_in_vi_insert_mode(event)
84 84 if called:
85 85 event.current_buffer.insert_text.assert_called()
86 86 else:
87 87 event.current_buffer.insert_text.assert_not_called()
88 88 # event.current_buffer.document.get_end_of_line_position.assert_called()
89 89
90 90
91 91 @pytest.mark.parametrize(
92 92 "text, suggestion, expected",
93 93 [
94 94 ("", "def out(tag: str, n=50):", "def "),
95 95 ("d", "ef out(tag: str, n=50):", "ef "),
96 96 ("de ", "f out(tag: str, n=50):", "f "),
97 97 ("def", " out(tag: str, n=50):", " "),
98 98 ("def ", "out(tag: str, n=50):", "out("),
99 99 ("def o", "ut(tag: str, n=50):", "ut("),
100 100 ("def ou", "t(tag: str, n=50):", "t("),
101 101 ("def out", "(tag: str, n=50):", "("),
102 102 ("def out(", "tag: str, n=50):", "tag: "),
103 103 ("def out(t", "ag: str, n=50):", "ag: "),
104 104 ("def out(ta", "g: str, n=50):", "g: "),
105 105 ("def out(tag", ": str, n=50):", ": "),
106 106 ("def out(tag:", " str, n=50):", " "),
107 107 ("def out(tag: ", "str, n=50):", "str, "),
108 108 ("def out(tag: s", "tr, n=50):", "tr, "),
109 109 ("def out(tag: st", "r, n=50):", "r, "),
110 110 ("def out(tag: str", ", n=50):", ", n"),
111 111 ("def out(tag: str,", " n=50):", " n"),
112 112 ("def out(tag: str, ", "n=50):", "n="),
113 113 ("def out(tag: str, n", "=50):", "="),
114 114 ("def out(tag: str, n=", "50):", "50)"),
115 115 ("def out(tag: str, n=5", "0):", "0)"),
116 116 ("def out(tag: str, n=50", "):", "):"),
117 117 ("def out(tag: str, n=50)", ":", ":"),
118 118 ],
119 119 )
120 120 def test_autosuggest_token(text, suggestion, expected):
121 121 event = make_event(text, len(text), suggestion)
122 122 event.current_buffer.insert_text = Mock()
123 123 accept_token(event)
124 124 assert event.current_buffer.insert_text.called
125 125 assert event.current_buffer.insert_text.call_args[0] == (expected,)
126 126
127 127
128 128 @pytest.mark.parametrize(
129 129 "text, suggestion, expected",
130 130 [
131 131 ("", "def out(tag: str, n=50):", "d"),
132 132 ("d", "ef out(tag: str, n=50):", "e"),
133 133 ("de ", "f out(tag: str, n=50):", "f"),
134 134 ("def", " out(tag: str, n=50):", " "),
135 135 ],
136 136 )
137 137 def test_accept_character(text, suggestion, expected):
138 138 event = make_event(text, len(text), suggestion)
139 139 event.current_buffer.insert_text = Mock()
140 140 accept_character(event)
141 141 assert event.current_buffer.insert_text.called
142 142 assert event.current_buffer.insert_text.call_args[0] == (expected,)
143 143
144 144
145 145 @pytest.mark.parametrize(
146 146 "text, suggestion, expected",
147 147 [
148 148 ("", "def out(tag: str, n=50):", "def "),
149 149 ("d", "ef out(tag: str, n=50):", "ef "),
150 150 ("de", "f out(tag: str, n=50):", "f "),
151 151 ("def", " out(tag: str, n=50):", " "),
152 152 # (this is why we also have accept_token)
153 153 ("def ", "out(tag: str, n=50):", "out(tag: "),
154 154 ],
155 155 )
156 156 def test_accept_word(text, suggestion, expected):
157 157 event = make_event(text, len(text), suggestion)
158 158 event.current_buffer.insert_text = Mock()
159 159 accept_word(event)
160 160 assert event.current_buffer.insert_text.called
161 161 assert event.current_buffer.insert_text.call_args[0] == (expected,)
162 162
163 163
164 164 @pytest.mark.parametrize(
165 165 "text, suggestion, expected, cursor",
166 166 [
167 167 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0),
168 168 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4),
169 169 ],
170 170 )
171 171 def test_accept_and_keep_cursor(text, suggestion, expected, cursor):
172 172 event = make_event(text, cursor, suggestion)
173 173 buffer = event.current_buffer
174 174 buffer.insert_text = Mock()
175 175 accept_and_keep_cursor(event)
176 176 assert buffer.insert_text.called
177 177 assert buffer.insert_text.call_args[0] == (expected,)
178 178 assert buffer.cursor_position == cursor
179 179
180 180
181 181 def test_autosuggest_token_empty():
182 182 full = "def out(tag: str, n=50):"
183 183 event = make_event(full, len(full), "")
184 184 event.current_buffer.insert_text = Mock()
185 185
186 186 with patch(
187 187 "prompt_toolkit.key_binding.bindings.named_commands.forward_word"
188 188 ) as forward_word:
189 189 accept_token(event)
190 190 assert not event.current_buffer.insert_text.called
191 191 assert forward_word.called
192 192
193 193
194 194 def test_other_providers():
195 195 """Ensure that swapping autosuggestions does not break with other providers"""
196 196 provider = AutoSuggestFromHistory()
197 197 ip = get_ipython()
198 198 ip.auto_suggest = provider
199 199 event = Mock()
200 200 event.current_buffer = Buffer()
201 201 assert swap_autosuggestion_up(event) is None
202 202 assert swap_autosuggestion_down(event) is None
203 203
204 204
205 205 async def test_navigable_provider():
206 206 provider = NavigableAutoSuggestFromHistory()
207 207 history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
208 208 buffer = Buffer(history=history)
209 209 ip = get_ipython()
210 210 ip.auto_suggest = provider
211 211
212 212 async for _ in history.load():
213 213 pass
214 214
215 215 buffer.cursor_position = 5
216 216 buffer.text = "very"
217 217
218 218 up = swap_autosuggestion_up
219 219 down = swap_autosuggestion_down
220 220
221 221 event = Mock()
222 222 event.current_buffer = buffer
223 223
224 224 def get_suggestion():
225 225 suggestion = provider.get_suggestion(buffer, buffer.document)
226 226 buffer.suggestion = suggestion
227 227 return suggestion
228 228
229 229 assert get_suggestion().text == "_c"
230 230
231 231 # should go up
232 232 up(event)
233 233 assert get_suggestion().text == "_b"
234 234
235 235 # should skip over 'very' which is identical to buffer content
236 236 up(event)
237 237 assert get_suggestion().text == "_a"
238 238
239 239 # should cycle back to beginning
240 240 up(event)
241 241 assert get_suggestion().text == "_c"
242 242
243 243 # should cycle back through end boundary
244 244 down(event)
245 245 assert get_suggestion().text == "_a"
246 246
247 247 down(event)
248 248 assert get_suggestion().text == "_b"
249 249
250 250 down(event)
251 251 assert get_suggestion().text == "_c"
252 252
253 253 down(event)
254 254 assert get_suggestion().text == "_a"
255 255
256 256
257 257 async def test_navigable_provider_multiline_entries():
258 258 provider = NavigableAutoSuggestFromHistory()
259 259 history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
260 260 buffer = Buffer(history=history)
261 261 ip = get_ipython()
262 262 ip.auto_suggest = provider
263 263
264 264 async for _ in history.load():
265 265 pass
266 266
267 267 buffer.cursor_position = 5
268 268 buffer.text = "very"
269 269 up = swap_autosuggestion_up
270 270 down = swap_autosuggestion_down
271 271
272 272 event = Mock()
273 273 event.current_buffer = buffer
274 274
275 275 def get_suggestion():
276 276 suggestion = provider.get_suggestion(buffer, buffer.document)
277 277 buffer.suggestion = suggestion
278 278 return suggestion
279 279
280 280 assert get_suggestion().text == "_c"
281 281
282 282 up(event)
283 283 assert get_suggestion().text == "_b"
284 284
285 285 up(event)
286 286 assert get_suggestion().text == "_a"
287 287
288 288 down(event)
289 289 assert get_suggestion().text == "_b"
290 290
291 291 down(event)
292 292 assert get_suggestion().text == "_c"
293 293
294 294
295 295 def create_session_mock():
296 296 session = Mock()
297 297 session.default_buffer = Buffer()
298 298 return session
299 299
300 300
301 301 def test_navigable_provider_connection():
302 302 provider = NavigableAutoSuggestFromHistory()
303 303 provider.skip_lines = 1
304 304
305 305 session_1 = create_session_mock()
306 306 provider.connect(session_1)
307 307
308 308 assert provider.skip_lines == 1
309 309 session_1.default_buffer.on_text_insert.fire()
310 310 assert provider.skip_lines == 0
311 311
312 312 session_2 = create_session_mock()
313 313 provider.connect(session_2)
314 314 provider.skip_lines = 2
315 315
316 316 assert provider.skip_lines == 2
317 317 session_2.default_buffer.on_text_insert.fire()
318 318 assert provider.skip_lines == 0
319 319
320 320 provider.skip_lines = 3
321 321 provider.disconnect()
322 322 session_1.default_buffer.on_text_insert.fire()
323 323 session_2.default_buffer.on_text_insert.fire()
324 324 assert provider.skip_lines == 3
325 325
326 326
327 327 @pytest.fixture
328 328 def ipython_with_prompt():
329 329 ip = get_ipython()
330 330 ip.pt_app = Mock()
331 331 ip.pt_app.key_bindings = create_ipython_shortcuts(ip)
332 332 try:
333 333 yield ip
334 334 finally:
335 335 ip.pt_app = None
336 336
337 337
338 338 def find_bindings_by_command(command):
339 339 ip = get_ipython()
340 340 return [
341 341 binding
342 342 for binding in ip.pt_app.key_bindings.bindings
343 343 if binding.handler == command
344 344 ]
345 345
346 346
347 347 def test_modify_unique_shortcut(ipython_with_prompt):
348 348 matched = find_bindings_by_command(accept_token)
349 349 assert len(matched) == 1
350 350
351 351 ipython_with_prompt.shortcuts = [
352 352 {"command": "IPython:auto_suggest.accept_token", "new_keys": ["a", "b", "c"]}
353 353 ]
354 354 matched = find_bindings_by_command(accept_token)
355 355 assert len(matched) == 1
356 356 assert list(matched[0].keys) == ["a", "b", "c"]
357 357
358 358 ipython_with_prompt.shortcuts = [
359 359 {"command": "IPython:auto_suggest.accept_token", "new_keys": []}
360 360 ]
361 361 matched = find_bindings_by_command(accept_token)
362 362 assert len(matched) == 0
363 363
364 364 ipython_with_prompt.shortcuts = []
365 365 matched = find_bindings_by_command(accept_token)
366 366 assert len(matched) == 1
367 367
368 368
369 369 def test_modify_shortcut_with_filters(ipython_with_prompt):
370 370 matched = find_bindings_by_command(skip_over)
371 371 matched_keys = {m.keys[0] for m in matched}
372 372 assert matched_keys == {")", "]", "}", "'", '"'}
373 373
374 374 with pytest.raises(ValueError, match="Multiple shortcuts matching"):
375 375 ipython_with_prompt.shortcuts = [
376 376 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"]}
377 377 ]
378 378
379 379 ipython_with_prompt.shortcuts = [
380 380 {
381 381 "command": "IPython:auto_match.skip_over",
382 382 "new_keys": ["x"],
383 383 "match_filter": "focused_insert & auto_match & followed_by_single_quote",
384 384 }
385 385 ]
386 386 matched = find_bindings_by_command(skip_over)
387 387 matched_keys = {m.keys[0] for m in matched}
388 388 assert matched_keys == {")", "]", "}", "x", '"'}
389 389
390 390
391 def test_command():
391 def example_command():
392 392 pass
393 393
394 394
395 395 def test_add_shortcut_for_new_command(ipython_with_prompt):
396 matched = find_bindings_by_command(test_command)
396 matched = find_bindings_by_command(example_command)
397 397 assert len(matched) == 0
398 398
399 with pytest.raises(ValueError, match="test_command is not a known"):
400 ipython_with_prompt.shortcuts = [{"command": "test_command", "new_keys": ["x"]}]
401 matched = find_bindings_by_command(test_command)
399 with pytest.raises(ValueError, match="example_command is not a known"):
400 ipython_with_prompt.shortcuts = [
401 {"command": "example_command", "new_keys": ["x"]}
402 ]
403 matched = find_bindings_by_command(example_command)
402 404 assert len(matched) == 0
403 405
404 406
405 407 def test_add_shortcut_for_existing_command(ipython_with_prompt):
406 408 matched = find_bindings_by_command(skip_over)
407 409 assert len(matched) == 5
408 410
409 411 with pytest.raises(ValueError, match="Cannot add a shortcut without keys"):
410 412 ipython_with_prompt.shortcuts = [
411 413 {"command": "IPython:auto_match.skip_over", "new_keys": [], "create": True}
412 414 ]
413 415
414 416 ipython_with_prompt.shortcuts = [
415 417 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
416 418 ]
417 419 matched = find_bindings_by_command(skip_over)
418 420 assert len(matched) == 6
419 421
420 422 ipython_with_prompt.shortcuts = []
421 423 matched = find_bindings_by_command(skip_over)
422 424 assert len(matched) == 5
425
426
427 def test_setting_shortcuts_before_pt_app_init():
428 ipython = get_ipython()
429 assert ipython.pt_app is None
430 shortcuts = [
431 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
432 ]
433 ipython.shortcuts = shortcuts
434 assert ipython.shortcuts == shortcuts
General Comments 0
You need to be logged in to leave comments. Login now