##// END OF EJS Templates
Merge branch 'main' into create_app_session_for_debugger_prompt
Maor Kleinberger -
r28087:23d5d48f merge
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (670 lines changed) Show them Hide them
@@ -0,0 +1,670
1 """
2 Module to define and register Terminal IPython shortcuts with
3 :mod:`prompt_toolkit`
4 """
5
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
8
9 import os
10 import re
11 import signal
12 import sys
13 import warnings
14 from typing import Callable, Dict, Union
15
16 from prompt_toolkit.application.current import get_app
17 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
18 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
19 from prompt_toolkit.filters import has_focus as has_focus_impl
20 from prompt_toolkit.filters import (
21 has_selection,
22 has_suggestion,
23 vi_insert_mode,
24 vi_mode,
25 )
26 from prompt_toolkit.key_binding import KeyBindings
27 from prompt_toolkit.key_binding.bindings import named_commands as nc
28 from prompt_toolkit.key_binding.bindings.completion import (
29 display_completions_like_readline,
30 )
31 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
32 from prompt_toolkit.layout.layout import FocusableElement
33
34 from IPython.terminal.shortcuts import auto_match as match
35 from IPython.terminal.shortcuts import auto_suggest
36 from IPython.utils.decorators import undoc
37
38 __all__ = ["create_ipython_shortcuts"]
39
40
41 @undoc
42 @Condition
43 def cursor_in_leading_ws():
44 before = get_app().current_buffer.document.current_line_before_cursor
45 return (not before) or before.isspace()
46
47
48 def has_focus(value: FocusableElement):
49 """Wrapper around has_focus adding a nice `__name__` to tester function"""
50 tester = has_focus_impl(value).func
51 tester.__name__ = f"is_focused({value})"
52 return Condition(tester)
53
54
55 @undoc
56 @Condition
57 def has_line_below() -> bool:
58 document = get_app().current_buffer.document
59 return document.cursor_position_row < len(document.lines) - 1
60
61
62 @undoc
63 @Condition
64 def has_line_above() -> bool:
65 document = get_app().current_buffer.document
66 return document.cursor_position_row != 0
67
68
69 def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindings:
70 """Set up the prompt_toolkit keyboard shortcuts for IPython.
71
72 Parameters
73 ----------
74 shell: InteractiveShell
75 The current IPython shell Instance
76 for_all_platforms: bool (default false)
77 This parameter is mostly used in generating the documentation
78 to create the shortcut binding for all the platforms, and export
79 them.
80
81 Returns
82 -------
83 KeyBindings
84 the keybinding instance for prompt toolkit.
85
86 """
87 # Warning: if possible, do NOT define handler functions in the locals
88 # scope of this function, instead define functions in the global
89 # scope, or a separate module, and include a user-friendly docstring
90 # describing the action.
91
92 kb = KeyBindings()
93 insert_mode = vi_insert_mode | emacs_insert_mode
94
95 if getattr(shell, "handle_return", None):
96 return_handler = shell.handle_return(shell)
97 else:
98 return_handler = newline_or_execute_outer(shell)
99
100 kb.add("enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode))(
101 return_handler
102 )
103
104 @Condition
105 def ebivim():
106 return shell.emacs_bindings_in_vi_insert_mode
107
108 @kb.add(
109 "escape",
110 "enter",
111 filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim),
112 )
113 def reformat_and_execute(event):
114 """Reformat code and execute it"""
115 reformat_text_before_cursor(
116 event.current_buffer, event.current_buffer.document, shell
117 )
118 event.current_buffer.validate_and_handle()
119
120 kb.add("c-\\")(quit)
121
122 kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
123 previous_history_or_previous_completion
124 )
125
126 kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
127 next_history_or_next_completion
128 )
129
130 kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))(
131 dismiss_completion
132 )
133
134 kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
135
136 kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
137
138 supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP"))
139 kb.add("c-z", filter=supports_suspend)(suspend_to_bg)
140
141 # Ctrl+I == Tab
142 kb.add(
143 "tab",
144 filter=(
145 has_focus(DEFAULT_BUFFER)
146 & ~has_selection
147 & insert_mode
148 & cursor_in_leading_ws
149 ),
150 )(indent_buffer)
151 kb.add("c-o", filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode))(
152 newline_autoindent_outer(shell.input_transformer_manager)
153 )
154
155 kb.add("f2", filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
156
157 @Condition
158 def auto_match():
159 return shell.auto_match
160
161 def all_quotes_paired(quote, buf):
162 paired = True
163 i = 0
164 while i < len(buf):
165 c = buf[i]
166 if c == quote:
167 paired = not paired
168 elif c == "\\":
169 i += 1
170 i += 1
171 return paired
172
173 focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER)
174 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
175 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
176
177 def preceding_text(pattern: Union[str, Callable]):
178 if pattern in _preceding_text_cache:
179 return _preceding_text_cache[pattern]
180
181 if callable(pattern):
182
183 def _preceding_text():
184 app = get_app()
185 before_cursor = app.current_buffer.document.current_line_before_cursor
186 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
187 return bool(pattern(before_cursor)) # type: ignore[operator]
188
189 else:
190 m = re.compile(pattern)
191
192 def _preceding_text():
193 app = get_app()
194 before_cursor = app.current_buffer.document.current_line_before_cursor
195 return bool(m.match(before_cursor))
196
197 _preceding_text.__name__ = f"preceding_text({pattern!r})"
198
199 condition = Condition(_preceding_text)
200 _preceding_text_cache[pattern] = condition
201 return condition
202
203 def following_text(pattern):
204 try:
205 return _following_text_cache[pattern]
206 except KeyError:
207 pass
208 m = re.compile(pattern)
209
210 def _following_text():
211 app = get_app()
212 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
213
214 _following_text.__name__ = f"following_text({pattern!r})"
215
216 condition = Condition(_following_text)
217 _following_text_cache[pattern] = condition
218 return condition
219
220 @Condition
221 def not_inside_unclosed_string():
222 app = get_app()
223 s = app.current_buffer.document.text_before_cursor
224 # remove escaped quotes
225 s = s.replace('\\"', "").replace("\\'", "")
226 # remove triple-quoted string literals
227 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
228 # remove single-quoted string literals
229 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
230 return not ('"' in s or "'" in s)
231
232 # auto match
233 for key, cmd in match.auto_match_parens.items():
234 kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))(
235 cmd
236 )
237
238 # raw string
239 for key, cmd in match.auto_match_parens_raw_string.items():
240 kb.add(
241 key,
242 filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"),
243 )(cmd)
244
245 kb.add(
246 '"',
247 filter=focused_insert
248 & auto_match
249 & not_inside_unclosed_string
250 & preceding_text(lambda line: all_quotes_paired('"', line))
251 & following_text(r"[,)}\]]|$"),
252 )(match.double_quote)
253
254 kb.add(
255 "'",
256 filter=focused_insert
257 & auto_match
258 & not_inside_unclosed_string
259 & preceding_text(lambda line: all_quotes_paired("'", line))
260 & following_text(r"[,)}\]]|$"),
261 )(match.single_quote)
262
263 kb.add(
264 '"',
265 filter=focused_insert
266 & auto_match
267 & not_inside_unclosed_string
268 & preceding_text(r'^.*""$'),
269 )(match.docstring_double_quotes)
270
271 kb.add(
272 "'",
273 filter=focused_insert
274 & auto_match
275 & not_inside_unclosed_string
276 & preceding_text(r"^.*''$"),
277 )(match.docstring_single_quotes)
278
279 # just move cursor
280 kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))(
281 match.skip_over
282 )
283 kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))(
284 match.skip_over
285 )
286 kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))(
287 match.skip_over
288 )
289 kb.add('"', filter=focused_insert & auto_match & following_text('^"'))(
290 match.skip_over
291 )
292 kb.add("'", filter=focused_insert & auto_match & following_text("^'"))(
293 match.skip_over
294 )
295
296 kb.add(
297 "backspace",
298 filter=focused_insert
299 & preceding_text(r".*\($")
300 & auto_match
301 & following_text(r"^\)"),
302 )(match.delete_pair)
303 kb.add(
304 "backspace",
305 filter=focused_insert
306 & preceding_text(r".*\[$")
307 & auto_match
308 & following_text(r"^\]"),
309 )(match.delete_pair)
310 kb.add(
311 "backspace",
312 filter=focused_insert
313 & preceding_text(r".*\{$")
314 & auto_match
315 & following_text(r"^\}"),
316 )(match.delete_pair)
317 kb.add(
318 "backspace",
319 filter=focused_insert
320 & preceding_text('.*"$')
321 & auto_match
322 & following_text('^"'),
323 )(match.delete_pair)
324 kb.add(
325 "backspace",
326 filter=focused_insert
327 & preceding_text(r".*'$")
328 & auto_match
329 & following_text(r"^'"),
330 )(match.delete_pair)
331
332 if shell.display_completions == "readlinelike":
333 kb.add(
334 "c-i",
335 filter=(
336 has_focus(DEFAULT_BUFFER)
337 & ~has_selection
338 & insert_mode
339 & ~cursor_in_leading_ws
340 ),
341 )(display_completions_like_readline)
342
343 if sys.platform == "win32" or for_all_platforms:
344 kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste)
345
346 focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode
347
348 # autosuggestions
349 @Condition
350 def navigable_suggestions():
351 return isinstance(
352 shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory
353 )
354
355 kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))(
356 auto_suggest.accept_in_vi_insert_mode
357 )
358 kb.add("c-e", filter=focused_insert_vi & ebivim)(
359 auto_suggest.accept_in_vi_insert_mode
360 )
361 kb.add("c-f", filter=focused_insert_vi)(auto_suggest.accept)
362 kb.add("escape", "f", filter=focused_insert_vi & ebivim)(auto_suggest.accept_word)
363 kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
364 auto_suggest.accept_token
365 )
366 kb.add(
367 "escape", filter=has_suggestion & has_focus(DEFAULT_BUFFER) & emacs_insert_mode
368 )(auto_suggest.discard)
369 kb.add(
370 "up",
371 filter=navigable_suggestions
372 & ~has_line_above
373 & has_suggestion
374 & has_focus(DEFAULT_BUFFER),
375 )(auto_suggest.swap_autosuggestion_up(shell.auto_suggest))
376 kb.add(
377 "down",
378 filter=navigable_suggestions
379 & ~has_line_below
380 & has_suggestion
381 & has_focus(DEFAULT_BUFFER),
382 )(auto_suggest.swap_autosuggestion_down(shell.auto_suggest))
383 kb.add(
384 "up", filter=has_line_above & navigable_suggestions & has_focus(DEFAULT_BUFFER)
385 )(auto_suggest.up_and_update_hint)
386 kb.add(
387 "down",
388 filter=has_line_below & navigable_suggestions & has_focus(DEFAULT_BUFFER),
389 )(auto_suggest.down_and_update_hint)
390 kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
391 auto_suggest.accept_character
392 )
393 kb.add("c-left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
394 auto_suggest.accept_and_move_cursor_left
395 )
396 kb.add("c-down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
397 auto_suggest.accept_and_keep_cursor
398 )
399 kb.add("backspace", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
400 auto_suggest.backspace_and_resume_hint
401 )
402
403 # Simple Control keybindings
404 key_cmd_dict = {
405 "c-a": nc.beginning_of_line,
406 "c-b": nc.backward_char,
407 "c-k": nc.kill_line,
408 "c-w": nc.backward_kill_word,
409 "c-y": nc.yank,
410 "c-_": nc.undo,
411 }
412
413 for key, cmd in key_cmd_dict.items():
414 kb.add(key, filter=focused_insert_vi & ebivim)(cmd)
415
416 # Alt and Combo Control keybindings
417 keys_cmd_dict = {
418 # Control Combos
419 ("c-x", "c-e"): nc.edit_and_execute,
420 ("c-x", "e"): nc.edit_and_execute,
421 # Alt
422 ("escape", "b"): nc.backward_word,
423 ("escape", "c"): nc.capitalize_word,
424 ("escape", "d"): nc.kill_word,
425 ("escape", "h"): nc.backward_kill_word,
426 ("escape", "l"): nc.downcase_word,
427 ("escape", "u"): nc.uppercase_word,
428 ("escape", "y"): nc.yank_pop,
429 ("escape", "."): nc.yank_last_arg,
430 }
431
432 for keys, cmd in keys_cmd_dict.items():
433 kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd)
434
435 def get_input_mode(self):
436 app = get_app()
437 app.ttimeoutlen = shell.ttimeoutlen
438 app.timeoutlen = shell.timeoutlen
439
440 return self._input_mode
441
442 def set_input_mode(self, mode):
443 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
444 cursor = "\x1b[{} q".format(shape)
445
446 sys.stdout.write(cursor)
447 sys.stdout.flush()
448
449 self._input_mode = mode
450
451 if shell.editing_mode == "vi" and shell.modal_cursor:
452 ViState._input_mode = InputMode.INSERT # type: ignore
453 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
454 return kb
455
456
457 def reformat_text_before_cursor(buffer, document, shell):
458 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
459 try:
460 formatted_text = shell.reformat_handler(text)
461 buffer.insert_text(formatted_text)
462 except Exception as e:
463 buffer.insert_text(text)
464
465
466 def newline_or_execute_outer(shell):
467 def newline_or_execute(event):
468 """When the user presses return, insert a newline or execute the code."""
469 b = event.current_buffer
470 d = b.document
471
472 if b.complete_state:
473 cc = b.complete_state.current_completion
474 if cc:
475 b.apply_completion(cc)
476 else:
477 b.cancel_completion()
478 return
479
480 # If there's only one line, treat it as if the cursor is at the end.
481 # See https://github.com/ipython/ipython/issues/10425
482 if d.line_count == 1:
483 check_text = d.text
484 else:
485 check_text = d.text[: d.cursor_position]
486 status, indent = shell.check_complete(check_text)
487
488 # if all we have after the cursor is whitespace: reformat current text
489 # before cursor
490 after_cursor = d.text[d.cursor_position :]
491 reformatted = False
492 if not after_cursor.strip():
493 reformat_text_before_cursor(b, d, shell)
494 reformatted = True
495 if not (
496 d.on_last_line
497 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
498 ):
499 if shell.autoindent:
500 b.insert_text("\n" + indent)
501 else:
502 b.insert_text("\n")
503 return
504
505 if (status != "incomplete") and b.accept_handler:
506 if not reformatted:
507 reformat_text_before_cursor(b, d, shell)
508 b.validate_and_handle()
509 else:
510 if shell.autoindent:
511 b.insert_text("\n" + indent)
512 else:
513 b.insert_text("\n")
514
515 newline_or_execute.__qualname__ = "newline_or_execute"
516
517 return newline_or_execute
518
519
520 def previous_history_or_previous_completion(event):
521 """
522 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
523
524 If completer is open this still select previous completion.
525 """
526 event.current_buffer.auto_up()
527
528
529 def next_history_or_next_completion(event):
530 """
531 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
532
533 If completer is open this still select next completion.
534 """
535 event.current_buffer.auto_down()
536
537
538 def dismiss_completion(event):
539 """Dismiss completion"""
540 b = event.current_buffer
541 if b.complete_state:
542 b.cancel_completion()
543
544
545 def reset_buffer(event):
546 """Reset buffer"""
547 b = event.current_buffer
548 if b.complete_state:
549 b.cancel_completion()
550 else:
551 b.reset()
552
553
554 def reset_search_buffer(event):
555 """Reset search buffer"""
556 if event.current_buffer.document.text:
557 event.current_buffer.reset()
558 else:
559 event.app.layout.focus(DEFAULT_BUFFER)
560
561
562 def suspend_to_bg(event):
563 """Suspend to background"""
564 event.app.suspend_to_background()
565
566
567 def quit(event):
568 """
569 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
570
571 On platforms that support SIGQUIT, send SIGQUIT to the current process.
572 On other platforms, just exit the process with a message.
573 """
574 sigquit = getattr(signal, "SIGQUIT", None)
575 if sigquit is not None:
576 os.kill(0, signal.SIGQUIT)
577 else:
578 sys.exit("Quit")
579
580
581 def indent_buffer(event):
582 """Indent buffer"""
583 event.current_buffer.insert_text(" " * 4)
584
585
586 @undoc
587 def newline_with_copy_margin(event):
588 """
589 DEPRECATED since IPython 6.0
590
591 See :any:`newline_autoindent_outer` for a replacement.
592
593 Preserve margin and cursor position when using
594 Control-O to insert a newline in EMACS mode
595 """
596 warnings.warn(
597 "`newline_with_copy_margin(event)` is deprecated since IPython 6.0. "
598 "see `newline_autoindent_outer(shell)(event)` for a replacement.",
599 DeprecationWarning,
600 stacklevel=2,
601 )
602
603 b = event.current_buffer
604 cursor_start_pos = b.document.cursor_position_col
605 b.newline(copy_margin=True)
606 b.cursor_up(count=1)
607 cursor_end_pos = b.document.cursor_position_col
608 if cursor_start_pos != cursor_end_pos:
609 pos_diff = cursor_start_pos - cursor_end_pos
610 b.cursor_right(count=pos_diff)
611
612
613 def newline_autoindent_outer(inputsplitter) -> Callable[..., None]:
614 """
615 Return a function suitable for inserting a indented newline after the cursor.
616
617 Fancier version of deprecated ``newline_with_copy_margin`` which should
618 compute the correct indentation of the inserted line. That is to say, indent
619 by 4 extra space after a function definition, class definition, context
620 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
621 """
622
623 def newline_autoindent(event):
624 """Insert a newline after the cursor indented appropriately."""
625 b = event.current_buffer
626 d = b.document
627
628 if b.complete_state:
629 b.cancel_completion()
630 text = d.text[: d.cursor_position] + "\n"
631 _, indent = inputsplitter.check_complete(text)
632 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
633
634 newline_autoindent.__qualname__ = "newline_autoindent"
635
636 return newline_autoindent
637
638
639 def open_input_in_editor(event):
640 """Open code from input in external editor"""
641 event.app.current_buffer.open_in_editor()
642
643
644 if sys.platform == "win32":
645 from IPython.core.error import TryNext
646 from IPython.lib.clipboard import (
647 ClipboardEmpty,
648 tkinter_clipboard_get,
649 win32_clipboard_get,
650 )
651
652 @undoc
653 def win_paste(event):
654 try:
655 text = win32_clipboard_get()
656 except TryNext:
657 try:
658 text = tkinter_clipboard_get()
659 except (TryNext, ClipboardEmpty):
660 return
661 except ClipboardEmpty:
662 return
663 event.current_buffer.insert_text(text.replace("\t", " " * 4))
664
665 else:
666
667 @undoc
668 def win_paste(event):
669 """Stub used when auto-generating shortcuts for documentation"""
670 pass
@@ -0,0 +1,104
1 """
2 Utilities function for keybinding with prompt toolkit.
3
4 This will be bound to specific key press and filter modes,
5 like whether we are in edit mode, and whether the completer is open.
6 """
7 import re
8 from prompt_toolkit.key_binding import KeyPressEvent
9
10
11 def parenthesis(event: KeyPressEvent):
12 """Auto-close parenthesis"""
13 event.current_buffer.insert_text("()")
14 event.current_buffer.cursor_left()
15
16
17 def brackets(event: KeyPressEvent):
18 """Auto-close brackets"""
19 event.current_buffer.insert_text("[]")
20 event.current_buffer.cursor_left()
21
22
23 def braces(event: KeyPressEvent):
24 """Auto-close braces"""
25 event.current_buffer.insert_text("{}")
26 event.current_buffer.cursor_left()
27
28
29 def double_quote(event: KeyPressEvent):
30 """Auto-close double quotes"""
31 event.current_buffer.insert_text('""')
32 event.current_buffer.cursor_left()
33
34
35 def single_quote(event: KeyPressEvent):
36 """Auto-close single quotes"""
37 event.current_buffer.insert_text("''")
38 event.current_buffer.cursor_left()
39
40
41 def docstring_double_quotes(event: KeyPressEvent):
42 """Auto-close docstring (double quotes)"""
43 event.current_buffer.insert_text('""""')
44 event.current_buffer.cursor_left(3)
45
46
47 def docstring_single_quotes(event: KeyPressEvent):
48 """Auto-close docstring (single quotes)"""
49 event.current_buffer.insert_text("''''")
50 event.current_buffer.cursor_left(3)
51
52
53 def raw_string_parenthesis(event: KeyPressEvent):
54 """Auto-close parenthesis in raw strings"""
55 matches = re.match(
56 r".*(r|R)[\"'](-*)",
57 event.current_buffer.document.current_line_before_cursor,
58 )
59 dashes = matches.group(2) if matches else ""
60 event.current_buffer.insert_text("()" + dashes)
61 event.current_buffer.cursor_left(len(dashes) + 1)
62
63
64 def raw_string_bracket(event: KeyPressEvent):
65 """Auto-close bracker in raw strings"""
66 matches = re.match(
67 r".*(r|R)[\"'](-*)",
68 event.current_buffer.document.current_line_before_cursor,
69 )
70 dashes = matches.group(2) if matches else ""
71 event.current_buffer.insert_text("[]" + dashes)
72 event.current_buffer.cursor_left(len(dashes) + 1)
73
74
75 def raw_string_braces(event: KeyPressEvent):
76 """Auto-close braces in raw strings"""
77 matches = re.match(
78 r".*(r|R)[\"'](-*)",
79 event.current_buffer.document.current_line_before_cursor,
80 )
81 dashes = matches.group(2) if matches else ""
82 event.current_buffer.insert_text("{}" + dashes)
83 event.current_buffer.cursor_left(len(dashes) + 1)
84
85
86 def skip_over(event: KeyPressEvent):
87 """Skip over automatically added parenthesis.
88
89 (rather than adding another parenthesis)"""
90 event.current_buffer.cursor_right()
91
92
93 def delete_pair(event: KeyPressEvent):
94 """Delete auto-closed parenthesis"""
95 event.current_buffer.delete()
96 event.current_buffer.delete_before_cursor()
97
98
99 auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces}
100 auto_match_parens_raw_string = {
101 "(": raw_string_parenthesis,
102 "[": raw_string_bracket,
103 "{": raw_string_braces,
104 }
@@ -0,0 +1,378
1 import re
2 import tokenize
3 from io import StringIO
4 from typing import Callable, List, Optional, Union, Generator, Tuple, Sequence
5
6 from prompt_toolkit.buffer import Buffer
7 from prompt_toolkit.key_binding import KeyPressEvent
8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 from prompt_toolkit.document import Document
11 from prompt_toolkit.history import History
12 from prompt_toolkit.shortcuts import PromptSession
13 from prompt_toolkit.layout.processors import (
14 Processor,
15 Transformation,
16 TransformationInput,
17 )
18
19 from IPython.utils.tokenutil import generate_tokens
20
21
22 def _get_query(document: Document):
23 return document.lines[document.cursor_position_row]
24
25
26 class AppendAutoSuggestionInAnyLine(Processor):
27 """
28 Append the auto suggestion to lines other than the last (appending to the
29 last line is natively supported by the prompt toolkit).
30 """
31
32 def __init__(self, style: str = "class:auto-suggestion") -> None:
33 self.style = style
34
35 def apply_transformation(self, ti: TransformationInput) -> Transformation:
36 is_last_line = ti.lineno == ti.document.line_count - 1
37 is_active_line = ti.lineno == ti.document.cursor_position_row
38
39 if not is_last_line and is_active_line:
40 buffer = ti.buffer_control.buffer
41
42 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
43 suggestion = buffer.suggestion.text
44 else:
45 suggestion = ""
46
47 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
48 else:
49 return Transformation(fragments=ti.fragments)
50
51
52 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
53 """
54 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
55 suggestion from history. To do so it remembers the current position, but it
56 state need to carefully be cleared on the right events.
57 """
58
59 def __init__(
60 self,
61 ):
62 self.skip_lines = 0
63 self._connected_apps = []
64
65 def reset_history_position(self, _: Buffer):
66 self.skip_lines = 0
67
68 def disconnect(self):
69 for pt_app in self._connected_apps:
70 text_insert_event = pt_app.default_buffer.on_text_insert
71 text_insert_event.remove_handler(self.reset_history_position)
72
73 def connect(self, pt_app: PromptSession):
74 self._connected_apps.append(pt_app)
75 # note: `on_text_changed` could be used for a bit different behaviour
76 # on character deletion (i.e. reseting history position on backspace)
77 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
78 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
79
80 def get_suggestion(
81 self, buffer: Buffer, document: Document
82 ) -> Optional[Suggestion]:
83 text = _get_query(document)
84
85 if text.strip():
86 for suggestion, _ in self._find_next_match(
87 text, self.skip_lines, buffer.history
88 ):
89 return Suggestion(suggestion)
90
91 return None
92
93 def _dismiss(self, buffer, *args, **kwargs):
94 buffer.suggestion = None
95
96 def _find_match(
97 self, text: str, skip_lines: float, history: History, previous: bool
98 ) -> Generator[Tuple[str, float], None, None]:
99 """
100 text : str
101 Text content to find a match for, the user cursor is most of the
102 time at the end of this text.
103 skip_lines : float
104 number of items to skip in the search, this is used to indicate how
105 far in the list the user has navigated by pressing up or down.
106 The float type is used as the base value is +inf
107 history : History
108 prompt_toolkit History instance to fetch previous entries from.
109 previous : bool
110 Direction of the search, whether we are looking previous match
111 (True), or next match (False).
112
113 Yields
114 ------
115 Tuple with:
116 str:
117 current suggestion.
118 float:
119 will actually yield only ints, which is passed back via skip_lines,
120 which may be a +inf (float)
121
122
123 """
124 line_number = -1
125 for string in reversed(list(history.get_strings())):
126 for line in reversed(string.splitlines()):
127 line_number += 1
128 if not previous and line_number < skip_lines:
129 continue
130 # do not return empty suggestions as these
131 # close the auto-suggestion overlay (and are useless)
132 if line.startswith(text) and len(line) > len(text):
133 yield line[len(text) :], line_number
134 if previous and line_number >= skip_lines:
135 return
136
137 def _find_next_match(
138 self, text: str, skip_lines: float, history: History
139 ) -> Generator[Tuple[str, float], None, None]:
140 return self._find_match(text, skip_lines, history, previous=False)
141
142 def _find_previous_match(self, text: str, skip_lines: float, history: History):
143 return reversed(
144 list(self._find_match(text, skip_lines, history, previous=True))
145 )
146
147 def up(self, query: str, other_than: str, history: History) -> None:
148 for suggestion, line_number in self._find_next_match(
149 query, self.skip_lines, history
150 ):
151 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
152 # we want to switch from 'very.b' to 'very.a' because a) if the
153 # suggestion equals current text, prompt-toolkit aborts suggesting
154 # b) user likely would not be interested in 'very' anyways (they
155 # already typed it).
156 if query + suggestion != other_than:
157 self.skip_lines = line_number
158 break
159 else:
160 # no matches found, cycle back to beginning
161 self.skip_lines = 0
162
163 def down(self, query: str, other_than: str, history: History) -> None:
164 for suggestion, line_number in self._find_previous_match(
165 query, self.skip_lines, history
166 ):
167 if query + suggestion != other_than:
168 self.skip_lines = line_number
169 break
170 else:
171 # no matches found, cycle to end
172 for suggestion, line_number in self._find_previous_match(
173 query, float("Inf"), history
174 ):
175 if query + suggestion != other_than:
176 self.skip_lines = line_number
177 break
178
179
180 # Needed for to accept autosuggestions in vi insert mode
181 def accept_in_vi_insert_mode(event: KeyPressEvent):
182 """Apply autosuggestion if at end of line."""
183 buffer = event.current_buffer
184 d = buffer.document
185 after_cursor = d.text[d.cursor_position :]
186 lines = after_cursor.split("\n")
187 end_of_current_line = lines[0].strip()
188 suggestion = buffer.suggestion
189 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
190 buffer.insert_text(suggestion.text)
191 else:
192 nc.end_of_line(event)
193
194
195 def accept(event: KeyPressEvent):
196 """Accept autosuggestion"""
197 buffer = event.current_buffer
198 suggestion = buffer.suggestion
199 if suggestion:
200 buffer.insert_text(suggestion.text)
201 else:
202 nc.forward_char(event)
203
204
205 def discard(event: KeyPressEvent):
206 """Discard autosuggestion"""
207 buffer = event.current_buffer
208 buffer.suggestion = None
209
210
211 def accept_word(event: KeyPressEvent):
212 """Fill partial autosuggestion by word"""
213 buffer = event.current_buffer
214 suggestion = buffer.suggestion
215 if suggestion:
216 t = re.split(r"(\S+\s+)", suggestion.text)
217 buffer.insert_text(next((x for x in t if x), ""))
218 else:
219 nc.forward_word(event)
220
221
222 def accept_character(event: KeyPressEvent):
223 """Fill partial autosuggestion by character"""
224 b = event.current_buffer
225 suggestion = b.suggestion
226 if suggestion and suggestion.text:
227 b.insert_text(suggestion.text[0])
228
229
230 def accept_and_keep_cursor(event: KeyPressEvent):
231 """Accept autosuggestion and keep cursor in place"""
232 buffer = event.current_buffer
233 old_position = buffer.cursor_position
234 suggestion = buffer.suggestion
235 if suggestion:
236 buffer.insert_text(suggestion.text)
237 buffer.cursor_position = old_position
238
239
240 def accept_and_move_cursor_left(event: KeyPressEvent):
241 """Accept autosuggestion and move cursor left in place"""
242 accept_and_keep_cursor(event)
243 nc.backward_char(event)
244
245
246 def _update_hint(buffer: Buffer):
247 if buffer.auto_suggest:
248 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
249 buffer.suggestion = suggestion
250
251
252 def backspace_and_resume_hint(event: KeyPressEvent):
253 """Resume autosuggestions after deleting last character"""
254 current_buffer = event.current_buffer
255
256 def resume_hinting(buffer: Buffer):
257 _update_hint(buffer)
258 current_buffer.on_text_changed.remove_handler(resume_hinting)
259
260 current_buffer.on_text_changed.add_handler(resume_hinting)
261 nc.backward_delete_char(event)
262
263
264 def up_and_update_hint(event: KeyPressEvent):
265 """Go up and update hint"""
266 current_buffer = event.current_buffer
267
268 current_buffer.auto_up(count=event.arg)
269 _update_hint(current_buffer)
270
271
272 def down_and_update_hint(event: KeyPressEvent):
273 """Go down and update hint"""
274 current_buffer = event.current_buffer
275
276 current_buffer.auto_down(count=event.arg)
277 _update_hint(current_buffer)
278
279
280 def accept_token(event: KeyPressEvent):
281 """Fill partial autosuggestion by token"""
282 b = event.current_buffer
283 suggestion = b.suggestion
284
285 if suggestion:
286 prefix = _get_query(b.document)
287 text = prefix + suggestion.text
288
289 tokens: List[Optional[str]] = [None, None, None]
290 substrings = [""]
291 i = 0
292
293 for token in generate_tokens(StringIO(text).readline):
294 if token.type == tokenize.NEWLINE:
295 index = len(text)
296 else:
297 index = text.index(token[1], len(substrings[-1]))
298 substrings.append(text[:index])
299 tokenized_so_far = substrings[-1]
300 if tokenized_so_far.startswith(prefix):
301 if i == 0 and len(tokenized_so_far) > len(prefix):
302 tokens[0] = tokenized_so_far[len(prefix) :]
303 substrings.append(tokenized_so_far)
304 i += 1
305 tokens[i] = token[1]
306 if i == 2:
307 break
308 i += 1
309
310 if tokens[0]:
311 to_insert: str
312 insert_text = substrings[-2]
313 if tokens[1] and len(tokens[1]) == 1:
314 insert_text = substrings[-1]
315 to_insert = insert_text[len(prefix) :]
316 b.insert_text(to_insert)
317 return
318
319 nc.forward_word(event)
320
321
322 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
323
324
325 def _swap_autosuggestion(
326 buffer: Buffer,
327 provider: NavigableAutoSuggestFromHistory,
328 direction_method: Callable,
329 ):
330 """
331 We skip most recent history entry (in either direction) if it equals the
332 current autosuggestion because if user cycles when auto-suggestion is shown
333 they most likely want something else than what was suggested (otherwise
334 they would have accepted the suggestion).
335 """
336 suggestion = buffer.suggestion
337 if not suggestion:
338 return
339
340 query = _get_query(buffer.document)
341 current = query + suggestion.text
342
343 direction_method(query=query, other_than=current, history=buffer.history)
344
345 new_suggestion = provider.get_suggestion(buffer, buffer.document)
346 buffer.suggestion = new_suggestion
347
348
349 def swap_autosuggestion_up(provider: Provider):
350 def swap_autosuggestion_up(event: KeyPressEvent):
351 """Get next autosuggestion from history."""
352 if not isinstance(provider, NavigableAutoSuggestFromHistory):
353 return
354
355 return _swap_autosuggestion(
356 buffer=event.current_buffer, provider=provider, direction_method=provider.up
357 )
358
359 swap_autosuggestion_up.__name__ = "swap_autosuggestion_up"
360 return swap_autosuggestion_up
361
362
363 def swap_autosuggestion_down(
364 provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
365 ):
366 def swap_autosuggestion_down(event: KeyPressEvent):
367 """Get previous autosuggestion from history."""
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 return
370
371 return _swap_autosuggestion(
372 buffer=event.current_buffer,
373 provider=provider,
374 direction_method=provider.down,
375 )
376
377 swap_autosuggestion_down.__name__ = "swap_autosuggestion_down"
378 return swap_autosuggestion_down
@@ -0,0 +1,318
1 import pytest
2 from IPython.terminal.shortcuts.auto_suggest import (
3 accept,
4 accept_in_vi_insert_mode,
5 accept_token,
6 accept_character,
7 accept_word,
8 accept_and_keep_cursor,
9 discard,
10 NavigableAutoSuggestFromHistory,
11 swap_autosuggestion_up,
12 swap_autosuggestion_down,
13 )
14
15 from prompt_toolkit.history import InMemoryHistory
16 from prompt_toolkit.buffer import Buffer
17 from prompt_toolkit.document import Document
18 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
19
20 from unittest.mock import patch, Mock
21
22
23 def make_event(text, cursor, suggestion):
24 event = Mock()
25 event.current_buffer = Mock()
26 event.current_buffer.suggestion = Mock()
27 event.current_buffer.text = text
28 event.current_buffer.cursor_position = cursor
29 event.current_buffer.suggestion.text = suggestion
30 event.current_buffer.document = Document(text=text, cursor_position=cursor)
31 return event
32
33
34 @pytest.mark.parametrize(
35 "text, suggestion, expected",
36 [
37 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"),
38 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"),
39 ],
40 )
41 def test_accept(text, suggestion, expected):
42 event = make_event(text, len(text), suggestion)
43 buffer = event.current_buffer
44 buffer.insert_text = Mock()
45 accept(event)
46 assert buffer.insert_text.called
47 assert buffer.insert_text.call_args[0] == (expected,)
48
49
50 @pytest.mark.parametrize(
51 "text, suggestion",
52 [
53 ("", "def out(tag: str, n=50):"),
54 ("def ", "out(tag: str, n=50):"),
55 ],
56 )
57 def test_discard(text, suggestion):
58 event = make_event(text, len(text), suggestion)
59 buffer = event.current_buffer
60 buffer.insert_text = Mock()
61 discard(event)
62 assert not buffer.insert_text.called
63 assert buffer.suggestion is None
64
65
66 @pytest.mark.parametrize(
67 "text, cursor, suggestion, called",
68 [
69 ("123456", 6, "123456789", True),
70 ("123456", 3, "123456789", False),
71 ("123456 \n789", 6, "123456789", True),
72 ],
73 )
74 def test_autosuggest_at_EOL(text, cursor, suggestion, called):
75 """
76 test that autosuggest is only applied at end of line.
77 """
78
79 event = make_event(text, cursor, suggestion)
80 event.current_buffer.insert_text = Mock()
81 accept_in_vi_insert_mode(event)
82 if called:
83 event.current_buffer.insert_text.assert_called()
84 else:
85 event.current_buffer.insert_text.assert_not_called()
86 # event.current_buffer.document.get_end_of_line_position.assert_called()
87
88
89 @pytest.mark.parametrize(
90 "text, suggestion, expected",
91 [
92 ("", "def out(tag: str, n=50):", "def "),
93 ("d", "ef out(tag: str, n=50):", "ef "),
94 ("de ", "f out(tag: str, n=50):", "f "),
95 ("def", " out(tag: str, n=50):", " "),
96 ("def ", "out(tag: str, n=50):", "out("),
97 ("def o", "ut(tag: str, n=50):", "ut("),
98 ("def ou", "t(tag: str, n=50):", "t("),
99 ("def out", "(tag: str, n=50):", "("),
100 ("def out(", "tag: str, n=50):", "tag: "),
101 ("def out(t", "ag: str, n=50):", "ag: "),
102 ("def out(ta", "g: str, n=50):", "g: "),
103 ("def out(tag", ": str, n=50):", ": "),
104 ("def out(tag:", " str, n=50):", " "),
105 ("def out(tag: ", "str, n=50):", "str, "),
106 ("def out(tag: s", "tr, n=50):", "tr, "),
107 ("def out(tag: st", "r, n=50):", "r, "),
108 ("def out(tag: str", ", n=50):", ", n"),
109 ("def out(tag: str,", " n=50):", " n"),
110 ("def out(tag: str, ", "n=50):", "n="),
111 ("def out(tag: str, n", "=50):", "="),
112 ("def out(tag: str, n=", "50):", "50)"),
113 ("def out(tag: str, n=5", "0):", "0)"),
114 ("def out(tag: str, n=50", "):", "):"),
115 ("def out(tag: str, n=50)", ":", ":"),
116 ],
117 )
118 def test_autosuggest_token(text, suggestion, expected):
119 event = make_event(text, len(text), suggestion)
120 event.current_buffer.insert_text = Mock()
121 accept_token(event)
122 assert event.current_buffer.insert_text.called
123 assert event.current_buffer.insert_text.call_args[0] == (expected,)
124
125
126 @pytest.mark.parametrize(
127 "text, suggestion, expected",
128 [
129 ("", "def out(tag: str, n=50):", "d"),
130 ("d", "ef out(tag: str, n=50):", "e"),
131 ("de ", "f out(tag: str, n=50):", "f"),
132 ("def", " out(tag: str, n=50):", " "),
133 ],
134 )
135 def test_accept_character(text, suggestion, expected):
136 event = make_event(text, len(text), suggestion)
137 event.current_buffer.insert_text = Mock()
138 accept_character(event)
139 assert event.current_buffer.insert_text.called
140 assert event.current_buffer.insert_text.call_args[0] == (expected,)
141
142
143 @pytest.mark.parametrize(
144 "text, suggestion, expected",
145 [
146 ("", "def out(tag: str, n=50):", "def "),
147 ("d", "ef out(tag: str, n=50):", "ef "),
148 ("de", "f out(tag: str, n=50):", "f "),
149 ("def", " out(tag: str, n=50):", " "),
150 # (this is why we also have accept_token)
151 ("def ", "out(tag: str, n=50):", "out(tag: "),
152 ],
153 )
154 def test_accept_word(text, suggestion, expected):
155 event = make_event(text, len(text), suggestion)
156 event.current_buffer.insert_text = Mock()
157 accept_word(event)
158 assert event.current_buffer.insert_text.called
159 assert event.current_buffer.insert_text.call_args[0] == (expected,)
160
161
162 @pytest.mark.parametrize(
163 "text, suggestion, expected, cursor",
164 [
165 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0),
166 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4),
167 ],
168 )
169 def test_accept_and_keep_cursor(text, suggestion, expected, cursor):
170 event = make_event(text, cursor, suggestion)
171 buffer = event.current_buffer
172 buffer.insert_text = Mock()
173 accept_and_keep_cursor(event)
174 assert buffer.insert_text.called
175 assert buffer.insert_text.call_args[0] == (expected,)
176 assert buffer.cursor_position == cursor
177
178
179 def test_autosuggest_token_empty():
180 full = "def out(tag: str, n=50):"
181 event = make_event(full, len(full), "")
182 event.current_buffer.insert_text = Mock()
183
184 with patch(
185 "prompt_toolkit.key_binding.bindings.named_commands.forward_word"
186 ) as forward_word:
187 accept_token(event)
188 assert not event.current_buffer.insert_text.called
189 assert forward_word.called
190
191
192 def test_other_providers():
193 """Ensure that swapping autosuggestions does not break with other providers"""
194 provider = AutoSuggestFromHistory()
195 up = swap_autosuggestion_up(provider)
196 down = swap_autosuggestion_down(provider)
197 event = Mock()
198 event.current_buffer = Buffer()
199 assert up(event) is None
200 assert down(event) is None
201
202
203 async def test_navigable_provider():
204 provider = NavigableAutoSuggestFromHistory()
205 history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
206 buffer = Buffer(history=history)
207
208 async for _ in history.load():
209 pass
210
211 buffer.cursor_position = 5
212 buffer.text = "very"
213
214 up = swap_autosuggestion_up(provider)
215 down = swap_autosuggestion_down(provider)
216
217 event = Mock()
218 event.current_buffer = buffer
219
220 def get_suggestion():
221 suggestion = provider.get_suggestion(buffer, buffer.document)
222 buffer.suggestion = suggestion
223 return suggestion
224
225 assert get_suggestion().text == "_c"
226
227 # should go up
228 up(event)
229 assert get_suggestion().text == "_b"
230
231 # should skip over 'very' which is identical to buffer content
232 up(event)
233 assert get_suggestion().text == "_a"
234
235 # should cycle back to beginning
236 up(event)
237 assert get_suggestion().text == "_c"
238
239 # should cycle back through end boundary
240 down(event)
241 assert get_suggestion().text == "_a"
242
243 down(event)
244 assert get_suggestion().text == "_b"
245
246 down(event)
247 assert get_suggestion().text == "_c"
248
249 down(event)
250 assert get_suggestion().text == "_a"
251
252
253 async def test_navigable_provider_multiline_entries():
254 provider = NavigableAutoSuggestFromHistory()
255 history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
256 buffer = Buffer(history=history)
257
258 async for _ in history.load():
259 pass
260
261 buffer.cursor_position = 5
262 buffer.text = "very"
263 up = swap_autosuggestion_up(provider)
264 down = swap_autosuggestion_down(provider)
265
266 event = Mock()
267 event.current_buffer = buffer
268
269 def get_suggestion():
270 suggestion = provider.get_suggestion(buffer, buffer.document)
271 buffer.suggestion = suggestion
272 return suggestion
273
274 assert get_suggestion().text == "_c"
275
276 up(event)
277 assert get_suggestion().text == "_b"
278
279 up(event)
280 assert get_suggestion().text == "_a"
281
282 down(event)
283 assert get_suggestion().text == "_b"
284
285 down(event)
286 assert get_suggestion().text == "_c"
287
288
289 def create_session_mock():
290 session = Mock()
291 session.default_buffer = Buffer()
292 return session
293
294
295 def test_navigable_provider_connection():
296 provider = NavigableAutoSuggestFromHistory()
297 provider.skip_lines = 1
298
299 session_1 = create_session_mock()
300 provider.connect(session_1)
301
302 assert provider.skip_lines == 1
303 session_1.default_buffer.on_text_insert.fire()
304 assert provider.skip_lines == 0
305
306 session_2 = create_session_mock()
307 provider.connect(session_2)
308 provider.skip_lines = 2
309
310 assert provider.skip_lines == 2
311 session_2.default_buffer.on_text_insert.fire()
312 assert provider.skip_lines == 0
313
314 provider.skip_lines = 3
315 provider.disconnect()
316 session_1.default_buffer.on_text_insert.fire()
317 session_2.default_buffer.on_text_insert.fire()
318 assert provider.skip_lines == 3
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,7
1 /*
2 Needed to revert problematic lack of wrapping in sphinx_rtd_theme, see:
3 https://github.com/readthedocs/sphinx_rtd_theme/issues/117
4 */
5 .wy-table-responsive table.shortcuts td, .wy-table-responsive table.shortcuts th {
6 white-space: normal!important;
7 }
@@ -29,11 +29,13 jobs:
29 pip install mypy pyflakes flake8
29 pip install mypy pyflakes flake8
30 - name: Lint with mypy
30 - name: Lint with mypy
31 run: |
31 run: |
32 set -e
32 mypy -p IPython.terminal
33 mypy -p IPython.terminal
33 mypy -p IPython.core.magics
34 mypy -p IPython.core.magics
34 mypy -p IPython.core.guarded_eval
35 mypy -p IPython.core.guarded_eval
35 mypy -p IPython.core.completer
36 mypy -p IPython.core.completer
36 - name: Lint with pyflakes
37 - name: Lint with pyflakes
37 run: |
38 run: |
39 set -e
38 flake8 IPython/core/magics/script.py
40 flake8 IPython/core/magics/script.py
39 flake8 IPython/core/magics/packaging.py
41 flake8 IPython/core/magics/packaging.py
@@ -123,9 +123,8 class ProfileAwareConfigLoader(PyFileConfigLoader):
123 return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path)
123 return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path)
124
124
125 class BaseIPythonApplication(Application):
125 class BaseIPythonApplication(Application):
126
126 name = "ipython"
127 name = u'ipython'
127 description = "IPython: an enhanced interactive Python shell."
128 description = Unicode(u'IPython: an enhanced interactive Python shell.')
129 version = Unicode(release.version)
128 version = Unicode(release.version)
130
129
131 aliases = base_aliases
130 aliases = base_aliases
@@ -311,7 +310,7 class BaseIPythonApplication(Application):
311 except OSError as e:
310 except OSError as e:
312 # this will not be EEXIST
311 # this will not be EEXIST
313 self.log.error("couldn't create path %s: %s", path, e)
312 self.log.error("couldn't create path %s: %s", path, e)
314 self.log.debug("IPYTHONDIR set to: %s" % new)
313 self.log.debug("IPYTHONDIR set to: %s", new)
315
314
316 def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS):
315 def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS):
317 """Load the config file.
316 """Load the config file.
@@ -401,7 +400,7 class BaseIPythonApplication(Application):
401 self.log.fatal("Profile %r not found."%self.profile)
400 self.log.fatal("Profile %r not found."%self.profile)
402 self.exit(1)
401 self.exit(1)
403 else:
402 else:
404 self.log.debug(f"Using existing profile dir: {p.location!r}")
403 self.log.debug("Using existing profile dir: %r", p.location)
405 else:
404 else:
406 location = self.config.ProfileDir.location
405 location = self.config.ProfileDir.location
407 # location is fully specified
406 # location is fully specified
@@ -421,7 +420,7 class BaseIPythonApplication(Application):
421 self.log.fatal("Profile directory %r not found."%location)
420 self.log.fatal("Profile directory %r not found."%location)
422 self.exit(1)
421 self.exit(1)
423 else:
422 else:
424 self.log.debug(f"Using existing profile dir: {p.location!r}")
423 self.log.debug("Using existing profile dir: %r", p.location)
425 # if profile_dir is specified explicitly, set profile name
424 # if profile_dir is specified explicitly, set profile name
426 dir_name = os.path.basename(p.location)
425 dir_name = os.path.basename(p.location)
427 if dir_name.startswith('profile_'):
426 if dir_name.startswith('profile_'):
@@ -468,7 +467,7 class BaseIPythonApplication(Application):
468 s = self.generate_config_file()
467 s = self.generate_config_file()
469 config_file = Path(self.profile_dir.location) / self.config_file_name
468 config_file = Path(self.profile_dir.location) / self.config_file_name
470 if self.overwrite or not config_file.exists():
469 if self.overwrite or not config_file.exists():
471 self.log.warning("Generating default config file: %r" % (config_file))
470 self.log.warning("Generating default config file: %r", (config_file))
472 config_file.write_text(s, encoding="utf-8")
471 config_file.write_text(s, encoding="utf-8")
473
472
474 @catch_config_error
473 @catch_config_error
@@ -91,7 +91,13 class DisplayHook(Configurable):
91 # some uses of ipshellembed may fail here
91 # some uses of ipshellembed may fail here
92 return False
92 return False
93
93
94 sio = _io.StringIO(cell)
94 return self.semicolon_at_end_of_expression(cell)
95
96 @staticmethod
97 def semicolon_at_end_of_expression(expression):
98 """Parse Python expression and detects whether last token is ';'"""
99
100 sio = _io.StringIO(expression)
95 tokens = list(tokenize.generate_tokens(sio.readline))
101 tokens = list(tokenize.generate_tokens(sio.readline))
96
102
97 for token in reversed(tokens):
103 for token in reversed(tokens):
@@ -2367,6 +2367,14 class InteractiveShell(SingletonConfigurable):
2367 kwargs['local_ns'] = self.get_local_scope(stack_depth)
2367 kwargs['local_ns'] = self.get_local_scope(stack_depth)
2368 with self.builtin_trap:
2368 with self.builtin_trap:
2369 result = fn(*args, **kwargs)
2369 result = fn(*args, **kwargs)
2370
2371 # The code below prevents the output from being displayed
2372 # when using magics with decodator @output_can_be_silenced
2373 # when the last Python token in the expression is a ';'.
2374 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
2375 if DisplayHook.semicolon_at_end_of_expression(magic_arg_s):
2376 return None
2377
2370 return result
2378 return result
2371
2379
2372 def get_local_scope(self, stack_depth):
2380 def get_local_scope(self, stack_depth):
@@ -2420,6 +2428,14 class InteractiveShell(SingletonConfigurable):
2420 with self.builtin_trap:
2428 with self.builtin_trap:
2421 args = (magic_arg_s, cell)
2429 args = (magic_arg_s, cell)
2422 result = fn(*args, **kwargs)
2430 result = fn(*args, **kwargs)
2431
2432 # The code below prevents the output from being displayed
2433 # when using magics with decodator @output_can_be_silenced
2434 # when the last Python token in the expression is a ';'.
2435 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
2436 if DisplayHook.semicolon_at_end_of_expression(cell):
2437 return None
2438
2423 return result
2439 return result
2424
2440
2425 def find_line_magic(self, magic_name):
2441 def find_line_magic(self, magic_name):
@@ -3200,6 +3216,7 class InteractiveShell(SingletonConfigurable):
3200 # Execute the user code
3216 # Execute the user code
3201 interactivity = "none" if silent else self.ast_node_interactivity
3217 interactivity = "none" if silent else self.ast_node_interactivity
3202
3218
3219
3203 has_raised = await self.run_ast_nodes(code_ast.body, cell_name,
3220 has_raised = await self.run_ast_nodes(code_ast.body, cell_name,
3204 interactivity=interactivity, compiler=compiler, result=result)
3221 interactivity=interactivity, compiler=compiler, result=result)
3205
3222
@@ -198,7 +198,16 which already exists. But you must first start the logging process with
198 odata = u'\n'.join([u'#[Out]# %s' % s
198 odata = u'\n'.join([u'#[Out]# %s' % s
199 for s in data.splitlines()])
199 for s in data.splitlines()])
200 write(u'%s\n' % odata)
200 write(u'%s\n' % odata)
201 self.logfile.flush()
201 try:
202 self.logfile.flush()
203 except OSError:
204 print("Failed to flush the log file.")
205 print(
206 f"Please check that {self.logfname} exists and have the right permissions."
207 )
208 print(
209 "Also consider turning off the log with `%logstop` to avoid this warning."
210 )
202
211
203 def logstop(self):
212 def logstop(self):
204 """Fully stop logging and close log file.
213 """Fully stop logging and close log file.
@@ -257,7 +257,8 def _function_magic_marker(magic_kind):
257 return magic_deco
257 return magic_deco
258
258
259
259
260 MAGIC_NO_VAR_EXPAND_ATTR = '_ipython_magic_no_var_expand'
260 MAGIC_NO_VAR_EXPAND_ATTR = "_ipython_magic_no_var_expand"
261 MAGIC_OUTPUT_CAN_BE_SILENCED = "_ipython_magic_output_can_be_silenced"
261
262
262
263
263 def no_var_expand(magic_func):
264 def no_var_expand(magic_func):
@@ -276,6 +277,16 def no_var_expand(magic_func):
276 return magic_func
277 return magic_func
277
278
278
279
280 def output_can_be_silenced(magic_func):
281 """Mark a magic function so its output may be silenced.
282
283 The output is silenced if the Python code used as a parameter of
284 the magic ends in a semicolon, not counting a Python comment that can
285 follow it.
286 """
287 setattr(magic_func, MAGIC_OUTPUT_CAN_BE_SILENCED, True)
288 return magic_func
289
279 # Create the actual decorators for public use
290 # Create the actual decorators for public use
280
291
281 # These three are used to decorate methods in class definitions
292 # These three are used to decorate methods in class definitions
@@ -37,6 +37,7 from IPython.core.magic import (
37 magics_class,
37 magics_class,
38 needs_local_scope,
38 needs_local_scope,
39 no_var_expand,
39 no_var_expand,
40 output_can_be_silenced,
40 on_off,
41 on_off,
41 )
42 )
42 from IPython.testing.skipdoctest import skip_doctest
43 from IPython.testing.skipdoctest import skip_doctest
@@ -1194,6 +1195,7 class ExecutionMagics(Magics):
1194 @no_var_expand
1195 @no_var_expand
1195 @needs_local_scope
1196 @needs_local_scope
1196 @line_cell_magic
1197 @line_cell_magic
1198 @output_can_be_silenced
1197 def time(self,line='', cell=None, local_ns=None):
1199 def time(self,line='', cell=None, local_ns=None):
1198 """Time execution of a Python statement or expression.
1200 """Time execution of a Python statement or expression.
1199
1201
@@ -468,9 +468,9 class OSMagics(Magics):
468 string.
468 string.
469
469
470 Usage:\\
470 Usage:\\
471 %set_env var val: set value for var
471 :``%set_env var val``: set value for var
472 %set_env var=val: set value for var
472 :``%set_env var=val``: set value for var
473 %set_env var=$val: set value for var, using python expansion if possible
473 :``%set_env var=$val``: set value for var, using python expansion if possible
474 """
474 """
475 split = '=' if '=' in parameter_s else ' '
475 split = '=' if '=' in parameter_s else ' '
476 bits = parameter_s.split(split, 1)
476 bits = parameter_s.split(split, 1)
@@ -54,7 +54,7 class PylabMagics(Magics):
54 If you are using the inline matplotlib backend in the IPython Notebook
54 If you are using the inline matplotlib backend in the IPython Notebook
55 you can set which figure formats are enabled using the following::
55 you can set which figure formats are enabled using the following::
56
56
57 In [1]: from IPython.display import set_matplotlib_formats
57 In [1]: from matplotlib_inline.backend_inline import set_matplotlib_formats
58
58
59 In [2]: set_matplotlib_formats('pdf', 'svg')
59 In [2]: set_matplotlib_formats('pdf', 'svg')
60
60
@@ -65,9 +65,9 class PylabMagics(Magics):
65
65
66 In [3]: %config InlineBackend.print_figure_kwargs = {'bbox_inches':None}
66 In [3]: %config InlineBackend.print_figure_kwargs = {'bbox_inches':None}
67
67
68 In addition, see the docstring of
68 In addition, see the docstrings of
69 `IPython.display.set_matplotlib_formats` and
69 `matplotlib_inline.backend_inline.set_matplotlib_formats` and
70 `IPython.display.set_matplotlib_close` for more information on
70 `matplotlib_inline.backend_inline.set_matplotlib_close` for more information on
71 changing additional behaviors of the inline backend.
71 changing additional behaviors of the inline backend.
72
72
73 Examples
73 Examples
@@ -210,7 +210,7 class ScriptMagics(Magics):
210
210
211 async def _handle_stream(stream, stream_arg, file_object):
211 async def _handle_stream(stream, stream_arg, file_object):
212 while True:
212 while True:
213 line = (await stream.readline()).decode("utf8")
213 line = (await stream.readline()).decode("utf8", errors="replace")
214 if not line:
214 if not line:
215 break
215 break
216 if stream_arg:
216 if stream_arg:
@@ -16,7 +16,7
16 # release. 'dev' as a _version_extra string means this is a development
16 # release. 'dev' as a _version_extra string means this is a development
17 # version
17 # version
18 _version_major = 8
18 _version_major = 8
19 _version_minor = 9
19 _version_minor = 10
20 _version_patch = 0
20 _version_patch = 0
21 _version_extra = ".dev"
21 _version_extra = ".dev"
22 # _version_extra = "rc1"
22 # _version_extra = "rc1"
@@ -278,7 +278,7 class InteractiveShellApp(Configurable):
278 )
278 )
279 for ext in extensions:
279 for ext in extensions:
280 try:
280 try:
281 self.log.info("Loading IPython extension: %s" % ext)
281 self.log.info("Loading IPython extension: %s", ext)
282 self.shell.extension_manager.load_extension(ext)
282 self.shell.extension_manager.load_extension(ext)
283 except:
283 except:
284 if self.reraise_ipython_extension_failures:
284 if self.reraise_ipython_extension_failures:
@@ -416,6 +416,65 def test_time():
416 with tt.AssertPrints("hihi", suppress=False):
416 with tt.AssertPrints("hihi", suppress=False):
417 ip.run_cell("f('hi')")
417 ip.run_cell("f('hi')")
418
418
419
420 # ';' at the end of %time prevents instruction value to be printed.
421 # This tests fix for #13837.
422 def test_time_no_output_with_semicolon():
423 ip = get_ipython()
424
425 # Test %time cases
426 with tt.AssertPrints(" 123456"):
427 with tt.AssertPrints("Wall time: ", suppress=False):
428 with tt.AssertPrints("CPU times: ", suppress=False):
429 ip.run_cell("%time 123000+456")
430
431 with tt.AssertNotPrints(" 123456"):
432 with tt.AssertPrints("Wall time: ", suppress=False):
433 with tt.AssertPrints("CPU times: ", suppress=False):
434 ip.run_cell("%time 123000+456;")
435
436 with tt.AssertPrints(" 123456"):
437 with tt.AssertPrints("Wall time: ", suppress=False):
438 with tt.AssertPrints("CPU times: ", suppress=False):
439 ip.run_cell("%time 123000+456 # Comment")
440
441 with tt.AssertNotPrints(" 123456"):
442 with tt.AssertPrints("Wall time: ", suppress=False):
443 with tt.AssertPrints("CPU times: ", suppress=False):
444 ip.run_cell("%time 123000+456; # Comment")
445
446 with tt.AssertPrints(" 123456"):
447 with tt.AssertPrints("Wall time: ", suppress=False):
448 with tt.AssertPrints("CPU times: ", suppress=False):
449 ip.run_cell("%time 123000+456 # ;Comment")
450
451 # Test %%time cases
452 with tt.AssertPrints("123456"):
453 with tt.AssertPrints("Wall time: ", suppress=False):
454 with tt.AssertPrints("CPU times: ", suppress=False):
455 ip.run_cell("%%time\n123000+456\n\n\n")
456
457 with tt.AssertNotPrints("123456"):
458 with tt.AssertPrints("Wall time: ", suppress=False):
459 with tt.AssertPrints("CPU times: ", suppress=False):
460 ip.run_cell("%%time\n123000+456;\n\n\n")
461
462 with tt.AssertPrints("123456"):
463 with tt.AssertPrints("Wall time: ", suppress=False):
464 with tt.AssertPrints("CPU times: ", suppress=False):
465 ip.run_cell("%%time\n123000+456 # Comment\n\n\n")
466
467 with tt.AssertNotPrints("123456"):
468 with tt.AssertPrints("Wall time: ", suppress=False):
469 with tt.AssertPrints("CPU times: ", suppress=False):
470 ip.run_cell("%%time\n123000+456; # Comment\n\n\n")
471
472 with tt.AssertPrints("123456"):
473 with tt.AssertPrints("Wall time: ", suppress=False):
474 with tt.AssertPrints("CPU times: ", suppress=False):
475 ip.run_cell("%%time\n123000+456 # ;Comment\n\n\n")
476
477
419 def test_time_last_not_expression():
478 def test_time_last_not_expression():
420 ip.run_cell("%%time\n"
479 ip.run_cell("%%time\n"
421 "var_1 = 1\n"
480 "var_1 = 1\n"
@@ -4,6 +4,7 import asyncio
4 import os
4 import os
5 import sys
5 import sys
6 from warnings import warn
6 from warnings import warn
7 from typing import Union as UnionType
7
8
8 from IPython.core.async_helpers import get_asyncio_loop
9 from IPython.core.async_helpers import get_asyncio_loop
9 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
@@ -49,6 +50,10 from .pt_inputhooks import get_inputhook_name_and_func
49 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
50 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
50 from .ptutils import IPythonPTCompleter, IPythonPTLexer
51 from .ptutils import IPythonPTCompleter, IPythonPTLexer
51 from .shortcuts import create_ipython_shortcuts
52 from .shortcuts import create_ipython_shortcuts
53 from .shortcuts.auto_suggest import (
54 NavigableAutoSuggestFromHistory,
55 AppendAutoSuggestionInAnyLine,
56 )
52
57
53 PTK3 = ptk_version.startswith('3.')
58 PTK3 = ptk_version.startswith('3.')
54
59
@@ -183,7 +188,10 class TerminalInteractiveShell(InteractiveShell):
183 'menus, decrease for short and wide.'
188 'menus, decrease for short and wide.'
184 ).tag(config=True)
189 ).tag(config=True)
185
190
186 pt_app = None
191 pt_app: UnionType[PromptSession, None] = None
192 auto_suggest: UnionType[
193 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
194 ] = None
187 debugger_history = None
195 debugger_history = None
188
196
189 debugger_history_file = Unicode(
197 debugger_history_file = Unicode(
@@ -376,18 +384,27 class TerminalInteractiveShell(InteractiveShell):
376 ).tag(config=True)
384 ).tag(config=True)
377
385
378 autosuggestions_provider = Unicode(
386 autosuggestions_provider = Unicode(
379 "AutoSuggestFromHistory",
387 "NavigableAutoSuggestFromHistory",
380 help="Specifies from which source automatic suggestions are provided. "
388 help="Specifies from which source automatic suggestions are provided. "
381 "Can be set to `'AutoSuggestFromHistory`' or `None` to disable"
389 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
382 "automatic suggestions. Default is `'AutoSuggestFromHistory`'.",
390 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
391 " or ``None`` to disable automatic suggestions. "
392 "Default is `'NavigableAutoSuggestFromHistory`'.",
383 allow_none=True,
393 allow_none=True,
384 ).tag(config=True)
394 ).tag(config=True)
385
395
386 def _set_autosuggestions(self, provider):
396 def _set_autosuggestions(self, provider):
397 # disconnect old handler
398 if self.auto_suggest and isinstance(
399 self.auto_suggest, NavigableAutoSuggestFromHistory
400 ):
401 self.auto_suggest.disconnect()
387 if provider is None:
402 if provider is None:
388 self.auto_suggest = None
403 self.auto_suggest = None
389 elif provider == "AutoSuggestFromHistory":
404 elif provider == "AutoSuggestFromHistory":
390 self.auto_suggest = AutoSuggestFromHistory()
405 self.auto_suggest = AutoSuggestFromHistory()
406 elif provider == "NavigableAutoSuggestFromHistory":
407 self.auto_suggest = NavigableAutoSuggestFromHistory()
391 else:
408 else:
392 raise ValueError("No valid provider.")
409 raise ValueError("No valid provider.")
393 if self.pt_app:
410 if self.pt_app:
@@ -462,6 +479,8 class TerminalInteractiveShell(InteractiveShell):
462 tempfile_suffix=".py",
479 tempfile_suffix=".py",
463 **self._extra_prompt_options()
480 **self._extra_prompt_options()
464 )
481 )
482 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
483 self.auto_suggest.connect(self.pt_app)
465
484
466 def _make_style_from_name_or_cls(self, name_or_cls):
485 def _make_style_from_name_or_cls(self, name_or_cls):
467 """
486 """
@@ -560,23 +579,39 class TerminalInteractiveShell(InteractiveShell):
560 get_message = get_message()
579 get_message = get_message()
561
580
562 options = {
581 options = {
563 'complete_in_thread': False,
582 "complete_in_thread": False,
564 'lexer':IPythonPTLexer(),
583 "lexer": IPythonPTLexer(),
565 'reserve_space_for_menu':self.space_for_menu,
584 "reserve_space_for_menu": self.space_for_menu,
566 'message': get_message,
585 "message": get_message,
567 'prompt_continuation': (
586 "prompt_continuation": (
568 lambda width, lineno, is_soft_wrap:
587 lambda width, lineno, is_soft_wrap: PygmentsTokens(
569 PygmentsTokens(self.prompts.continuation_prompt_tokens(width))),
588 self.prompts.continuation_prompt_tokens(width)
570 'multiline': True,
589 )
571 'complete_style': self.pt_complete_style,
590 ),
572
591 "multiline": True,
592 "complete_style": self.pt_complete_style,
593 "input_processors": [
573 # Highlight matching brackets, but only when this setting is
594 # Highlight matching brackets, but only when this setting is
574 # enabled, and only when the DEFAULT_BUFFER has the focus.
595 # enabled, and only when the DEFAULT_BUFFER has the focus.
575 'input_processors': [ConditionalProcessor(
596 ConditionalProcessor(
576 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
597 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
577 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
598 filter=HasFocus(DEFAULT_BUFFER)
578 Condition(lambda: self.highlight_matching_brackets))],
599 & ~IsDone()
579 }
600 & Condition(lambda: self.highlight_matching_brackets),
601 ),
602 # Show auto-suggestion in lines other than the last line.
603 ConditionalProcessor(
604 processor=AppendAutoSuggestionInAnyLine(),
605 filter=HasFocus(DEFAULT_BUFFER)
606 & ~IsDone()
607 & Condition(
608 lambda: isinstance(
609 self.auto_suggest, NavigableAutoSuggestFromHistory
610 )
611 ),
612 ),
613 ],
614 }
580 if not PTK3:
615 if not PTK3:
581 options['inputhook'] = self.inputhook
616 options['inputhook'] = self.inputhook
582
617
@@ -647,7 +682,7 class TerminalInteractiveShell(InteractiveShell):
647 self.alias_manager.soft_define_alias(cmd, cmd)
682 self.alias_manager.soft_define_alias(cmd, cmd)
648
683
649
684
650 def __init__(self, *args, **kwargs):
685 def __init__(self, *args, **kwargs) -> None:
651 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
686 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
652 self._set_autosuggestions(self.autosuggestions_provider)
687 self._set_autosuggestions(self.autosuggestions_provider)
653 self.init_prompt_toolkit_cli()
688 self.init_prompt_toolkit_cli()
@@ -156,7 +156,7 frontend_flags['i'] = (
156 flags.update(frontend_flags)
156 flags.update(frontend_flags)
157
157
158 aliases = dict(base_aliases)
158 aliases = dict(base_aliases)
159 aliases.update(shell_aliases)
159 aliases.update(shell_aliases) # type: ignore[arg-type]
160
160
161 #-----------------------------------------------------------------------------
161 #-----------------------------------------------------------------------------
162 # Main classes and functions
162 # Main classes and functions
@@ -180,7 +180,7 class LocateIPythonApp(BaseIPythonApplication):
180 class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
180 class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
181 name = u'ipython'
181 name = u'ipython'
182 description = usage.cl_usage
182 description = usage.cl_usage
183 crash_handler_class = IPAppCrashHandler
183 crash_handler_class = IPAppCrashHandler # typing: ignore[assignment]
184 examples = _examples
184 examples = _examples
185
185
186 flags = flags
186 flags = flags
@@ -7,11 +7,25 import sys
7 import unittest
7 import unittest
8 import os
8 import os
9
9
10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
11
10 from IPython.core.inputtransformer import InputTransformer
12 from IPython.core.inputtransformer import InputTransformer
11 from IPython.testing import tools as tt
13 from IPython.testing import tools as tt
12 from IPython.utils.capture import capture_output
14 from IPython.utils.capture import capture_output
13
15
14 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
16 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
17 from IPython.terminal.shortcuts.auto_suggest import NavigableAutoSuggestFromHistory
18
19
20 class TestAutoSuggest(unittest.TestCase):
21 def test_changing_provider(self):
22 ip = get_ipython()
23 ip.autosuggestions_provider = None
24 self.assertEqual(ip.auto_suggest, None)
25 ip.autosuggestions_provider = "AutoSuggestFromHistory"
26 self.assertIsInstance(ip.auto_suggest, AutoSuggestFromHistory)
27 ip.autosuggestions_provider = "NavigableAutoSuggestFromHistory"
28 self.assertIsInstance(ip.auto_suggest, NavigableAutoSuggestFromHistory)
15
29
16
30
17 class TestElide(unittest.TestCase):
31 class TestElide(unittest.TestCase):
@@ -24,10 +38,10 class TestElide(unittest.TestCase):
24 )
38 )
25
39
26 test_string = os.sep.join(["", 10 * "a", 10 * "b", 10 * "c", ""])
40 test_string = os.sep.join(["", 10 * "a", 10 * "b", 10 * "c", ""])
27 expect_stirng = (
41 expect_string = (
28 os.sep + "a" + "\N{HORIZONTAL ELLIPSIS}" + "b" + os.sep + 10 * "c"
42 os.sep + "a" + "\N{HORIZONTAL ELLIPSIS}" + "b" + os.sep + 10 * "c"
29 )
43 )
30 self.assertEqual(_elide(test_string, ""), expect_stirng)
44 self.assertEqual(_elide(test_string, ""), expect_string)
31
45
32 def test_elide_typed_normal(self):
46 def test_elide_typed_normal(self):
33 self.assertEqual(
47 self.assertEqual(
@@ -18,11 +18,6 from warnings import warn
18 from IPython.utils.decorators import undoc
18 from IPython.utils.decorators import undoc
19 from .capture import CapturedIO, capture_output
19 from .capture import CapturedIO, capture_output
20
20
21 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
22 devnull = open(os.devnull, "w", encoding="utf-8")
23 atexit.register(devnull.close)
24
25
26 class Tee(object):
21 class Tee(object):
27 """A class to duplicate an output stream to stdout/err.
22 """A class to duplicate an output stream to stdout/err.
28
23
@@ -1,45 +1,98
1 from dataclasses import dataclass
2 from inspect import getsource
1 from pathlib import Path
3 from pathlib import Path
4 from typing import cast, Callable, List, Union
5 from html import escape as html_escape
6 import re
7
8 from prompt_toolkit.keys import KEY_ALIASES
9 from prompt_toolkit.key_binding import KeyBindingsBase
10 from prompt_toolkit.filters import Filter, Condition
11 from prompt_toolkit.shortcuts import PromptSession
2
12
3 from IPython.terminal.shortcuts import create_ipython_shortcuts
13 from IPython.terminal.shortcuts import create_ipython_shortcuts
4
14
5 def name(c):
6 s = c.__class__.__name__
7 if s == '_Invert':
8 return '(Not: %s)' % name(c.filter)
9 if s in log_filters.keys():
10 return '(%s: %s)' % (log_filters[s], ', '.join(name(x) for x in c.filters))
11 return log_filters[s] if s in log_filters.keys() else s
12
15
16 @dataclass
17 class Shortcut:
18 #: a sequence of keys (each element on the list corresponds to pressing one or more keys)
19 keys_sequence: list[str]
20 filter: str
13
21
14 def sentencize(s):
15 """Extract first sentence
16 """
17 s = s.replace('\n', ' ').strip().split('.')
18 s = s[0] if len(s) else s
19 try:
20 return " ".join(s.split())
21 except AttributeError:
22 return s
23
22
23 @dataclass
24 class Handler:
25 description: str
26 identifier: str
24
27
25 def most_common(lst, n=3):
26 """Most common elements occurring more then `n` times
27 """
28 from collections import Counter
29
28
30 c = Counter(lst)
29 @dataclass
31 return [k for (k, v) in c.items() if k and v > n]
30 class Binding:
31 handler: Handler
32 shortcut: Shortcut
32
33
33
34
34 def multi_filter_str(flt):
35 class _NestedFilter(Filter):
35 """Yield readable conditional filter
36 """Protocol reflecting non-public prompt_toolkit's `_AndList` and `_OrList`."""
36 """
37
37 assert hasattr(flt, 'filters'), 'Conditional filter required'
38 filters: List[Filter]
38 yield name(flt)
39
40
41 class _Invert(Filter):
42 """Protocol reflecting non-public prompt_toolkit's `_Invert`."""
43
44 filter: Filter
45
46
47 conjunctions_labels = {"_AndList": "and", "_OrList": "or"}
39
48
49 ATOMIC_CLASSES = {"Never", "Always", "Condition"}
50
51
52 def format_filter(
53 filter_: Union[Filter, _NestedFilter, Condition, _Invert],
54 is_top_level=True,
55 skip=None,
56 ) -> str:
57 """Create easily readable description of the filter."""
58 s = filter_.__class__.__name__
59 if s == "Condition":
60 func = cast(Condition, filter_).func
61 name = func.__name__
62 if name == "<lambda>":
63 source = getsource(func)
64 return source.split("=")[0].strip()
65 return func.__name__
66 elif s == "_Invert":
67 operand = cast(_Invert, filter_).filter
68 if operand.__class__.__name__ in ATOMIC_CLASSES:
69 return f"not {format_filter(operand, is_top_level=False)}"
70 return f"not ({format_filter(operand, is_top_level=False)})"
71 elif s in conjunctions_labels:
72 filters = cast(_NestedFilter, filter_).filters
73 conjunction = conjunctions_labels[s]
74 glue = f" {conjunction} "
75 result = glue.join(format_filter(x, is_top_level=False) for x in filters)
76 if len(filters) > 1 and not is_top_level:
77 result = f"({result})"
78 return result
79 elif s in ["Never", "Always"]:
80 return s.lower()
81 else:
82 raise ValueError(f"Unknown filter type: {filter_}")
83
84
85 def sentencize(s) -> str:
86 """Extract first sentence"""
87 s = re.split(r"\.\W", s.replace("\n", " ").strip())
88 s = s[0] if len(s) else ""
89 if not s.endswith("."):
90 s += "."
91 try:
92 return " ".join(s.split())
93 except AttributeError:
94 return s
40
95
41 log_filters = {'_AndList': 'And', '_OrList': 'Or'}
42 log_invert = {'_Invert'}
43
96
44 class _DummyTerminal:
97 class _DummyTerminal:
45 """Used as a buffer to get prompt_toolkit bindings
98 """Used as a buffer to get prompt_toolkit bindings
@@ -48,49 +101,121 class _DummyTerminal:
48 input_transformer_manager = None
101 input_transformer_manager = None
49 display_completions = None
102 display_completions = None
50 editing_mode = "emacs"
103 editing_mode = "emacs"
104 auto_suggest = None
51
105
52
106
53 ipy_bindings = create_ipython_shortcuts(_DummyTerminal()).bindings
107 def create_identifier(handler: Callable):
54
108 parts = handler.__module__.split(".")
55 dummy_docs = [] # ignore bindings without proper documentation
109 name = handler.__name__
56
110 package = parts[0]
57 common_docs = most_common([kb.handler.__doc__ for kb in ipy_bindings])
111 if len(parts) > 1:
58 if common_docs:
112 final_module = parts[-1]
59 dummy_docs.extend(common_docs)
113 return f"{package}:{final_module}.{name}"
114 else:
115 return f"{package}:{name}"
116
117
118 def bindings_from_prompt_toolkit(prompt_bindings: KeyBindingsBase) -> List[Binding]:
119 """Collect bindings to a simple format that does not depend on prompt-toolkit internals"""
120 bindings: List[Binding] = []
121
122 for kb in prompt_bindings.bindings:
123 bindings.append(
124 Binding(
125 handler=Handler(
126 description=kb.handler.__doc__ or "",
127 identifier=create_identifier(kb.handler),
128 ),
129 shortcut=Shortcut(
130 keys_sequence=[
131 str(k.value) if hasattr(k, "value") else k for k in kb.keys
132 ],
133 filter=format_filter(kb.filter, skip={"has_focus_filter"}),
134 ),
135 )
136 )
137 return bindings
138
139
140 INDISTINGUISHABLE_KEYS = {**KEY_ALIASES, **{v: k for k, v in KEY_ALIASES.items()}}
141
142
143 def format_prompt_keys(keys: str, add_alternatives=True) -> str:
144 """Format prompt toolkit key with modifier into an RST representation."""
145
146 def to_rst(key):
147 escaped = key.replace("\\", "\\\\")
148 return f":kbd:`{escaped}`"
149
150 keys_to_press: list[str]
151
152 prefixes = {
153 "c-s-": [to_rst("ctrl"), to_rst("shift")],
154 "s-c-": [to_rst("ctrl"), to_rst("shift")],
155 "c-": [to_rst("ctrl")],
156 "s-": [to_rst("shift")],
157 }
158
159 for prefix, modifiers in prefixes.items():
160 if keys.startswith(prefix):
161 remainder = keys[len(prefix) :]
162 keys_to_press = [*modifiers, to_rst(remainder)]
163 break
164 else:
165 keys_to_press = [to_rst(keys)]
60
166
61 dummy_docs = list(set(dummy_docs))
167 result = " + ".join(keys_to_press)
62
168
63 single_filter = {}
169 if keys in INDISTINGUISHABLE_KEYS and add_alternatives:
64 multi_filter = {}
170 alternative = INDISTINGUISHABLE_KEYS[keys]
65 for kb in ipy_bindings:
66 doc = kb.handler.__doc__
67 if not doc or doc in dummy_docs:
68 continue
69
171
70 shortcut = ' '.join([k if isinstance(k, str) else k.name for k in kb.keys])
172 result = (
71 shortcut += shortcut.endswith('\\') and '\\' or ''
173 result
72 if hasattr(kb.filter, 'filters'):
174 + " (or "
73 flt = ' '.join(multi_filter_str(kb.filter))
175 + format_prompt_keys(alternative, add_alternatives=False)
74 multi_filter[(shortcut, flt)] = sentencize(doc)
176 + ")"
75 else:
177 )
76 single_filter[(shortcut, name(kb.filter))] = sentencize(doc)
77
178
179 return result
78
180
79 if __name__ == '__main__':
181 if __name__ == '__main__':
80 here = Path(__file__).parent
182 here = Path(__file__).parent
81 dest = here / "source" / "config" / "shortcuts"
183 dest = here / "source" / "config" / "shortcuts"
82
184
83 def sort_key(item):
185 ipy_bindings = create_ipython_shortcuts(_DummyTerminal(), for_all_platforms=True)
84 k, v = item
186
85 shortcut, flt = k
187 session = PromptSession(key_bindings=ipy_bindings)
86 return (str(shortcut), str(flt))
188 prompt_bindings = session.app.key_bindings
87
189
88 for filters, output_filename in [
190 assert prompt_bindings
89 (single_filter, "single_filtered"),
191 # Ensure that we collected the default shortcuts
90 (multi_filter, "multi_filtered"),
192 assert len(prompt_bindings.bindings) > len(ipy_bindings.bindings)
91 ]:
193
92 with (dest / "{}.csv".format(output_filename)).open(
194 bindings = bindings_from_prompt_toolkit(prompt_bindings)
93 "w", encoding="utf-8"
195
94 ) as csv:
196 def sort_key(binding: Binding):
95 for (shortcut, flt), v in sorted(filters.items(), key=sort_key):
197 return binding.handler.identifier, binding.shortcut.filter
96 csv.write(":kbd:`{}`\t{}\t{}\n".format(shortcut, flt, v))
198
199 filters = []
200 with (dest / "table.tsv").open("w", encoding="utf-8") as csv:
201 for binding in sorted(bindings, key=sort_key):
202 sequence = ", ".join(
203 [format_prompt_keys(keys) for keys in binding.shortcut.keys_sequence]
204 )
205 if binding.shortcut.filter == "always":
206 condition_label = "-"
207 else:
208 # we cannot fit all the columns as the filters got too complex over time
209 condition_label = "β“˜"
210
211 csv.write(
212 "\t".join(
213 [
214 sequence,
215 sentencize(binding.handler.description)
216 + f" :raw-html:`<br>` `{binding.handler.identifier}`",
217 f':raw-html:`<span title="{html_escape(binding.shortcut.filter)}" style="cursor: help">{condition_label}</span>`',
218 ]
219 )
220 + "\n"
221 )
@@ -3,14 +3,14 channels:
3 - conda-forge
3 - conda-forge
4 - defaults
4 - defaults
5 dependencies:
5 dependencies:
6 - python=3.8
6 - python=3.10
7 - setuptools>=18.5
7 - setuptools
8 - sphinx>=4.2
8 - sphinx>=4.2
9 - sphinx_rtd_theme>=1.0
9 - sphinx_rtd_theme
10 - numpy
10 - numpy
11 - nose
12 - testpath
11 - testpath
13 - matplotlib
12 - matplotlib
13 - pip
14 - pip:
14 - pip:
15 - docrepr
15 - docrepr
16 - prompt_toolkit
16 - prompt_toolkit
@@ -183,8 +183,7 today_fmt = '%B %d, %Y'
183
183
184 # Exclude these glob-style patterns when looking for source files. They are
184 # Exclude these glob-style patterns when looking for source files. They are
185 # relative to the source/ directory.
185 # relative to the source/ directory.
186 exclude_patterns = []
186 exclude_patterns = ["**.ipynb_checkpoints"]
187
188
187
189 # If true, '()' will be appended to :func: etc. cross-reference text.
188 # If true, '()' will be appended to :func: etc. cross-reference text.
190 #add_function_parentheses = True
189 #add_function_parentheses = True
@@ -211,7 +210,6 default_role = 'literal'
211 # given in html_static_path.
210 # given in html_static_path.
212 # html_style = 'default.css'
211 # html_style = 'default.css'
213
212
214
215 # The name for this set of Sphinx documents. If None, it defaults to
213 # The name for this set of Sphinx documents. If None, it defaults to
216 # "<project> v<release> documentation".
214 # "<project> v<release> documentation".
217 #html_title = None
215 #html_title = None
@@ -327,6 +325,10 texinfo_documents = [
327 modindex_common_prefix = ['IPython.']
325 modindex_common_prefix = ['IPython.']
328
326
329
327
328 def setup(app):
329 app.add_css_file("theme_overrides.css")
330
331
330 # Cleanup
332 # Cleanup
331 # -------
333 # -------
332 # delete release info to avoid pickling errors from sphinx
334 # delete release info to avoid pickling errors from sphinx
@@ -139,13 +139,26 Accessing user namespace and local scope
139 ========================================
139 ========================================
140
140
141 When creating line magics, you may need to access surrounding scope to get user
141 When creating line magics, you may need to access surrounding scope to get user
142 variables (e.g when called inside functions). IPython provide the
142 variables (e.g when called inside functions). IPython provides the
143 ``@needs_local_scope`` decorator that can be imported from
143 ``@needs_local_scope`` decorator that can be imported from
144 ``IPython.core.magics``. When decorated with ``@needs_local_scope`` a magic will
144 ``IPython.core.magics``. When decorated with ``@needs_local_scope`` a magic will
145 be passed ``local_ns`` as an argument. As a convenience ``@needs_local_scope``
145 be passed ``local_ns`` as an argument. As a convenience ``@needs_local_scope``
146 can also be applied to cell magics even if cell magics cannot appear at local
146 can also be applied to cell magics even if cell magics cannot appear at local
147 scope context.
147 scope context.
148
148
149 Silencing the magic output
150 ==========================
151
152 Sometimes it may be useful to define a magic that can be silenced the same way
153 that non-magic expressions can, i.e., by appending a semicolon at the end of the Python
154 code to be executed. That can be achieved by decorating the magic function with
155 the decorator ``@output_can_be_silenced`` that can be imported from
156 ``IPython.core.magics``. When this decorator is used, IPython will parse the Python
157 code used by the magic and, if the last token is a ``;``, the output created by the
158 magic will not show up on the screen. If you want to see an example of this decorator
159 in action, take a look on the ``time`` magic defined in
160 ``IPython.core.magics.execution.py``.
161
149 Complete Example
162 Complete Example
150 ================
163 ================
151
164
@@ -4,28 +4,23 IPython shortcuts
4
4
5 Available shortcuts in an IPython terminal.
5 Available shortcuts in an IPython terminal.
6
6
7 .. warning::
7 .. note::
8
8
9 This list is automatically generated, and may not hold all available
9 This list is automatically generated. Key bindings defined in ``prompt_toolkit`` may differ
10 shortcuts. In particular, it may depend on the version of ``prompt_toolkit``
10 between installations depending on the ``prompt_toolkit`` version.
11 installed during the generation of this page.
12
11
13
12
14 Single Filtered shortcuts
13 * Comma-separated keys, e.g. :kbd:`Esc`, :kbd:`f`, indicate a sequence which can be activated by pressing the listed keys in succession.
15 =========================
14 * Plus-separated keys, e.g. :kbd:`Esc` + :kbd:`f` indicate a combination which requires pressing all keys simultaneously.
16
15 * Hover over the β“˜ icon in the filter column to see when the shortcut is active.g
17 .. csv-table::
18 :header: Shortcut,Filter,Description
19 :widths: 30, 30, 100
20 :delim: tab
21 :file: single_filtered.csv
22
16
17 .. role:: raw-html(raw)
18 :format: html
23
19
24 Multi Filtered shortcuts
25 ========================
26
20
27 .. csv-table::
21 .. csv-table::
28 :header: Shortcut,Filter,Description
22 :header: Shortcut,Description and identifier,Filter
29 :widths: 30, 30, 100
30 :delim: tab
23 :delim: tab
31 :file: multi_filtered.csv
24 :class: shortcuts
25 :file: table.tsv
26 :widths: 20 75 5
@@ -2,6 +2,54
2 8.x Series
2 8.x Series
3 ============
3 ============
4
4
5 .. _version 8.9.0:
6
7 IPython 8.9.0
8 -------------
9
10 Second release of IPython in 2023, last Friday of the month, we are back on
11 track. This is a small release with a few bug-fixes, and improvements, mostly
12 with respect to terminal shortcuts.
13
14
15 The biggest improvement for 8.9 is a drastic amelioration of the
16 auto-suggestions sponsored by D.E. Shaw and implemented by the more and more
17 active contributor `@krassowski <https://github.com/krassowski>`.
18
19 - ``right`` accepts a single character from suggestion
20 - ``ctrl+right`` accepts a semantic token (macos default shortcuts take
21 precedence and need to be disabled to make this work)
22 - ``backspace`` deletes a character and resumes hinting autosuggestions
23 - ``ctrl-left`` accepts suggestion and moves cursor left one character.
24 - ``backspace`` deletes a character and resumes hinting autosuggestions
25 - ``down`` moves to suggestion to later in history when no lines are present below the cursors.
26 - ``up`` moves to suggestion from earlier in history when no lines are present above the cursor.
27
28 This is best described by the Gif posted by `@krassowski
29 <https://github.com/krassowski>`, and in the PR itself :ghpull:`13888`.
30
31 .. image:: ../_images/autosuggest.gif
32
33 Please report any feedback in order for us to improve the user experience.
34 In particular we are also working on making the shortcuts configurable.
35
36 If you are interested in better terminal shortcuts, I also invite you to
37 participate in issue `13879
38 <https://github.com/ipython/ipython/issues/13879>`__.
39
40
41 As we follow `NEP29
42 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`__, next version of
43 IPython will officially stop supporting numpy 1.20, and will stop supporting
44 Python 3.8 after April release.
45
46 As usual you can find the full list of PRs on GitHub under `the 8.9 milestone
47 <https://github.com/ipython/ipython/milestone/111?closed=1>`__.
48
49
50 Thanks to the `D. E. Shaw group <https://deshaw.com/>`__ for sponsoring
51 work on IPython and related libraries.
52
5 .. _version 8.8.0:
53 .. _version 8.8.0:
6
54
7 IPython 8.8.0
55 IPython 8.8.0
@@ -11,11 +59,11 First release of IPython in 2023 as there was no release at the end of
11 December.
59 December.
12
60
13 This is an unusually big release (relatively speaking) with more than 15 Pull
61 This is an unusually big release (relatively speaking) with more than 15 Pull
14 Requests merge.
62 Requests merged.
15
63
16 Of particular interest are:
64 Of particular interest are:
17
65
18 - :ghpull:`13852` that replace the greedy completer and improve
66 - :ghpull:`13852` that replaces the greedy completer and improves
19 completion, in particular for dictionary keys.
67 completion, in particular for dictionary keys.
20 - :ghpull:`13858` that adds ``py.typed`` to ``setup.cfg`` to make sure it is
68 - :ghpull:`13858` that adds ``py.typed`` to ``setup.cfg`` to make sure it is
21 bundled in wheels.
69 bundled in wheels.
@@ -24,7 +72,7 Of particular interest are:
24 believe this also needs a recent version of Traitlets.
72 believe this also needs a recent version of Traitlets.
25 - :ghpull:`13865` makes the ``inspector`` class of `InteractiveShell`
73 - :ghpull:`13865` makes the ``inspector`` class of `InteractiveShell`
26 configurable.
74 configurable.
27 - :ghpull:`13880` that remove minor-version entrypoints as the minor version
75 - :ghpull:`13880` that removes minor-version entrypoints as the minor version
28 entry points that would be included in the wheel would be the one of the
76 entry points that would be included in the wheel would be the one of the
29 Python version that was used to build the ``whl`` file.
77 Python version that was used to build the ``whl`` file.
30
78
@@ -48,8 +96,8 IPython 8.7.0
48
96
49
97
50 Small release of IPython with a couple of bug fixes and new features for this
98 Small release of IPython with a couple of bug fixes and new features for this
51 month. Next month is end of year, it is unclear if there will be a release close
99 month. Next month is the end of year, it is unclear if there will be a release
52 the new year's eve, or if the next release will be at end of January.
100 close to the new year's eve, or if the next release will be at the end of January.
53
101
54 Here are a few of the relevant fixes,
102 Here are a few of the relevant fixes,
55 as usual you can find the full list of PRs on GitHub under `the 8.7 milestone
103 as usual you can find the full list of PRs on GitHub under `the 8.7 milestone
@@ -73,29 +121,29 IPython 8.6.0
73
121
74 Back to a more regular release schedule (at least I try), as Friday is
122 Back to a more regular release schedule (at least I try), as Friday is
75 already over by more than 24h hours. This is a slightly bigger release with a
123 already over by more than 24h hours. This is a slightly bigger release with a
76 few new features that contain no less then 25 PRs.
124 few new features that contain no less than 25 PRs.
77
125
78 We'll notably found a couple of non negligible changes:
126 We'll notably found a couple of non negligible changes:
79
127
80 The ``install_ext`` and related functions have been removed after being
128 The ``install_ext`` and related functions have been removed after being
81 deprecated for years. You can use pip to install extensions. ``pip`` did not
129 deprecated for years. You can use pip to install extensions. ``pip`` did not
82 exists when ``install_ext`` was introduced. You can still load local extensions
130 exist when ``install_ext`` was introduced. You can still load local extensions
83 without installing them. Just set your ``sys.path`` for example. :ghpull:`13744`
131 without installing them. Just set your ``sys.path`` for example. :ghpull:`13744`
84
132
85 IPython now have extra entry points that that the major *and minor* version of
133 IPython now has extra entry points that use the major *and minor* version of
86 python. For some of you this mean that you can do a quick ``ipython3.10`` to
134 python. For some of you this means that you can do a quick ``ipython3.10`` to
87 launch IPython from the Python 3.10 interpreter, while still using Python 3.11
135 launch IPython from the Python 3.10 interpreter, while still using Python 3.11
88 as your main Python. :ghpull:`13743`
136 as your main Python. :ghpull:`13743`
89
137
90 The completer matcher API have been improved. See :ghpull:`13745`. This should
138 The completer matcher API has been improved. See :ghpull:`13745`. This should
91 improve the type inference and improve dict keys completions in many use case.
139 improve the type inference and improve dict keys completions in many use case.
92 Tanks ``@krassowski`` for all the works, and the D.E. Shaw group for sponsoring
140 Thanks ``@krassowski`` for all the work, and the D.E. Shaw group for sponsoring
93 it.
141 it.
94
142
95 The color of error nodes in tracebacks can now be customized. See
143 The color of error nodes in tracebacks can now be customized. See
96 :ghpull:`13756`. This is a private attribute until someone find the time to
144 :ghpull:`13756`. This is a private attribute until someone finds the time to
97 properly add a configuration option. Note that with Python 3.11 that also show
145 properly add a configuration option. Note that with Python 3.11 that also shows
98 the relevant nodes in traceback, it would be good to leverage this informations
146 the relevant nodes in traceback, it would be good to leverage this information
99 (plus the "did you mean" info added on attribute errors). But that's likely work
147 (plus the "did you mean" info added on attribute errors). But that's likely work
100 I won't have time to do before long, so contributions welcome.
148 I won't have time to do before long, so contributions welcome.
101
149
@@ -108,7 +156,7 This mostly occurs in teaching context when incorrect values get passed around.
108
156
109
157
110 The ``?``, ``??``, and corresponding ``pinfo``, ``pinfo2`` magics can now find
158 The ``?``, ``??``, and corresponding ``pinfo``, ``pinfo2`` magics can now find
111 objects insides arrays. That is to say, the following now works::
159 objects inside arrays. That is to say, the following now works::
112
160
113
161
114 >>> def my_func(*arg, **kwargs):pass
162 >>> def my_func(*arg, **kwargs):pass
@@ -117,7 +165,7 objects insides arrays. That is to say, the following now works::
117
165
118
166
119 If ``container`` define a custom ``getitem``, this __will__ trigger the custom
167 If ``container`` define a custom ``getitem``, this __will__ trigger the custom
120 method. So don't put side effects in your ``getitems``. Thanks the D.E. Shaw
168 method. So don't put side effects in your ``getitems``. Thanks to the D.E. Shaw
121 group for the request and sponsoring the work.
169 group for the request and sponsoring the work.
122
170
123
171
@@ -143,17 +191,17 an bug fixes.
143 Many thanks to everybody who contributed PRs for your patience in review and
191 Many thanks to everybody who contributed PRs for your patience in review and
144 merges.
192 merges.
145
193
146 Here is a non exhaustive list of changes that have been implemented for IPython
194 Here is a non-exhaustive list of changes that have been implemented for IPython
147 8.5.0. As usual you can find the full list of issues and PRs tagged with `the
195 8.5.0. As usual you can find the full list of issues and PRs tagged with `the
148 8.5 milestone
196 8.5 milestone
149 <https://github.com/ipython/ipython/pulls?q=is%3Aclosed+milestone%3A8.5+>`__.
197 <https://github.com/ipython/ipython/pulls?q=is%3Aclosed+milestone%3A8.5+>`__.
150
198
151 - Added shortcut for accepting auto suggestion. The End key shortcut for
199 - Added a shortcut for accepting auto suggestion. The End key shortcut for
152 accepting auto-suggestion This binding works in Vi mode too, provided
200 accepting auto-suggestion This binding works in Vi mode too, provided
153 ``TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode`` is set to be
201 ``TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode`` is set to be
154 ``True`` :ghpull:`13566`.
202 ``True`` :ghpull:`13566`.
155
203
156 - No popup in window for latex generation w hen generating latex (e.g. via
204 - No popup in window for latex generation when generating latex (e.g. via
157 `_latex_repr_`) no popup window is shows under Windows. :ghpull:`13679`
205 `_latex_repr_`) no popup window is shows under Windows. :ghpull:`13679`
158
206
159 - Fixed error raised when attempting to tab-complete an input string with
207 - Fixed error raised when attempting to tab-complete an input string with
@@ -269,12 +317,12 IPython 8.3.0
269
317
270
318
271 - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on
319 - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on
272 the info object when frontend provide it. This has been backported to 7.33
320 the info object when frontend provides it. This has been backported to 7.33
273
321
274 - :ghpull:`13624`, fixed :kbd:`End` key being broken after accepting an
322 - :ghpull:`13624`, fixed :kbd:`End` key being broken after accepting an
275 auto-suggestion.
323 auto-suggestion.
276
324
277 - :ghpull:`13657` fix issue where history from different sessions would be mixed.
325 - :ghpull:`13657` fixed an issue where history from different sessions would be mixed.
278
326
279 .. _version 8.2.0:
327 .. _version 8.2.0:
280
328
@@ -292,8 +340,8 IPython 8.2 mostly bring bugfixes to IPython.
292 - Fixes to ``ultratb`` ipdb support when used outside of IPython. :ghpull:`13498`
340 - Fixes to ``ultratb`` ipdb support when used outside of IPython. :ghpull:`13498`
293
341
294
342
295 I am still trying to fix and investigate :ghissue:`13598`, which seem to be
343 I am still trying to fix and investigate :ghissue:`13598`, which seems to be
296 random, and would appreciate help if you find reproducible minimal case. I've
344 random, and would appreciate help if you find a reproducible minimal case. I've
297 tried to make various changes to the codebase to mitigate it, but a proper fix
345 tried to make various changes to the codebase to mitigate it, but a proper fix
298 will be difficult without understanding the cause.
346 will be difficult without understanding the cause.
299
347
@@ -322,7 +370,7 IPython 8.1.0
322 -------------
370 -------------
323
371
324 IPython 8.1 is the first minor release after 8.0 and fixes a number of bugs and
372 IPython 8.1 is the first minor release after 8.0 and fixes a number of bugs and
325 Update a few behavior that were problematic with the 8.0 as with many new major
373 updates a few behaviors that were problematic with the 8.0 as with many new major
326 release.
374 release.
327
375
328 Note that beyond the changes listed here, IPython 8.1.0 also contains all the
376 Note that beyond the changes listed here, IPython 8.1.0 also contains all the
@@ -373,8 +421,8 We want to remind users that IPython is part of the Jupyter organisations, and
373 thus governed by a Code of Conduct. Some of the behavior we have seen on GitHub is not acceptable.
421 thus governed by a Code of Conduct. Some of the behavior we have seen on GitHub is not acceptable.
374 Abuse and non-respectful comments on discussion will not be tolerated.
422 Abuse and non-respectful comments on discussion will not be tolerated.
375
423
376 Many thanks to all the contributors to this release, many of the above fixed issue and
424 Many thanks to all the contributors to this release, many of the above fixed issues and
377 new features where done by first time contributors, showing there is still
425 new features were done by first time contributors, showing there is still
378 plenty of easy contribution possible in IPython
426 plenty of easy contribution possible in IPython
379 . You can find all individual contributions
427 . You can find all individual contributions
380 to this milestone `on github <https://github.com/ipython/ipython/milestone/91>`__.
428 to this milestone `on github <https://github.com/ipython/ipython/milestone/91>`__.
@@ -435,7 +483,7 IPython 8.0
435
483
436 IPython 8.0 is bringing a large number of new features and improvements to both the
484 IPython 8.0 is bringing a large number of new features and improvements to both the
437 user of the terminal and of the kernel via Jupyter. The removal of compatibility
485 user of the terminal and of the kernel via Jupyter. The removal of compatibility
438 with older version of Python is also the opportunity to do a couple of
486 with an older version of Python is also the opportunity to do a couple of
439 performance improvements in particular with respect to startup time.
487 performance improvements in particular with respect to startup time.
440 The 8.x branch started diverging from its predecessor around IPython 7.12
488 The 8.x branch started diverging from its predecessor around IPython 7.12
441 (January 2020).
489 (January 2020).
@@ -444,7 +492,7 This release contains 250+ pull requests, in addition to many of the features
444 and backports that have made it to the 7.x branch. Please see the
492 and backports that have made it to the 7.x branch. Please see the
445 `8.0 milestone <https://github.com/ipython/ipython/milestone/73?closed=1>`__ for the full list of pull requests.
493 `8.0 milestone <https://github.com/ipython/ipython/milestone/73?closed=1>`__ for the full list of pull requests.
446
494
447 Please feel free to send pull requests to updates those notes after release,
495 Please feel free to send pull requests to update those notes after release,
448 I have likely forgotten a few things reviewing 250+ PRs.
496 I have likely forgotten a few things reviewing 250+ PRs.
449
497
450 Dependencies changes/downstream packaging
498 Dependencies changes/downstream packaging
@@ -459,8 +507,8 looking for help to do so.
459 - minimal Python is now 3.8
507 - minimal Python is now 3.8
460 - ``nose`` is not a testing requirement anymore
508 - ``nose`` is not a testing requirement anymore
461 - ``pytest`` replaces nose.
509 - ``pytest`` replaces nose.
462 - ``iptest``/``iptest3`` cli entrypoints do not exists anymore.
510 - ``iptest``/``iptest3`` cli entrypoints do not exist anymore.
463 - minimum officially support ``numpy`` version has been bumped, but this should
511 - the minimum officially ​supported ``numpy`` version has been bumped, but this should
464 not have much effect on packaging.
512 not have much effect on packaging.
465
513
466
514
@@ -37,7 +37,7 install_requires =
37 matplotlib-inline
37 matplotlib-inline
38 pexpect>4.3; sys_platform != "win32"
38 pexpect>4.3; sys_platform != "win32"
39 pickleshare
39 pickleshare
40 prompt_toolkit>=3.0.11,<3.1.0
40 prompt_toolkit>=3.0.30,<3.1.0
41 pygments>=2.4.0
41 pygments>=2.4.0
42 stack_data
42 stack_data
43 traitlets>=5
43 traitlets>=5
This diff has been collapsed as it changes many lines, (608 lines changed) Show them Hide them
@@ -1,608 +0,0
1 """
2 Module to define and register Terminal IPython shortcuts with
3 :mod:`prompt_toolkit`
4 """
5
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
8
9 import warnings
10 import signal
11 import sys
12 import re
13 import os
14 from typing import Callable
15
16
17 from prompt_toolkit.application.current import get_app
18 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
19 from prompt_toolkit.filters import (has_focus, has_selection, Condition,
20 vi_insert_mode, emacs_insert_mode, has_completions, vi_mode)
21 from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline
22 from prompt_toolkit.key_binding import KeyBindings
23 from prompt_toolkit.key_binding.bindings import named_commands as nc
24 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
25
26 from IPython.utils.decorators import undoc
27
28 @undoc
29 @Condition
30 def cursor_in_leading_ws():
31 before = get_app().current_buffer.document.current_line_before_cursor
32 return (not before) or before.isspace()
33
34
35 # Needed for to accept autosuggestions in vi insert mode
36 def _apply_autosuggest(event):
37 """
38 Apply autosuggestion if at end of line.
39 """
40 b = event.current_buffer
41 d = b.document
42 after_cursor = d.text[d.cursor_position :]
43 lines = after_cursor.split("\n")
44 end_of_current_line = lines[0].strip()
45 suggestion = b.suggestion
46 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
47 b.insert_text(suggestion.text)
48 else:
49 nc.end_of_line(event)
50
51 def create_ipython_shortcuts(shell):
52 """Set up the prompt_toolkit keyboard shortcuts for IPython"""
53
54 kb = KeyBindings()
55 insert_mode = vi_insert_mode | emacs_insert_mode
56
57 if getattr(shell, 'handle_return', None):
58 return_handler = shell.handle_return(shell)
59 else:
60 return_handler = newline_or_execute_outer(shell)
61
62 kb.add('enter', filter=(has_focus(DEFAULT_BUFFER)
63 & ~has_selection
64 & insert_mode
65 ))(return_handler)
66
67 def reformat_and_execute(event):
68 reformat_text_before_cursor(event.current_buffer, event.current_buffer.document, shell)
69 event.current_buffer.validate_and_handle()
70
71 @Condition
72 def ebivim():
73 return shell.emacs_bindings_in_vi_insert_mode
74
75 kb.add(
76 "escape",
77 "enter",
78 filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim),
79 )(reformat_and_execute)
80
81 kb.add("c-\\")(quit)
82
83 kb.add('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER))
84 )(previous_history_or_previous_completion)
85
86 kb.add('c-n', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER))
87 )(next_history_or_next_completion)
88
89 kb.add('c-g', filter=(has_focus(DEFAULT_BUFFER) & has_completions)
90 )(dismiss_completion)
91
92 kb.add('c-c', filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
93
94 kb.add('c-c', filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
95
96 supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP'))
97 kb.add('c-z', filter=supports_suspend)(suspend_to_bg)
98
99 # Ctrl+I == Tab
100 kb.add('tab', filter=(has_focus(DEFAULT_BUFFER)
101 & ~has_selection
102 & insert_mode
103 & cursor_in_leading_ws
104 ))(indent_buffer)
105 kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode)
106 )(newline_autoindent_outer(shell.input_transformer_manager))
107
108 kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
109
110 @Condition
111 def auto_match():
112 return shell.auto_match
113
114 def all_quotes_paired(quote, buf):
115 paired = True
116 i = 0
117 while i < len(buf):
118 c = buf[i]
119 if c == quote:
120 paired = not paired
121 elif c == "\\":
122 i += 1
123 i += 1
124 return paired
125
126 focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER)
127 _preceding_text_cache = {}
128 _following_text_cache = {}
129
130 def preceding_text(pattern):
131 if pattern in _preceding_text_cache:
132 return _preceding_text_cache[pattern]
133
134 if callable(pattern):
135
136 def _preceding_text():
137 app = get_app()
138 before_cursor = app.current_buffer.document.current_line_before_cursor
139 return bool(pattern(before_cursor))
140
141 else:
142 m = re.compile(pattern)
143
144 def _preceding_text():
145 app = get_app()
146 before_cursor = app.current_buffer.document.current_line_before_cursor
147 return bool(m.match(before_cursor))
148
149 condition = Condition(_preceding_text)
150 _preceding_text_cache[pattern] = condition
151 return condition
152
153 def following_text(pattern):
154 try:
155 return _following_text_cache[pattern]
156 except KeyError:
157 pass
158 m = re.compile(pattern)
159
160 def _following_text():
161 app = get_app()
162 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
163
164 condition = Condition(_following_text)
165 _following_text_cache[pattern] = condition
166 return condition
167
168 @Condition
169 def not_inside_unclosed_string():
170 app = get_app()
171 s = app.current_buffer.document.text_before_cursor
172 # remove escaped quotes
173 s = s.replace('\\"', "").replace("\\'", "")
174 # remove triple-quoted string literals
175 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
176 # remove single-quoted string literals
177 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
178 return not ('"' in s or "'" in s)
179
180 # auto match
181 @kb.add("(", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))
182 def _(event):
183 event.current_buffer.insert_text("()")
184 event.current_buffer.cursor_left()
185
186 @kb.add("[", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))
187 def _(event):
188 event.current_buffer.insert_text("[]")
189 event.current_buffer.cursor_left()
190
191 @kb.add("{", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))
192 def _(event):
193 event.current_buffer.insert_text("{}")
194 event.current_buffer.cursor_left()
195
196 @kb.add(
197 '"',
198 filter=focused_insert
199 & auto_match
200 & not_inside_unclosed_string
201 & preceding_text(lambda line: all_quotes_paired('"', line))
202 & following_text(r"[,)}\]]|$"),
203 )
204 def _(event):
205 event.current_buffer.insert_text('""')
206 event.current_buffer.cursor_left()
207
208 @kb.add(
209 "'",
210 filter=focused_insert
211 & auto_match
212 & not_inside_unclosed_string
213 & preceding_text(lambda line: all_quotes_paired("'", line))
214 & following_text(r"[,)}\]]|$"),
215 )
216 def _(event):
217 event.current_buffer.insert_text("''")
218 event.current_buffer.cursor_left()
219
220 @kb.add(
221 '"',
222 filter=focused_insert
223 & auto_match
224 & not_inside_unclosed_string
225 & preceding_text(r'^.*""$'),
226 )
227 def _(event):
228 event.current_buffer.insert_text('""""')
229 event.current_buffer.cursor_left(3)
230
231 @kb.add(
232 "'",
233 filter=focused_insert
234 & auto_match
235 & not_inside_unclosed_string
236 & preceding_text(r"^.*''$"),
237 )
238 def _(event):
239 event.current_buffer.insert_text("''''")
240 event.current_buffer.cursor_left(3)
241
242 # raw string
243 @kb.add(
244 "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$")
245 )
246 def _(event):
247 matches = re.match(
248 r".*(r|R)[\"'](-*)",
249 event.current_buffer.document.current_line_before_cursor,
250 )
251 dashes = matches.group(2) or ""
252 event.current_buffer.insert_text("()" + dashes)
253 event.current_buffer.cursor_left(len(dashes) + 1)
254
255 @kb.add(
256 "[", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$")
257 )
258 def _(event):
259 matches = re.match(
260 r".*(r|R)[\"'](-*)",
261 event.current_buffer.document.current_line_before_cursor,
262 )
263 dashes = matches.group(2) or ""
264 event.current_buffer.insert_text("[]" + dashes)
265 event.current_buffer.cursor_left(len(dashes) + 1)
266
267 @kb.add(
268 "{", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$")
269 )
270 def _(event):
271 matches = re.match(
272 r".*(r|R)[\"'](-*)",
273 event.current_buffer.document.current_line_before_cursor,
274 )
275 dashes = matches.group(2) or ""
276 event.current_buffer.insert_text("{}" + dashes)
277 event.current_buffer.cursor_left(len(dashes) + 1)
278
279 # just move cursor
280 @kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))
281 @kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))
282 @kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))
283 @kb.add('"', filter=focused_insert & auto_match & following_text('^"'))
284 @kb.add("'", filter=focused_insert & auto_match & following_text("^'"))
285 def _(event):
286 event.current_buffer.cursor_right()
287
288 @kb.add(
289 "backspace",
290 filter=focused_insert
291 & preceding_text(r".*\($")
292 & auto_match
293 & following_text(r"^\)"),
294 )
295 @kb.add(
296 "backspace",
297 filter=focused_insert
298 & preceding_text(r".*\[$")
299 & auto_match
300 & following_text(r"^\]"),
301 )
302 @kb.add(
303 "backspace",
304 filter=focused_insert
305 & preceding_text(r".*\{$")
306 & auto_match
307 & following_text(r"^\}"),
308 )
309 @kb.add(
310 "backspace",
311 filter=focused_insert
312 & preceding_text('.*"$')
313 & auto_match
314 & following_text('^"'),
315 )
316 @kb.add(
317 "backspace",
318 filter=focused_insert
319 & preceding_text(r".*'$")
320 & auto_match
321 & following_text(r"^'"),
322 )
323 def _(event):
324 event.current_buffer.delete()
325 event.current_buffer.delete_before_cursor()
326
327 if shell.display_completions == "readlinelike":
328 kb.add(
329 "c-i",
330 filter=(
331 has_focus(DEFAULT_BUFFER)
332 & ~has_selection
333 & insert_mode
334 & ~cursor_in_leading_ws
335 ),
336 )(display_completions_like_readline)
337
338 if sys.platform == "win32":
339 kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste)
340
341 focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode
342
343 @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))
344 def _(event):
345 _apply_autosuggest(event)
346
347 @kb.add("c-e", filter=focused_insert_vi & ebivim)
348 def _(event):
349 _apply_autosuggest(event)
350
351 @kb.add("c-f", filter=focused_insert_vi)
352 def _(event):
353 b = event.current_buffer
354 suggestion = b.suggestion
355 if suggestion:
356 b.insert_text(suggestion.text)
357 else:
358 nc.forward_char(event)
359
360 @kb.add("escape", "f", filter=focused_insert_vi & ebivim)
361 def _(event):
362 b = event.current_buffer
363 suggestion = b.suggestion
364 if suggestion:
365 t = re.split(r"(\S+\s+)", suggestion.text)
366 b.insert_text(next((x for x in t if x), ""))
367 else:
368 nc.forward_word(event)
369
370 # Simple Control keybindings
371 key_cmd_dict = {
372 "c-a": nc.beginning_of_line,
373 "c-b": nc.backward_char,
374 "c-k": nc.kill_line,
375 "c-w": nc.backward_kill_word,
376 "c-y": nc.yank,
377 "c-_": nc.undo,
378 }
379
380 for key, cmd in key_cmd_dict.items():
381 kb.add(key, filter=focused_insert_vi & ebivim)(cmd)
382
383 # Alt and Combo Control keybindings
384 keys_cmd_dict = {
385 # Control Combos
386 ("c-x", "c-e"): nc.edit_and_execute,
387 ("c-x", "e"): nc.edit_and_execute,
388 # Alt
389 ("escape", "b"): nc.backward_word,
390 ("escape", "c"): nc.capitalize_word,
391 ("escape", "d"): nc.kill_word,
392 ("escape", "h"): nc.backward_kill_word,
393 ("escape", "l"): nc.downcase_word,
394 ("escape", "u"): nc.uppercase_word,
395 ("escape", "y"): nc.yank_pop,
396 ("escape", "."): nc.yank_last_arg,
397 }
398
399 for keys, cmd in keys_cmd_dict.items():
400 kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd)
401
402 def get_input_mode(self):
403 app = get_app()
404 app.ttimeoutlen = shell.ttimeoutlen
405 app.timeoutlen = shell.timeoutlen
406
407 return self._input_mode
408
409 def set_input_mode(self, mode):
410 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
411 cursor = "\x1b[{} q".format(shape)
412
413 sys.stdout.write(cursor)
414 sys.stdout.flush()
415
416 self._input_mode = mode
417
418 if shell.editing_mode == "vi" and shell.modal_cursor:
419 ViState._input_mode = InputMode.INSERT
420 ViState.input_mode = property(get_input_mode, set_input_mode)
421
422 return kb
423
424
425 def reformat_text_before_cursor(buffer, document, shell):
426 text = buffer.delete_before_cursor(len(document.text[:document.cursor_position]))
427 try:
428 formatted_text = shell.reformat_handler(text)
429 buffer.insert_text(formatted_text)
430 except Exception as e:
431 buffer.insert_text(text)
432
433
434 def newline_or_execute_outer(shell):
435
436 def newline_or_execute(event):
437 """When the user presses return, insert a newline or execute the code."""
438 b = event.current_buffer
439 d = b.document
440
441 if b.complete_state:
442 cc = b.complete_state.current_completion
443 if cc:
444 b.apply_completion(cc)
445 else:
446 b.cancel_completion()
447 return
448
449 # If there's only one line, treat it as if the cursor is at the end.
450 # See https://github.com/ipython/ipython/issues/10425
451 if d.line_count == 1:
452 check_text = d.text
453 else:
454 check_text = d.text[:d.cursor_position]
455 status, indent = shell.check_complete(check_text)
456
457 # if all we have after the cursor is whitespace: reformat current text
458 # before cursor
459 after_cursor = d.text[d.cursor_position:]
460 reformatted = False
461 if not after_cursor.strip():
462 reformat_text_before_cursor(b, d, shell)
463 reformatted = True
464 if not (d.on_last_line or
465 d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
466 ):
467 if shell.autoindent:
468 b.insert_text('\n' + indent)
469 else:
470 b.insert_text('\n')
471 return
472
473 if (status != 'incomplete') and b.accept_handler:
474 if not reformatted:
475 reformat_text_before_cursor(b, d, shell)
476 b.validate_and_handle()
477 else:
478 if shell.autoindent:
479 b.insert_text('\n' + indent)
480 else:
481 b.insert_text('\n')
482 return newline_or_execute
483
484
485 def previous_history_or_previous_completion(event):
486 """
487 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
488
489 If completer is open this still select previous completion.
490 """
491 event.current_buffer.auto_up()
492
493
494 def next_history_or_next_completion(event):
495 """
496 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
497
498 If completer is open this still select next completion.
499 """
500 event.current_buffer.auto_down()
501
502
503 def dismiss_completion(event):
504 b = event.current_buffer
505 if b.complete_state:
506 b.cancel_completion()
507
508
509 def reset_buffer(event):
510 b = event.current_buffer
511 if b.complete_state:
512 b.cancel_completion()
513 else:
514 b.reset()
515
516
517 def reset_search_buffer(event):
518 if event.current_buffer.document.text:
519 event.current_buffer.reset()
520 else:
521 event.app.layout.focus(DEFAULT_BUFFER)
522
523 def suspend_to_bg(event):
524 event.app.suspend_to_background()
525
526 def quit(event):
527 """
528 On platforms that support SIGQUIT, send SIGQUIT to the current process.
529 On other platforms, just exit the process with a message.
530 """
531 sigquit = getattr(signal, "SIGQUIT", None)
532 if sigquit is not None:
533 os.kill(0, signal.SIGQUIT)
534 else:
535 sys.exit("Quit")
536
537 def indent_buffer(event):
538 event.current_buffer.insert_text(' ' * 4)
539
540 @undoc
541 def newline_with_copy_margin(event):
542 """
543 DEPRECATED since IPython 6.0
544
545 See :any:`newline_autoindent_outer` for a replacement.
546
547 Preserve margin and cursor position when using
548 Control-O to insert a newline in EMACS mode
549 """
550 warnings.warn("`newline_with_copy_margin(event)` is deprecated since IPython 6.0. "
551 "see `newline_autoindent_outer(shell)(event)` for a replacement.",
552 DeprecationWarning, stacklevel=2)
553
554 b = event.current_buffer
555 cursor_start_pos = b.document.cursor_position_col
556 b.newline(copy_margin=True)
557 b.cursor_up(count=1)
558 cursor_end_pos = b.document.cursor_position_col
559 if cursor_start_pos != cursor_end_pos:
560 pos_diff = cursor_start_pos - cursor_end_pos
561 b.cursor_right(count=pos_diff)
562
563 def newline_autoindent_outer(inputsplitter) -> Callable[..., None]:
564 """
565 Return a function suitable for inserting a indented newline after the cursor.
566
567 Fancier version of deprecated ``newline_with_copy_margin`` which should
568 compute the correct indentation of the inserted line. That is to say, indent
569 by 4 extra space after a function definition, class definition, context
570 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
571 """
572
573 def newline_autoindent(event):
574 """insert a newline after the cursor indented appropriately."""
575 b = event.current_buffer
576 d = b.document
577
578 if b.complete_state:
579 b.cancel_completion()
580 text = d.text[:d.cursor_position] + '\n'
581 _, indent = inputsplitter.check_complete(text)
582 b.insert_text('\n' + (' ' * (indent or 0)), move_cursor=False)
583
584 return newline_autoindent
585
586
587 def open_input_in_editor(event):
588 event.app.current_buffer.open_in_editor()
589
590
591 if sys.platform == 'win32':
592 from IPython.core.error import TryNext
593 from IPython.lib.clipboard import (ClipboardEmpty,
594 win32_clipboard_get,
595 tkinter_clipboard_get)
596
597 @undoc
598 def win_paste(event):
599 try:
600 text = win32_clipboard_get()
601 except TryNext:
602 try:
603 text = tkinter_clipboard_get()
604 except (TryNext, ClipboardEmpty):
605 return
606 except ClipboardEmpty:
607 return
608 event.current_buffer.insert_text(text.replace("\t", " " * 4))
@@ -1,40 +0,0
1 import pytest
2 from IPython.terminal.shortcuts import _apply_autosuggest
3
4 from unittest.mock import Mock
5
6
7 def make_event(text, cursor, suggestion):
8 event = Mock()
9 event.current_buffer = Mock()
10 event.current_buffer.suggestion = Mock()
11 event.current_buffer.cursor_position = cursor
12 event.current_buffer.suggestion.text = suggestion
13 event.current_buffer.document = Mock()
14 event.current_buffer.document.get_end_of_line_position = Mock(return_value=0)
15 event.current_buffer.document.text = text
16 event.current_buffer.document.cursor_position = cursor
17 return event
18
19
20 @pytest.mark.parametrize(
21 "text, cursor, suggestion, called",
22 [
23 ("123456", 6, "123456789", True),
24 ("123456", 3, "123456789", False),
25 ("123456 \n789", 6, "123456789", True),
26 ],
27 )
28 def test_autosuggest_at_EOL(text, cursor, suggestion, called):
29 """
30 test that autosuggest is only applied at end of line.
31 """
32
33 event = make_event(text, cursor, suggestion)
34 event.current_buffer.insert_text = Mock()
35 _apply_autosuggest(event)
36 if called:
37 event.current_buffer.insert_text.assert_called()
38 else:
39 event.current_buffer.insert_text.assert_not_called()
40 # event.current_buffer.document.get_end_of_line_position.assert_called()
General Comments 0
You need to be logged in to leave comments. Login now