##// END OF EJS Templates
Merge branch 'ipython:main' into main
Nelson Ferreira -
r28111:db5b4983 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 b''
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 b''
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 b''
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 b''
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 b''
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 b' 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
@@ -63,7 +63,7 b' __version__ = release.version'
63 version_info = release.version_info
63 version_info = release.version_info
64 # list of CVEs that should have been patched in this release.
64 # list of CVEs that should have been patched in this release.
65 # this is informational and should not be relied upon.
65 # this is informational and should not be relied upon.
66 __patched_cves__ = {"CVE-2022-21699"}
66 __patched_cves__ = {"CVE-2022-21699", "CVE-2023-24816"}
67
67
68
68
69 def embed_kernel(module=None, local_ns=None, **kwargs):
69 def embed_kernel(module=None, local_ns=None, **kwargs):
@@ -310,7 +310,7 b' class BaseIPythonApplication(Application):'
310 except OSError as e:
310 except OSError as e:
311 # this will not be EEXIST
311 # this will not be EEXIST
312 self.log.error("couldn't create path %s: %s", path, e)
312 self.log.error("couldn't create path %s: %s", path, e)
313 self.log.debug("IPYTHONDIR set to: %s" % new)
313 self.log.debug("IPYTHONDIR set to: %s", new)
314
314
315 def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS):
315 def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS):
316 """Load the config file.
316 """Load the config file.
@@ -400,7 +400,7 b' class BaseIPythonApplication(Application):'
400 self.log.fatal("Profile %r not found."%self.profile)
400 self.log.fatal("Profile %r not found."%self.profile)
401 self.exit(1)
401 self.exit(1)
402 else:
402 else:
403 self.log.debug(f"Using existing profile dir: {p.location!r}")
403 self.log.debug("Using existing profile dir: %r", p.location)
404 else:
404 else:
405 location = self.config.ProfileDir.location
405 location = self.config.ProfileDir.location
406 # location is fully specified
406 # location is fully specified
@@ -420,7 +420,7 b' class BaseIPythonApplication(Application):'
420 self.log.fatal("Profile directory %r not found."%location)
420 self.log.fatal("Profile directory %r not found."%location)
421 self.exit(1)
421 self.exit(1)
422 else:
422 else:
423 self.log.debug(f"Using existing profile dir: {p.location!r}")
423 self.log.debug("Using existing profile dir: %r", p.location)
424 # if profile_dir is specified explicitly, set profile name
424 # if profile_dir is specified explicitly, set profile name
425 dir_name = os.path.basename(p.location)
425 dir_name = os.path.basename(p.location)
426 if dir_name.startswith('profile_'):
426 if dir_name.startswith('profile_'):
@@ -467,7 +467,7 b' class BaseIPythonApplication(Application):'
467 s = self.generate_config_file()
467 s = self.generate_config_file()
468 config_file = Path(self.profile_dir.location) / self.config_file_name
468 config_file = Path(self.profile_dir.location) / self.config_file_name
469 if self.overwrite or not config_file.exists():
469 if self.overwrite or not config_file.exists():
470 self.log.warning("Generating default config file: %r" % (config_file))
470 self.log.warning("Generating default config file: %r", (config_file))
471 config_file.write_text(s, encoding="utf-8")
471 config_file.write_text(s, encoding="utf-8")
472
472
473 @catch_config_error
473 @catch_config_error
@@ -609,8 +609,8 b' SUPPORTED_EXTERNAL_GETITEM = {'
609
609
610 BUILTIN_GETITEM: Set[InstancesHaveGetItem] = {
610 BUILTIN_GETITEM: Set[InstancesHaveGetItem] = {
611 dict,
611 dict,
612 str,
612 str, # type: ignore[arg-type]
613 bytes,
613 bytes, # type: ignore[arg-type]
614 list,
614 list,
615 tuple,
615 tuple,
616 collections.defaultdict,
616 collections.defaultdict,
@@ -619,7 +619,7 b' BUILTIN_GETITEM: Set[InstancesHaveGetItem] = {'
619 collections.ChainMap,
619 collections.ChainMap,
620 collections.UserDict,
620 collections.UserDict,
621 collections.UserList,
621 collections.UserList,
622 collections.UserString,
622 collections.UserString, # type: ignore[arg-type]
623 _DummyNamedTuple,
623 _DummyNamedTuple,
624 _IdentitySubscript,
624 _IdentitySubscript,
625 }
625 }
@@ -3013,7 +3013,7 b' class InteractiveShell(SingletonConfigurable):'
3013 runner = _pseudo_sync_runner
3013 runner = _pseudo_sync_runner
3014
3014
3015 try:
3015 try:
3016 return runner(coro)
3016 result = runner(coro)
3017 except BaseException as e:
3017 except BaseException as e:
3018 info = ExecutionInfo(
3018 info = ExecutionInfo(
3019 raw_cell, store_history, silent, shell_futures, cell_id
3019 raw_cell, store_history, silent, shell_futures, cell_id
@@ -3021,6 +3021,7 b' class InteractiveShell(SingletonConfigurable):'
3021 result = ExecutionResult(info)
3021 result = ExecutionResult(info)
3022 result.error_in_exec = e
3022 result.error_in_exec = e
3023 self.showtraceback(running_compiled_code=True)
3023 self.showtraceback(running_compiled_code=True)
3024 finally:
3024 return result
3025 return result
3025
3026
3026 def should_run_async(
3027 def should_run_async(
@@ -198,7 +198,16 b' 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.
@@ -468,9 +468,9 b' 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 b' 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 b' 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
@@ -16,7 +16,7 b''
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 = 11
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 b' 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:
@@ -17,6 +17,7 b' import shutil'
17 import sys
17 import sys
18 import tempfile
18 import tempfile
19 import unittest
19 import unittest
20 import pytest
20 from unittest import mock
21 from unittest import mock
21
22
22 from os.path import join
23 from os.path import join
@@ -635,10 +636,23 b' class TestSystemRaw(ExitCodeChecks):'
635 )
636 )
636 self.assertEqual(ip.user_ns["_exit_code"], -signal.SIGINT)
637 self.assertEqual(ip.user_ns["_exit_code"], -signal.SIGINT)
637
638
638 def test_magic_warnings(self):
639
639 for magic_cmd in ("pip", "conda", "cd"):
640 @pytest.mark.parametrize("magic_cmd", ["pip", "conda", "cd"])
640 with self.assertWarnsRegex(Warning, "You executed the system command"):
641 def test_magic_warnings(magic_cmd):
641 ip.system_raw(magic_cmd)
642 if sys.platform == "win32":
643 to_mock = "os.system"
644 expected_arg, expected_kwargs = magic_cmd, dict()
645 else:
646 to_mock = "subprocess.call"
647 expected_arg, expected_kwargs = magic_cmd, dict(
648 shell=True, executable=os.environ.get("SHELL", None)
649 )
650
651 with mock.patch(to_mock, return_value=0) as mock_sub:
652 with pytest.warns(Warning, match=r"You executed the system command"):
653 ip.system_raw(magic_cmd)
654 mock_sub.assert_called_once_with(expected_arg, **expected_kwargs)
655
642
656
643 # TODO: Exit codes are currently ignored on Windows.
657 # TODO: Exit codes are currently ignored on Windows.
644 class TestSystemPipedExitCode(ExitCodeChecks):
658 class TestSystemPipedExitCode(ExitCodeChecks):
@@ -1089,9 +1103,12 b' def test_run_cell_asyncio_run():'
1089
1103
1090
1104
1091 def test_should_run_async():
1105 def test_should_run_async():
1092 assert not ip.should_run_async("a = 5")
1106 assert not ip.should_run_async("a = 5", transformed_cell="a = 5")
1093 assert ip.should_run_async("await x")
1107 assert ip.should_run_async("await x", transformed_cell="await x")
1094 assert ip.should_run_async("import asyncio; await asyncio.sleep(1)")
1108 assert ip.should_run_async(
1109 "import asyncio; await asyncio.sleep(1)",
1110 transformed_cell="import asyncio; await asyncio.sleep(1)",
1111 )
1095
1112
1096
1113
1097 def test_set_custom_completer():
1114 def test_set_custom_completer():
@@ -1110,3 +1127,49 b' def test_set_custom_completer():'
1110
1127
1111 # clean up
1128 # clean up
1112 ip.Completer.custom_matchers.pop()
1129 ip.Completer.custom_matchers.pop()
1130
1131
1132 class TestShowTracebackAttack(unittest.TestCase):
1133 """Test that the interactive shell is resilient against the client attack of
1134 manipulating the showtracebacks method. These attacks shouldn't result in an
1135 unhandled exception in the kernel."""
1136
1137 def setUp(self):
1138 self.orig_showtraceback = interactiveshell.InteractiveShell.showtraceback
1139
1140 def tearDown(self):
1141 interactiveshell.InteractiveShell.showtraceback = self.orig_showtraceback
1142
1143 def test_set_show_tracebacks_none(self):
1144 """Test the case of the client setting showtracebacks to None"""
1145
1146 result = ip.run_cell(
1147 """
1148 import IPython.core.interactiveshell
1149 IPython.core.interactiveshell.InteractiveShell.showtraceback = None
1150
1151 assert False, "This should not raise an exception"
1152 """
1153 )
1154 print(result)
1155
1156 assert result.result is None
1157 assert isinstance(result.error_in_exec, TypeError)
1158 assert str(result.error_in_exec) == "'NoneType' object is not callable"
1159
1160 def test_set_show_tracebacks_noop(self):
1161 """Test the case of the client setting showtracebacks to a no op lambda"""
1162
1163 result = ip.run_cell(
1164 """
1165 import IPython.core.interactiveshell
1166 IPython.core.interactiveshell.InteractiveShell.showtraceback = lambda *args, **kwargs: None
1167
1168 assert False, "This should not raise an exception"
1169 """
1170 )
1171 print(result)
1172
1173 assert result.result is None
1174 assert isinstance(result.error_in_exec, AssertionError)
1175 assert str(result.error_in_exec) == "This should not raise an exception"
@@ -160,38 +160,37 b" def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):"
160 with workdir.joinpath(tmpfile).open("w", encoding="utf8") as f:
160 with workdir.joinpath(tmpfile).open("w", encoding="utf8") as f:
161 f.writelines(genelatex(s, wrap))
161 f.writelines(genelatex(s, wrap))
162
162
163 with open(os.devnull, 'wb') as devnull:
163 subprocess.check_call(
164 subprocess.check_call(
164 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
165 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
165 cwd=workdir,
166 cwd=workdir,
166 stdout=subprocess.DEVNULL,
167 stdout=devnull,
167 stderr=subprocess.DEVNULL,
168 stderr=devnull,
168 startupinfo=startupinfo,
169 startupinfo=startupinfo,
169 )
170 )
170
171
171 resolution = round(150 * scale)
172 resolution = round(150*scale)
172 subprocess.check_call(
173 subprocess.check_call(
173 [
174 [
174 "dvipng",
175 "dvipng",
175 "-T",
176 "-T",
176 "tight",
177 "tight",
177 "-D",
178 "-D",
178 str(resolution),
179 str(resolution),
179 "-z",
180 "-z",
180 "9",
181 "9",
181 "-bg",
182 "-bg",
182 "Transparent",
183 "Transparent",
183 "-o",
184 "-o",
184 outfile,
185 outfile,
185 dvifile,
186 dvifile,
186 "-fg",
187 "-fg",
187 color,
188 color,
188 ],
189 ],
189 cwd=workdir,
190 cwd=workdir,
190 stdout=subprocess.DEVNULL,
191 stdout=devnull,
191 stderr=subprocess.DEVNULL,
192 stderr=devnull,
192 startupinfo=startupinfo,
193 startupinfo=startupinfo,
193 )
194 )
195
194
196 with workdir.joinpath(outfile).open("rb") as f:
195 with workdir.joinpath(outfile).open("rb") as f:
197 return f.read()
196 return f.read()
@@ -4,6 +4,7 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b" 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 b' 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 b' 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 b' 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 b' 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
@@ -91,30 +91,14 b" if os.name == 'posix':"
91 _set_term_title = _set_term_title_xterm
91 _set_term_title = _set_term_title_xterm
92 _restore_term_title = _restore_term_title_xterm
92 _restore_term_title = _restore_term_title_xterm
93 elif sys.platform == 'win32':
93 elif sys.platform == 'win32':
94 try:
94 import ctypes
95 import ctypes
95
96
96 SetConsoleTitleW = ctypes.windll.kernel32.SetConsoleTitleW
97 SetConsoleTitleW = ctypes.windll.kernel32.SetConsoleTitleW
97 SetConsoleTitleW.argtypes = [ctypes.c_wchar_p]
98 SetConsoleTitleW.argtypes = [ctypes.c_wchar_p]
98
99
99 def _set_term_title(title):
100 def _set_term_title(title):
100 """Set terminal title using ctypes to access the Win32 APIs."""
101 """Set terminal title using ctypes to access the Win32 APIs."""
101 SetConsoleTitleW(title)
102 SetConsoleTitleW(title)
103 except ImportError:
104 def _set_term_title(title):
105 """Set terminal title using the 'title' command."""
106 global ignore_termtitle
107
108 try:
109 # Cannot be on network share when issuing system commands
110 curr = os.getcwd()
111 os.chdir("C:")
112 ret = os.system("title " + title)
113 finally:
114 os.chdir(curr)
115 if ret:
116 # non-zero return code signals error, don't try again
117 ignore_termtitle = True
118
102
119
103
120 def set_term_title(title):
104 def set_term_title(title):
@@ -1,45 +1,98 b''
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 b' 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 b' 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 b" 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 b" 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 b' 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
@@ -4,28 +4,23 b' 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,91 b''
2 8.x Series
2 8.x Series
3 ============
3 ============
4
4
5
6 .. _version 8.10.0:
7
8 IPython 8.10
9 ------------
10
11 Out of schedule release of IPython with minor fixes to patch a potential CVE-2023-24816.
12 This is a really low severity CVE that you most likely are not affected by unless:
13
14 - You are on windows.
15 - You have a custom build of Python without ``_ctypes``
16 - You cd or start IPython or Jupyter in untrusted directory which names may be
17 valid shell commands.
18
19 You can read more on `the advisory
20 <https://github.com/ipython/ipython/security/advisories/GHSA-29gw-9793-fvw7>`__.
21
22 In addition to fixing this CVE we also fix a couple of outstanding bugs and issues.
23
24 As usual you can find the full list of PRs on GitHub under `the 8.10 milestone
25 <https://github.com/ipython/ipython/milestone/112?closed=1>`__.
26
27 In Particular:
28
29 - bump minimum numpy to `>=1.21` version following NEP29. :ghpull:`13930`
30 - fix for compatibility with MyPy 1.0. :ghpull:`13933`
31 - fix nbgrader stalling when IPython's ``showtraceback`` function is
32 monkeypatched. :ghpull:`13934`
33
34
35
36 As this release also contains those minimal changes in addition to fixing the
37 CVE I decided to bump the minor version anyway.
38
39 This will not affect the normal release schedule, so IPython 8.11 is due in
40 about 2 weeks.
41
42 .. _version 8.9.0:
43
44 IPython 8.9.0
45 -------------
46
47 Second release of IPython in 2023, last Friday of the month, we are back on
48 track. This is a small release with a few bug-fixes, and improvements, mostly
49 with respect to terminal shortcuts.
50
51
52 The biggest improvement for 8.9 is a drastic amelioration of the
53 auto-suggestions sponsored by D.E. Shaw and implemented by the more and more
54 active contributor `@krassowski <https://github.com/krassowski>`.
55
56 - ``right`` accepts a single character from suggestion
57 - ``ctrl+right`` accepts a semantic token (macos default shortcuts take
58 precedence and need to be disabled to make this work)
59 - ``backspace`` deletes a character and resumes hinting autosuggestions
60 - ``ctrl-left`` accepts suggestion and moves cursor left one character.
61 - ``backspace`` deletes a character and resumes hinting autosuggestions
62 - ``down`` moves to suggestion to later in history when no lines are present below the cursors.
63 - ``up`` moves to suggestion from earlier in history when no lines are present above the cursor.
64
65 This is best described by the Gif posted by `@krassowski
66 <https://github.com/krassowski>`, and in the PR itself :ghpull:`13888`.
67
68 .. image:: ../_images/autosuggest.gif
69
70 Please report any feedback in order for us to improve the user experience.
71 In particular we are also working on making the shortcuts configurable.
72
73 If you are interested in better terminal shortcuts, I also invite you to
74 participate in issue `13879
75 <https://github.com/ipython/ipython/issues/13879>`__.
76
77
78 As we follow `NEP29
79 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`__, next version of
80 IPython will officially stop supporting numpy 1.20, and will stop supporting
81 Python 3.8 after April release.
82
83 As usual you can find the full list of PRs on GitHub under `the 8.9 milestone
84 <https://github.com/ipython/ipython/milestone/111?closed=1>`__.
85
86
87 Thanks to the `D. E. Shaw group <https://deshaw.com/>`__ for sponsoring
88 work on IPython and related libraries.
89
5 .. _version 8.8.0:
90 .. _version 8.8.0:
6
91
7 IPython 8.8.0
92 IPython 8.8.0
@@ -11,11 +96,11 b' First release of IPython in 2023 as there was no release at the end of'
11 December.
96 December.
12
97
13 This is an unusually big release (relatively speaking) with more than 15 Pull
98 This is an unusually big release (relatively speaking) with more than 15 Pull
14 Requests merge.
99 Requests merged.
15
100
16 Of particular interest are:
101 Of particular interest are:
17
102
18 - :ghpull:`13852` that replace the greedy completer and improve
103 - :ghpull:`13852` that replaces the greedy completer and improves
19 completion, in particular for dictionary keys.
104 completion, in particular for dictionary keys.
20 - :ghpull:`13858` that adds ``py.typed`` to ``setup.cfg`` to make sure it is
105 - :ghpull:`13858` that adds ``py.typed`` to ``setup.cfg`` to make sure it is
21 bundled in wheels.
106 bundled in wheels.
@@ -24,7 +109,7 b' Of particular interest are:'
24 believe this also needs a recent version of Traitlets.
109 believe this also needs a recent version of Traitlets.
25 - :ghpull:`13865` makes the ``inspector`` class of `InteractiveShell`
110 - :ghpull:`13865` makes the ``inspector`` class of `InteractiveShell`
26 configurable.
111 configurable.
27 - :ghpull:`13880` that remove minor-version entrypoints as the minor version
112 - :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
113 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.
114 Python version that was used to build the ``whl`` file.
30
115
@@ -48,8 +133,8 b' IPython 8.7.0'
48
133
49
134
50 Small release of IPython with a couple of bug fixes and new features for this
135 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
136 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.
137 close to the new year's eve, or if the next release will be at the end of January.
53
138
54 Here are a few of the relevant fixes,
139 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
140 as usual you can find the full list of PRs on GitHub under `the 8.7 milestone
@@ -73,29 +158,29 b' IPython 8.6.0'
73
158
74 Back to a more regular release schedule (at least I try), as Friday is
159 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
160 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.
161 few new features that contain no less than 25 PRs.
77
162
78 We'll notably found a couple of non negligible changes:
163 We'll notably found a couple of non negligible changes:
79
164
80 The ``install_ext`` and related functions have been removed after being
165 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
166 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
167 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`
168 without installing them. Just set your ``sys.path`` for example. :ghpull:`13744`
84
169
85 IPython now have extra entry points that that the major *and minor* version of
170 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
171 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
172 launch IPython from the Python 3.10 interpreter, while still using Python 3.11
88 as your main Python. :ghpull:`13743`
173 as your main Python. :ghpull:`13743`
89
174
90 The completer matcher API have been improved. See :ghpull:`13745`. This should
175 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.
176 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
177 Thanks ``@krassowski`` for all the work, and the D.E. Shaw group for sponsoring
93 it.
178 it.
94
179
95 The color of error nodes in tracebacks can now be customized. See
180 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
181 :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
182 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
183 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
184 (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.
185 I won't have time to do before long, so contributions welcome.
101
186
@@ -108,7 +193,7 b' This mostly occurs in teaching context when incorrect values get passed around.'
108
193
109
194
110 The ``?``, ``??``, and corresponding ``pinfo``, ``pinfo2`` magics can now find
195 The ``?``, ``??``, and corresponding ``pinfo``, ``pinfo2`` magics can now find
111 objects insides arrays. That is to say, the following now works::
196 objects inside arrays. That is to say, the following now works::
112
197
113
198
114 >>> def my_func(*arg, **kwargs):pass
199 >>> def my_func(*arg, **kwargs):pass
@@ -117,7 +202,7 b' objects insides arrays. That is to say, the following now works::'
117
202
118
203
119 If ``container`` define a custom ``getitem``, this __will__ trigger the custom
204 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
205 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.
206 group for the request and sponsoring the work.
122
207
123
208
@@ -143,17 +228,17 b' an bug fixes.'
143 Many thanks to everybody who contributed PRs for your patience in review and
228 Many thanks to everybody who contributed PRs for your patience in review and
144 merges.
229 merges.
145
230
146 Here is a non exhaustive list of changes that have been implemented for IPython
231 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
232 8.5.0. As usual you can find the full list of issues and PRs tagged with `the
148 8.5 milestone
233 8.5 milestone
149 <https://github.com/ipython/ipython/pulls?q=is%3Aclosed+milestone%3A8.5+>`__.
234 <https://github.com/ipython/ipython/pulls?q=is%3Aclosed+milestone%3A8.5+>`__.
150
235
151 - Added shortcut for accepting auto suggestion. The End key shortcut for
236 - Added a shortcut for accepting auto suggestion. The End key shortcut for
152 accepting auto-suggestion This binding works in Vi mode too, provided
237 accepting auto-suggestion This binding works in Vi mode too, provided
153 ``TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode`` is set to be
238 ``TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode`` is set to be
154 ``True`` :ghpull:`13566`.
239 ``True`` :ghpull:`13566`.
155
240
156 - No popup in window for latex generation w hen generating latex (e.g. via
241 - 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`
242 `_latex_repr_`) no popup window is shows under Windows. :ghpull:`13679`
158
243
159 - Fixed error raised when attempting to tab-complete an input string with
244 - Fixed error raised when attempting to tab-complete an input string with
@@ -269,12 +354,12 b' IPython 8.3.0'
269
354
270
355
271 - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on
356 - :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
357 the info object when frontend provides it. This has been backported to 7.33
273
358
274 - :ghpull:`13624`, fixed :kbd:`End` key being broken after accepting an
359 - :ghpull:`13624`, fixed :kbd:`End` key being broken after accepting an
275 auto-suggestion.
360 auto-suggestion.
276
361
277 - :ghpull:`13657` fix issue where history from different sessions would be mixed.
362 - :ghpull:`13657` fixed an issue where history from different sessions would be mixed.
278
363
279 .. _version 8.2.0:
364 .. _version 8.2.0:
280
365
@@ -292,8 +377,8 b' IPython 8.2 mostly bring bugfixes to IPython.'
292 - Fixes to ``ultratb`` ipdb support when used outside of IPython. :ghpull:`13498`
377 - Fixes to ``ultratb`` ipdb support when used outside of IPython. :ghpull:`13498`
293
378
294
379
295 I am still trying to fix and investigate :ghissue:`13598`, which seem to be
380 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
381 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
382 tried to make various changes to the codebase to mitigate it, but a proper fix
298 will be difficult without understanding the cause.
383 will be difficult without understanding the cause.
299
384
@@ -322,7 +407,7 b' IPython 8.1.0'
322 -------------
407 -------------
323
408
324 IPython 8.1 is the first minor release after 8.0 and fixes a number of bugs and
409 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
410 updates a few behaviors that were problematic with the 8.0 as with many new major
326 release.
411 release.
327
412
328 Note that beyond the changes listed here, IPython 8.1.0 also contains all the
413 Note that beyond the changes listed here, IPython 8.1.0 also contains all the
@@ -373,8 +458,8 b' 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.
458 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.
459 Abuse and non-respectful comments on discussion will not be tolerated.
375
460
376 Many thanks to all the contributors to this release, many of the above fixed issue and
461 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
462 new features were done by first time contributors, showing there is still
378 plenty of easy contribution possible in IPython
463 plenty of easy contribution possible in IPython
379 . You can find all individual contributions
464 . You can find all individual contributions
380 to this milestone `on github <https://github.com/ipython/ipython/milestone/91>`__.
465 to this milestone `on github <https://github.com/ipython/ipython/milestone/91>`__.
@@ -435,7 +520,7 b' IPython 8.0'
435
520
436 IPython 8.0 is bringing a large number of new features and improvements to both the
521 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
522 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
523 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.
524 performance improvements in particular with respect to startup time.
440 The 8.x branch started diverging from its predecessor around IPython 7.12
525 The 8.x branch started diverging from its predecessor around IPython 7.12
441 (January 2020).
526 (January 2020).
@@ -444,7 +529,7 b' 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
529 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.
530 `8.0 milestone <https://github.com/ipython/ipython/milestone/73?closed=1>`__ for the full list of pull requests.
446
531
447 Please feel free to send pull requests to updates those notes after release,
532 Please feel free to send pull requests to update those notes after release,
448 I have likely forgotten a few things reviewing 250+ PRs.
533 I have likely forgotten a few things reviewing 250+ PRs.
449
534
450 Dependencies changes/downstream packaging
535 Dependencies changes/downstream packaging
@@ -459,8 +544,8 b' looking for help to do so.'
459 - minimal Python is now 3.8
544 - minimal Python is now 3.8
460 - ``nose`` is not a testing requirement anymore
545 - ``nose`` is not a testing requirement anymore
461 - ``pytest`` replaces nose.
546 - ``pytest`` replaces nose.
462 - ``iptest``/``iptest3`` cli entrypoints do not exists anymore.
547 - ``iptest``/``iptest3`` cli entrypoints do not exist anymore.
463 - minimum officially support ``numpy`` version has been bumped, but this should
548 - the minimum officially supported ``numpy`` version has been bumped, but this should
464 not have much effect on packaging.
549 not have much effect on packaging.
465
550
466
551
@@ -37,7 +37,7 b' 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
@@ -79,7 +79,7 b' test_extra ='
79 curio
79 curio
80 matplotlib!=3.2.0
80 matplotlib!=3.2.0
81 nbformat
81 nbformat
82 numpy>=1.20
82 numpy>=1.21
83 pandas
83 pandas
84 trio
84 trio
85 all =
85 all =
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (608 lines changed) Show them Hide them
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now