##// END OF EJS Templates
Revert "enable formatting by default"...
Adam Johnson -
Show More
@@ -1,742 +1,742 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2
3 3 import asyncio
4 4 import os
5 5 import sys
6 6 import warnings
7 7 from warnings import warn
8 8
9 9 from IPython.core.async_helpers import get_asyncio_loop
10 10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
11 11 from IPython.utils import io
12 12 from IPython.utils.py3compat import input
13 13 from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
14 14 from IPython.utils.process import abbrev_cwd
15 15 from traitlets import (
16 16 Bool,
17 17 Unicode,
18 18 Dict,
19 19 Integer,
20 20 observe,
21 21 Instance,
22 22 Type,
23 23 default,
24 24 Enum,
25 25 Union,
26 26 Any,
27 27 validate,
28 28 Float,
29 29 )
30 30
31 31 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
32 32 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
33 33 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
34 34 from prompt_toolkit.formatted_text import PygmentsTokens
35 35 from prompt_toolkit.history import InMemoryHistory
36 36 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
37 37 from prompt_toolkit.output import ColorDepth
38 38 from prompt_toolkit.patch_stdout import patch_stdout
39 39 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
40 40 from prompt_toolkit.styles import DynamicStyle, merge_styles
41 41 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
42 42 from prompt_toolkit import __version__ as ptk_version
43 43
44 44 from pygments.styles import get_style_by_name
45 45 from pygments.style import Style
46 46 from pygments.token import Token
47 47
48 48 from .debugger import TerminalPdb, Pdb
49 49 from .magics import TerminalMagics
50 50 from .pt_inputhooks import get_inputhook_name_and_func
51 51 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
52 52 from .ptutils import IPythonPTCompleter, IPythonPTLexer
53 53 from .shortcuts import create_ipython_shortcuts
54 54
55 55 PTK3 = ptk_version.startswith('3.')
56 56
57 57
58 58 class _NoStyle(Style): pass
59 59
60 60
61 61
62 62 _style_overrides_light_bg = {
63 63 Token.Prompt: '#ansibrightblue',
64 64 Token.PromptNum: '#ansiblue bold',
65 65 Token.OutPrompt: '#ansibrightred',
66 66 Token.OutPromptNum: '#ansired bold',
67 67 }
68 68
69 69 _style_overrides_linux = {
70 70 Token.Prompt: '#ansibrightgreen',
71 71 Token.PromptNum: '#ansigreen bold',
72 72 Token.OutPrompt: '#ansibrightred',
73 73 Token.OutPromptNum: '#ansired bold',
74 74 }
75 75
76 76 def get_default_editor():
77 77 try:
78 78 return os.environ['EDITOR']
79 79 except KeyError:
80 80 pass
81 81 except UnicodeError:
82 82 warn("$EDITOR environment variable is not pure ASCII. Using platform "
83 83 "default editor.")
84 84
85 85 if os.name == 'posix':
86 86 return 'vi' # the only one guaranteed to be there!
87 87 else:
88 88 return 'notepad' # same in Windows!
89 89
90 90 # conservatively check for tty
91 91 # overridden streams can result in things like:
92 92 # - sys.stdin = None
93 93 # - no isatty method
94 94 for _name in ('stdin', 'stdout', 'stderr'):
95 95 _stream = getattr(sys, _name)
96 96 if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
97 97 _is_tty = False
98 98 break
99 99 else:
100 100 _is_tty = True
101 101
102 102
103 103 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
104 104
105 105 def black_reformat_handler(text_before_cursor):
106 106 """
107 107 We do not need to protect against error,
108 108 this is taken care at a higher level where any reformat error is ignored.
109 109 Indeed we may call reformatting on incomplete code.
110 110 """
111 111 import black
112 112
113 113 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
114 114 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
115 115 formatted_text = formatted_text[:-1]
116 116 return formatted_text
117 117
118 118
119 119 def yapf_reformat_handler(text_before_cursor):
120 120 from yapf.yapflib import file_resources
121 121 from yapf.yapflib import yapf_api
122 122
123 123 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
124 124 formatted_text, was_formatted = yapf_api.FormatCode(
125 125 text_before_cursor, style_config=style_config
126 126 )
127 127 if was_formatted:
128 128 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
129 129 formatted_text = formatted_text[:-1]
130 130 return formatted_text
131 131 else:
132 132 return text_before_cursor
133 133
134 134
135 135 class TerminalInteractiveShell(InteractiveShell):
136 136 mime_renderers = Dict().tag(config=True)
137 137
138 138 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
139 139 'to reserve for the tab completion menu, '
140 140 'search history, ...etc, the height of '
141 141 'these menus will at most this value. '
142 142 'Increase it is you prefer long and skinny '
143 143 'menus, decrease for short and wide.'
144 144 ).tag(config=True)
145 145
146 146 pt_app = None
147 147 debugger_history = None
148 148
149 149 debugger_history_file = Unicode(
150 150 "~/.pdbhistory", help="File in which to store and read history"
151 151 ).tag(config=True)
152 152
153 153 simple_prompt = Bool(_use_simple_prompt,
154 154 help="""Use `raw_input` for the REPL, without completion and prompt colors.
155 155
156 156 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
157 157 IPython own testing machinery, and emacs inferior-shell integration through elpy.
158 158
159 159 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
160 160 environment variable is set, or the current terminal is not a tty."""
161 161 ).tag(config=True)
162 162
163 163 @property
164 164 def debugger_cls(self):
165 165 return Pdb if self.simple_prompt else TerminalPdb
166 166
167 167 confirm_exit = Bool(True,
168 168 help="""
169 169 Set to confirm when you try to exit IPython with an EOF (Control-D
170 170 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
171 171 you can force a direct exit without any confirmation.""",
172 172 ).tag(config=True)
173 173
174 174 editing_mode = Unicode('emacs',
175 175 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
176 176 ).tag(config=True)
177 177
178 178 emacs_bindings_in_vi_insert_mode = Bool(
179 179 True,
180 180 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
181 181 ).tag(config=True)
182 182
183 183 modal_cursor = Bool(
184 184 True,
185 185 help="""
186 186 Cursor shape changes depending on vi mode: beam in vi insert mode,
187 187 block in nav mode, underscore in replace mode.""",
188 188 ).tag(config=True)
189 189
190 190 ttimeoutlen = Float(
191 191 0.01,
192 192 help="""The time in milliseconds that is waited for a key code
193 193 to complete.""",
194 194 ).tag(config=True)
195 195
196 196 timeoutlen = Float(
197 197 0.5,
198 198 help="""The time in milliseconds that is waited for a mapped key
199 199 sequence to complete.""",
200 200 ).tag(config=True)
201 201
202 202 autoformatter = Unicode(
203 "black",
203 None,
204 204 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
205 205 allow_none=True
206 206 ).tag(config=True)
207 207
208 208 auto_match = Bool(
209 209 False,
210 210 help="""
211 211 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
212 212 Brackets: (), [], {}
213 213 Quotes: '', \"\"
214 214 """,
215 215 ).tag(config=True)
216 216
217 217 mouse_support = Bool(False,
218 218 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
219 219 ).tag(config=True)
220 220
221 221 # We don't load the list of styles for the help string, because loading
222 222 # Pygments plugins takes time and can cause unexpected errors.
223 223 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
224 224 help="""The name or class of a Pygments style to use for syntax
225 225 highlighting. To see available styles, run `pygmentize -L styles`."""
226 226 ).tag(config=True)
227 227
228 228 @validate('editing_mode')
229 229 def _validate_editing_mode(self, proposal):
230 230 if proposal['value'].lower() == 'vim':
231 231 proposal['value']= 'vi'
232 232 elif proposal['value'].lower() == 'default':
233 233 proposal['value']= 'emacs'
234 234
235 235 if hasattr(EditingMode, proposal['value'].upper()):
236 236 return proposal['value'].lower()
237 237
238 238 return self.editing_mode
239 239
240 240
241 241 @observe('editing_mode')
242 242 def _editing_mode(self, change):
243 243 if self.pt_app:
244 244 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
245 245
246 246 def _set_formatter(self, formatter):
247 247 if formatter is None:
248 248 self.reformat_handler = lambda x:x
249 249 elif formatter == 'black':
250 250 self.reformat_handler = black_reformat_handler
251 251 elif formatter == "yapf":
252 252 self.reformat_handler = yapf_reformat_handler
253 253 else:
254 254 raise ValueError
255 255
256 256 @observe("autoformatter")
257 257 def _autoformatter_changed(self, change):
258 258 formatter = change.new
259 259 self._set_formatter(formatter)
260 260
261 261 @observe('highlighting_style')
262 262 @observe('colors')
263 263 def _highlighting_style_changed(self, change):
264 264 self.refresh_style()
265 265
266 266 def refresh_style(self):
267 267 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
268 268
269 269
270 270 highlighting_style_overrides = Dict(
271 271 help="Override highlighting format for specific tokens"
272 272 ).tag(config=True)
273 273
274 274 true_color = Bool(False,
275 275 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
276 276 If your terminal supports true color, the following command should
277 277 print ``TRUECOLOR`` in orange::
278 278
279 279 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
280 280 """,
281 281 ).tag(config=True)
282 282
283 283 editor = Unicode(get_default_editor(),
284 284 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
285 285 ).tag(config=True)
286 286
287 287 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
288 288
289 289 prompts = Instance(Prompts)
290 290
291 291 @default('prompts')
292 292 def _prompts_default(self):
293 293 return self.prompts_class(self)
294 294
295 295 # @observe('prompts')
296 296 # def _(self, change):
297 297 # self._update_layout()
298 298
299 299 @default('displayhook_class')
300 300 def _displayhook_class_default(self):
301 301 return RichPromptDisplayHook
302 302
303 303 term_title = Bool(True,
304 304 help="Automatically set the terminal title"
305 305 ).tag(config=True)
306 306
307 307 term_title_format = Unicode("IPython: {cwd}",
308 308 help="Customize the terminal title format. This is a python format string. " +
309 309 "Available substitutions are: {cwd}."
310 310 ).tag(config=True)
311 311
312 312 display_completions = Enum(('column', 'multicolumn','readlinelike'),
313 313 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
314 314 "'readlinelike'. These options are for `prompt_toolkit`, see "
315 315 "`prompt_toolkit` documentation for more information."
316 316 ),
317 317 default_value='multicolumn').tag(config=True)
318 318
319 319 highlight_matching_brackets = Bool(True,
320 320 help="Highlight matching brackets.",
321 321 ).tag(config=True)
322 322
323 323 extra_open_editor_shortcuts = Bool(False,
324 324 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
325 325 "This is in addition to the F2 binding, which is always enabled."
326 326 ).tag(config=True)
327 327
328 328 handle_return = Any(None,
329 329 help="Provide an alternative handler to be called when the user presses "
330 330 "Return. This is an advanced option intended for debugging, which "
331 331 "may be changed or removed in later releases."
332 332 ).tag(config=True)
333 333
334 334 enable_history_search = Bool(True,
335 335 help="Allows to enable/disable the prompt toolkit history search"
336 336 ).tag(config=True)
337 337
338 338 autosuggestions_provider = Unicode(
339 339 "AutoSuggestFromHistory",
340 340 help="Specifies from which source automatic suggestions are provided. "
341 341 "Can be set to `'AutoSuggestFromHistory`' or `None` to disable"
342 342 "automatic suggestions. Default is `'AutoSuggestFromHistory`'.",
343 343 allow_none=True,
344 344 ).tag(config=True)
345 345
346 346 def _set_autosuggestions(self, provider):
347 347 if provider is None:
348 348 self.auto_suggest = None
349 349 elif provider == "AutoSuggestFromHistory":
350 350 self.auto_suggest = AutoSuggestFromHistory()
351 351 else:
352 352 raise ValueError("No valid provider.")
353 353 if self.pt_app:
354 354 self.pt_app.auto_suggest = self.auto_suggest
355 355
356 356 @observe("autosuggestions_provider")
357 357 def _autosuggestions_provider_changed(self, change):
358 358 provider = change.new
359 359 self._set_autosuggestions(provider)
360 360
361 361 prompt_includes_vi_mode = Bool(True,
362 362 help="Display the current vi mode (when using vi editing mode)."
363 363 ).tag(config=True)
364 364
365 365 @observe('term_title')
366 366 def init_term_title(self, change=None):
367 367 # Enable or disable the terminal title.
368 368 if self.term_title:
369 369 toggle_set_term_title(True)
370 370 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
371 371 else:
372 372 toggle_set_term_title(False)
373 373
374 374 def restore_term_title(self):
375 375 if self.term_title:
376 376 restore_term_title()
377 377
378 378 def init_display_formatter(self):
379 379 super(TerminalInteractiveShell, self).init_display_formatter()
380 380 # terminal only supports plain text
381 381 self.display_formatter.active_types = ["text/plain"]
382 382
383 383 def init_prompt_toolkit_cli(self):
384 384 if self.simple_prompt:
385 385 # Fall back to plain non-interactive output for tests.
386 386 # This is very limited.
387 387 def prompt():
388 388 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
389 389 lines = [input(prompt_text)]
390 390 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
391 391 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
392 392 lines.append( input(prompt_continuation) )
393 393 return '\n'.join(lines)
394 394 self.prompt_for_code = prompt
395 395 return
396 396
397 397 # Set up keyboard shortcuts
398 398 key_bindings = create_ipython_shortcuts(self)
399 399
400 400 # Pre-populate history from IPython's history database
401 401 history = InMemoryHistory()
402 402 last_cell = u""
403 403 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
404 404 include_latest=True):
405 405 # Ignore blank lines and consecutive duplicates
406 406 cell = cell.rstrip()
407 407 if cell and (cell != last_cell):
408 408 history.append_string(cell)
409 409 last_cell = cell
410 410
411 411 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
412 412 self.style = DynamicStyle(lambda: self._style)
413 413
414 414 editing_mode = getattr(EditingMode, self.editing_mode.upper())
415 415
416 416 self.pt_loop = asyncio.new_event_loop()
417 417 self.pt_app = PromptSession(
418 418 auto_suggest=self.auto_suggest,
419 419 editing_mode=editing_mode,
420 420 key_bindings=key_bindings,
421 421 history=history,
422 422 completer=IPythonPTCompleter(shell=self),
423 423 enable_history_search=self.enable_history_search,
424 424 style=self.style,
425 425 include_default_pygments_style=False,
426 426 mouse_support=self.mouse_support,
427 427 enable_open_in_editor=self.extra_open_editor_shortcuts,
428 428 color_depth=self.color_depth,
429 429 tempfile_suffix=".py",
430 430 **self._extra_prompt_options()
431 431 )
432 432
433 433 def _make_style_from_name_or_cls(self, name_or_cls):
434 434 """
435 435 Small wrapper that make an IPython compatible style from a style name
436 436
437 437 We need that to add style for prompt ... etc.
438 438 """
439 439 style_overrides = {}
440 440 if name_or_cls == 'legacy':
441 441 legacy = self.colors.lower()
442 442 if legacy == 'linux':
443 443 style_cls = get_style_by_name('monokai')
444 444 style_overrides = _style_overrides_linux
445 445 elif legacy == 'lightbg':
446 446 style_overrides = _style_overrides_light_bg
447 447 style_cls = get_style_by_name('pastie')
448 448 elif legacy == 'neutral':
449 449 # The default theme needs to be visible on both a dark background
450 450 # and a light background, because we can't tell what the terminal
451 451 # looks like. These tweaks to the default theme help with that.
452 452 style_cls = get_style_by_name('default')
453 453 style_overrides.update({
454 454 Token.Number: '#ansigreen',
455 455 Token.Operator: 'noinherit',
456 456 Token.String: '#ansiyellow',
457 457 Token.Name.Function: '#ansiblue',
458 458 Token.Name.Class: 'bold #ansiblue',
459 459 Token.Name.Namespace: 'bold #ansiblue',
460 460 Token.Name.Variable.Magic: '#ansiblue',
461 461 Token.Prompt: '#ansigreen',
462 462 Token.PromptNum: '#ansibrightgreen bold',
463 463 Token.OutPrompt: '#ansired',
464 464 Token.OutPromptNum: '#ansibrightred bold',
465 465 })
466 466
467 467 # Hack: Due to limited color support on the Windows console
468 468 # the prompt colors will be wrong without this
469 469 if os.name == 'nt':
470 470 style_overrides.update({
471 471 Token.Prompt: '#ansidarkgreen',
472 472 Token.PromptNum: '#ansigreen bold',
473 473 Token.OutPrompt: '#ansidarkred',
474 474 Token.OutPromptNum: '#ansired bold',
475 475 })
476 476 elif legacy =='nocolor':
477 477 style_cls=_NoStyle
478 478 style_overrides = {}
479 479 else :
480 480 raise ValueError('Got unknown colors: ', legacy)
481 481 else :
482 482 if isinstance(name_or_cls, str):
483 483 style_cls = get_style_by_name(name_or_cls)
484 484 else:
485 485 style_cls = name_or_cls
486 486 style_overrides = {
487 487 Token.Prompt: '#ansigreen',
488 488 Token.PromptNum: '#ansibrightgreen bold',
489 489 Token.OutPrompt: '#ansired',
490 490 Token.OutPromptNum: '#ansibrightred bold',
491 491 }
492 492 style_overrides.update(self.highlighting_style_overrides)
493 493 style = merge_styles([
494 494 style_from_pygments_cls(style_cls),
495 495 style_from_pygments_dict(style_overrides),
496 496 ])
497 497
498 498 return style
499 499
500 500 @property
501 501 def pt_complete_style(self):
502 502 return {
503 503 'multicolumn': CompleteStyle.MULTI_COLUMN,
504 504 'column': CompleteStyle.COLUMN,
505 505 'readlinelike': CompleteStyle.READLINE_LIKE,
506 506 }[self.display_completions]
507 507
508 508 @property
509 509 def color_depth(self):
510 510 return (ColorDepth.TRUE_COLOR if self.true_color else None)
511 511
512 512 def _extra_prompt_options(self):
513 513 """
514 514 Return the current layout option for the current Terminal InteractiveShell
515 515 """
516 516 def get_message():
517 517 return PygmentsTokens(self.prompts.in_prompt_tokens())
518 518
519 519 if self.editing_mode == 'emacs':
520 520 # with emacs mode the prompt is (usually) static, so we call only
521 521 # the function once. With VI mode it can toggle between [ins] and
522 522 # [nor] so we can't precompute.
523 523 # here I'm going to favor the default keybinding which almost
524 524 # everybody uses to decrease CPU usage.
525 525 # if we have issues with users with custom Prompts we can see how to
526 526 # work around this.
527 527 get_message = get_message()
528 528
529 529 options = {
530 530 'complete_in_thread': False,
531 531 'lexer':IPythonPTLexer(),
532 532 'reserve_space_for_menu':self.space_for_menu,
533 533 'message': get_message,
534 534 'prompt_continuation': (
535 535 lambda width, lineno, is_soft_wrap:
536 536 PygmentsTokens(self.prompts.continuation_prompt_tokens(width))),
537 537 'multiline': True,
538 538 'complete_style': self.pt_complete_style,
539 539
540 540 # Highlight matching brackets, but only when this setting is
541 541 # enabled, and only when the DEFAULT_BUFFER has the focus.
542 542 'input_processors': [ConditionalProcessor(
543 543 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
544 544 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
545 545 Condition(lambda: self.highlight_matching_brackets))],
546 546 }
547 547 if not PTK3:
548 548 options['inputhook'] = self.inputhook
549 549
550 550 return options
551 551
552 552 def prompt_for_code(self):
553 553 if self.rl_next_input:
554 554 default = self.rl_next_input
555 555 self.rl_next_input = None
556 556 else:
557 557 default = ''
558 558
559 559 # In order to make sure that asyncio code written in the
560 560 # interactive shell doesn't interfere with the prompt, we run the
561 561 # prompt in a different event loop.
562 562 # If we don't do this, people could spawn coroutine with a
563 563 # while/true inside which will freeze the prompt.
564 564
565 565 policy = asyncio.get_event_loop_policy()
566 566 old_loop = get_asyncio_loop()
567 567
568 568 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
569 569 # to get the current event loop.
570 570 # This will probably be replaced by an attribute or input argument,
571 571 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
572 572 if old_loop is not self.pt_loop:
573 573 policy.set_event_loop(self.pt_loop)
574 574 try:
575 575 with patch_stdout(raw=True):
576 576 text = self.pt_app.prompt(
577 577 default=default,
578 578 **self._extra_prompt_options())
579 579 finally:
580 580 # Restore the original event loop.
581 581 if old_loop is not None and old_loop is not self.pt_loop:
582 582 policy.set_event_loop(old_loop)
583 583
584 584 return text
585 585
586 586 def enable_win_unicode_console(self):
587 587 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
588 588 # console by default, so WUC shouldn't be needed.
589 589 from warnings import warn
590 590 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
591 591 DeprecationWarning,
592 592 stacklevel=2)
593 593
594 594 def init_io(self):
595 595 if sys.platform not in {'win32', 'cli'}:
596 596 return
597 597
598 598 import colorama
599 599 colorama.init()
600 600
601 601 def init_magics(self):
602 602 super(TerminalInteractiveShell, self).init_magics()
603 603 self.register_magics(TerminalMagics)
604 604
605 605 def init_alias(self):
606 606 # The parent class defines aliases that can be safely used with any
607 607 # frontend.
608 608 super(TerminalInteractiveShell, self).init_alias()
609 609
610 610 # Now define aliases that only make sense on the terminal, because they
611 611 # need direct access to the console in a way that we can't emulate in
612 612 # GUI or web frontend
613 613 if os.name == 'posix':
614 614 for cmd in ('clear', 'more', 'less', 'man'):
615 615 self.alias_manager.soft_define_alias(cmd, cmd)
616 616
617 617
618 618 def __init__(self, *args, **kwargs):
619 619 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
620 620 self._set_autosuggestions(self.autosuggestions_provider)
621 621 self.init_prompt_toolkit_cli()
622 622 self.init_term_title()
623 623 self.keep_running = True
624 624 self._set_formatter(self.autoformatter)
625 625
626 626
627 627 def ask_exit(self):
628 628 self.keep_running = False
629 629
630 630 rl_next_input = None
631 631
632 632 def interact(self):
633 633 self.keep_running = True
634 634 while self.keep_running:
635 635 print(self.separate_in, end='')
636 636
637 637 try:
638 638 code = self.prompt_for_code()
639 639 except EOFError:
640 640 if (not self.confirm_exit) \
641 641 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
642 642 self.ask_exit()
643 643
644 644 else:
645 645 if code:
646 646 self.run_cell(code, store_history=True)
647 647
648 648 def mainloop(self):
649 649 # An extra layer of protection in case someone mashing Ctrl-C breaks
650 650 # out of our internal code.
651 651 while True:
652 652 try:
653 653 self.interact()
654 654 break
655 655 except KeyboardInterrupt as e:
656 656 print("\n%s escaped interact()\n" % type(e).__name__)
657 657 finally:
658 658 # An interrupt during the eventloop will mess up the
659 659 # internal state of the prompt_toolkit library.
660 660 # Stopping the eventloop fixes this, see
661 661 # https://github.com/ipython/ipython/pull/9867
662 662 if hasattr(self, '_eventloop'):
663 663 self._eventloop.stop()
664 664
665 665 self.restore_term_title()
666 666
667 667 # try to call some at-exit operation optimistically as some things can't
668 668 # be done during interpreter shutdown. this is technically inaccurate as
669 669 # this make mainlool not re-callable, but that should be a rare if not
670 670 # in existent use case.
671 671
672 672 self._atexit_once()
673 673
674 674
675 675 _inputhook = None
676 676 def inputhook(self, context):
677 677 if self._inputhook is not None:
678 678 self._inputhook(context)
679 679
680 680 active_eventloop = None
681 681 def enable_gui(self, gui=None):
682 682 if gui and (gui != 'inline') :
683 683 self.active_eventloop, self._inputhook =\
684 684 get_inputhook_name_and_func(gui)
685 685 else:
686 686 self.active_eventloop = self._inputhook = None
687 687
688 688 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
689 689 # this inputhook.
690 690 if PTK3:
691 691 import asyncio
692 692 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
693 693
694 694 if gui == 'asyncio':
695 695 # When we integrate the asyncio event loop, run the UI in the
696 696 # same event loop as the rest of the code. don't use an actual
697 697 # input hook. (Asyncio is not made for nesting event loops.)
698 698 self.pt_loop = get_asyncio_loop()
699 699
700 700 elif self._inputhook:
701 701 # If an inputhook was set, create a new asyncio event loop with
702 702 # this inputhook for the prompt.
703 703 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
704 704 else:
705 705 # When there's no inputhook, run the prompt in a separate
706 706 # asyncio event loop.
707 707 self.pt_loop = asyncio.new_event_loop()
708 708
709 709 # Run !system commands directly, not through pipes, so terminal programs
710 710 # work correctly.
711 711 system = InteractiveShell.system_raw
712 712
713 713 def auto_rewrite_input(self, cmd):
714 714 """Overridden from the parent class to use fancy rewriting prompt"""
715 715 if not self.show_rewritten_input:
716 716 return
717 717
718 718 tokens = self.prompts.rewrite_prompt_tokens()
719 719 if self.pt_app:
720 720 print_formatted_text(PygmentsTokens(tokens), end='',
721 721 style=self.pt_app.app.style)
722 722 print(cmd)
723 723 else:
724 724 prompt = ''.join(s for t, s in tokens)
725 725 print(prompt, cmd, sep='')
726 726
727 727 _prompts_before = None
728 728 def switch_doctest_mode(self, mode):
729 729 """Switch prompts to classic for %doctest_mode"""
730 730 if mode:
731 731 self._prompts_before = self.prompts
732 732 self.prompts = ClassicPrompts(self)
733 733 elif self._prompts_before:
734 734 self.prompts = self._prompts_before
735 735 self._prompts_before = None
736 736 # self._update_layout()
737 737
738 738
739 739 InteractiveShellABC.register(TerminalInteractiveShell)
740 740
741 741 if __name__ == '__main__':
742 742 TerminalInteractiveShell.instance().interact()
@@ -1,941 +1,940 b''
1 1 ============
2 2 8.x Series
3 3 ============
4 4
5 5
6 6 IPython 8.0.1 (CVE-2022-21699)
7 7 ------------------------------
8 8
9 9 IPython 8.0.1, 7.31.1 and 5.11 are security releases that change some default
10 10 values in order to prevent potential Execution with Unnecessary Privileges.
11 11
12 12 Almost all version of IPython looks for configuration and profiles in current
13 13 working directory. Since IPython was developed before pip and environments
14 14 existed it was used a convenient way to load code/packages in a project
15 15 dependant way.
16 16
17 17 In 2022, it is not necessary anymore, and can lead to confusing behavior where
18 18 for example cloning a repository and starting IPython or loading a notebook from
19 19 any Jupyter-Compatible interface that has ipython set as a kernel can lead to
20 20 code execution.
21 21
22 22
23 23 I did not find any standard way for packaged to advertise CVEs they fix, I'm
24 24 thus trying to add a ``__patched_cves__`` attribute to the IPython module that
25 25 list the CVEs that should have been fixed. This attribute is informational only
26 26 as if a executable has a flaw, this value can always be changed by an attacker.
27 27
28 28 .. code::
29 29
30 30 In [1]: import IPython
31 31
32 32 In [2]: IPython.__patched_cves__
33 33 Out[2]: {'CVE-2022-21699'}
34 34
35 35 In [3]: 'CVE-2022-21699' in IPython.__patched_cves__
36 36 Out[3]: True
37 37
38 38 Thus starting with this version:
39 39
40 40 - The current working directory is not searched anymore for profiles or
41 41 configurations files.
42 42 - Added a ``__patched_cves__`` attribute (set of strings) to IPython module that contain
43 43 the list of fixed CVE. This is informational only.
44 44
45 45 Further details can be read on the `GitHub Advisory <https://github.com/ipython/ipython/security/advisories/GHSA-pq7m-3gw7-gq5x>`__
46 46
47 47
48 48
49 49 IPython 8.0
50 50 -----------
51 51
52 52 IPython 8.0 is bringing a large number of new features and improvements to both the
53 53 user of the terminal and of the kernel via Jupyter. The removal of compatibility
54 54 with older version of Python is also the opportunity to do a couple of
55 55 performance improvements in particular with respect to startup time.
56 56 The 8.x branch started diverging from its predecessor around IPython 7.12
57 57 (January 2020).
58 58
59 59 This release contains 250+ pull requests, in addition to many of the features
60 60 and backports that have made it to the 7.x branch. Please see the
61 61 `8.0 milestone <https://github.com/ipython/ipython/milestone/73?closed=1>`__ for the full list of pull requests.
62 62
63 63 Please feel free to send pull requests to updates those notes after release,
64 64 I have likely forgotten a few things reviewing 250+ PRs.
65 65
66 66 Dependencies changes/downstream packaging
67 67 -----------------------------------------
68 68
69 69 Most of our building steps have been changed to be (mostly) declarative
70 70 and follow PEP 517. We are trying to completely remove ``setup.py`` (:ghpull:`13238`) and are
71 71 looking for help to do so.
72 72
73 73 - minimum supported ``traitlets`` version is now 5+
74 74 - we now require ``stack_data``
75 75 - minimal Python is now 3.8
76 76 - ``nose`` is not a testing requirement anymore
77 77 - ``pytest`` replaces nose.
78 78 - ``iptest``/``iptest3`` cli entrypoints do not exists anymore.
79 79 - minimum officially support ``numpy`` version has been bumped, but this should
80 80 not have much effect on packaging.
81 81
82 82
83 83 Deprecation and removal
84 84 -----------------------
85 85
86 86 We removed almost all features, arguments, functions, and modules that were
87 87 marked as deprecated between IPython 1.0 and 5.0. As a reminder, 5.0 was released
88 88 in 2016, and 1.0 in 2013. Last release of the 5 branch was 5.10.0, in May 2020.
89 89 The few remaining deprecated features we left have better deprecation warnings
90 90 or have been turned into explicit errors for better error messages.
91 91
92 92 I will use this occasion to add the following requests to anyone emitting a
93 93 deprecation warning:
94 94
95 95 - Please add at least ``stacklevel=2`` so that the warning is emitted into the
96 96 caller context, and not the callee one.
97 97 - Please add **since which version** something is deprecated.
98 98
99 99 As a side note, it is much easier to conditionally compare version
100 100 numbers rather than using ``try/except`` when functionality changes with a version.
101 101
102 102 I won't list all the removed features here, but modules like ``IPython.kernel``,
103 103 which was just a shim module around ``ipykernel`` for the past 8 years, have been
104 104 removed, and so many other similar things that pre-date the name **Jupyter**
105 105 itself.
106 106
107 107 We no longer need to add ``IPython.extensions`` to the PYTHONPATH because that is being
108 108 handled by ``load_extension``.
109 109
110 110 We are also removing ``Cythonmagic``, ``sympyprinting`` and ``rmagic`` as they are now in
111 111 other packages and no longer need to be inside IPython.
112 112
113 113
114 114 Documentation
115 115 -------------
116 116
117 117 The majority of our docstrings have now been reformatted and automatically fixed by
118 118 the experimental `VΓ©lin <https://pypi.org/project/velin/>`_ project to conform
119 119 to numpydoc.
120 120
121 121 Type annotations
122 122 ----------------
123 123
124 124 While IPython itself is highly dynamic and can't be completely typed, many of
125 125 the functions now have type annotations, and part of the codebase is now checked
126 126 by mypy.
127 127
128 128
129 129 Featured changes
130 130 ----------------
131 131
132 132 Here is a features list of changes in IPython 8.0. This is of course non-exhaustive.
133 133 Please note as well that many features have been added in the 7.x branch as well
134 134 (and hence why you want to read the 7.x what's new notes), in particular
135 135 features contributed by QuantStack (with respect to debugger protocol and Xeus
136 136 Python), as well as many debugger features that I was pleased to implement as
137 137 part of my work at QuanSight and sponsored by DE Shaw.
138 138
139 139 Traceback improvements
140 140 ~~~~~~~~~~~~~~~~~~~~~~
141 141
142 142 Previously, error tracebacks for errors happening in code cells were showing a
143 143 hash, the one used for compiling the Python AST::
144 144
145 145 In [1]: def foo():
146 146 ...: return 3 / 0
147 147 ...:
148 148
149 149 In [2]: foo()
150 150 ---------------------------------------------------------------------------
151 151 ZeroDivisionError Traceback (most recent call last)
152 152 <ipython-input-2-c19b6d9633cf> in <module>
153 153 ----> 1 foo()
154 154
155 155 <ipython-input-1-1595a74c32d5> in foo()
156 156 1 def foo():
157 157 ----> 2 return 3 / 0
158 158 3
159 159
160 160 ZeroDivisionError: division by zero
161 161
162 162 The error traceback is now correctly formatted, showing the cell number in which the error happened::
163 163
164 164 In [1]: def foo():
165 165 ...: return 3 / 0
166 166 ...:
167 167
168 168 Input In [2]: foo()
169 169 ---------------------------------------------------------------------------
170 170 ZeroDivisionError Traceback (most recent call last)
171 171 input In [2], in <module>
172 172 ----> 1 foo()
173 173
174 174 Input In [1], in foo()
175 175 1 def foo():
176 176 ----> 2 return 3 / 0
177 177
178 178 ZeroDivisionError: division by zero
179 179
180 180 The ``stack_data`` package has been integrated, which provides smarter information in the traceback;
181 181 in particular it will highlight the AST node where an error occurs which can help to quickly narrow down errors.
182 182
183 183 For example in the following snippet::
184 184
185 185 def foo(i):
186 186 x = [[[0]]]
187 187 return x[0][i][0]
188 188
189 189
190 190 def bar():
191 191 return foo(0) + foo(
192 192 1
193 193 ) + foo(2)
194 194
195 195
196 196 calling ``bar()`` would raise an ``IndexError`` on the return line of ``foo``,
197 197 and IPython 8.0 is capable of telling you where the index error occurs::
198 198
199 199
200 200 IndexError
201 201 Input In [2], in <module>
202 202 ----> 1 bar()
203 203 ^^^^^
204 204
205 205 Input In [1], in bar()
206 206 6 def bar():
207 207 ----> 7 return foo(0) + foo(
208 208 ^^^^
209 209 8 1
210 210 ^^^^^^^^
211 211 9 ) + foo(2)
212 212 ^^^^
213 213
214 214 Input In [1], in foo(i)
215 215 1 def foo(i):
216 216 2 x = [[[0]]]
217 217 ----> 3 return x[0][i][0]
218 218 ^^^^^^^
219 219
220 220 The corresponding locations marked here with ``^`` will show up highlighted in
221 221 the terminal and notebooks.
222 222
223 223 Finally, a colon ``::`` and line number is appended after a filename in
224 224 traceback::
225 225
226 226
227 227 ZeroDivisionError Traceback (most recent call last)
228 228 File ~/error.py:4, in <module>
229 229 1 def f():
230 230 2 1/0
231 231 ----> 4 f()
232 232
233 233 File ~/error.py:2, in f()
234 234 1 def f():
235 235 ----> 2 1/0
236 236
237 237 Many terminals and editors have integrations enabling you to directly jump to the
238 238 relevant file/line when this syntax is used, so this small addition may have a high
239 239 impact on productivity.
240 240
241 241
242 242 Autosuggestions
243 243 ~~~~~~~~~~~~~~~
244 244
245 245 Autosuggestion is a very useful feature available in `fish <https://fishshell.com/>`__, `zsh <https://en.wikipedia.org/wiki/Z_shell>`__, and `prompt-toolkit <https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#auto-suggestion>`__.
246 246
247 247 `Ptpython <https://github.com/prompt-toolkit/ptpython#ptpython>`__ allows users to enable this feature in
248 248 `ptpython/config.py <https://github.com/prompt-toolkit/ptpython/blob/master/examples/ptpython_config/config.py#L90>`__.
249 249
250 250 This feature allows users to accept autosuggestions with ctrl e, ctrl f,
251 251 or right arrow as described below.
252 252
253 253 1. Start ipython
254 254
255 255 .. image:: ../_images/8.0/auto_suggest_1_prompt_no_text.png
256 256
257 257 2. Run ``print("hello")``
258 258
259 259 .. image:: ../_images/8.0/auto_suggest_2_print_hello_suggest.png
260 260
261 261 3. start typing ``print`` again to see the autosuggestion
262 262
263 263 .. image:: ../_images/8.0/auto_suggest_3_print_hello_suggest.png
264 264
265 265 4. Press ``ctrl-f``, or ``ctrl-e``, or ``right-arrow`` to accept the suggestion
266 266
267 267 .. image:: ../_images/8.0/auto_suggest_4_print_hello.png
268 268
269 269 You can also complete word by word:
270 270
271 271 1. Run ``def say_hello(): print("hello")``
272 272
273 273 .. image:: ../_images/8.0/auto_suggest_second_prompt.png
274 274
275 275 2. Start typing the first letter if ``def`` to see the autosuggestion
276 276
277 277 .. image:: ../_images/8.0/auto_suggest_d_phantom.png
278 278
279 279 3. Press ``alt-f`` (or ``escape`` followed by ``f``), to accept the first word of the suggestion
280 280
281 281 .. image:: ../_images/8.0/auto_suggest_def_phantom.png
282 282
283 283 Importantly, this feature does not interfere with tab completion:
284 284
285 285 1. After running ``def say_hello(): print("hello")``, press d
286 286
287 287 .. image:: ../_images/8.0/auto_suggest_d_phantom.png
288 288
289 289 2. Press Tab to start tab completion
290 290
291 291 .. image:: ../_images/8.0/auto_suggest_d_completions.png
292 292
293 293 3A. Press Tab again to select the first option
294 294
295 295 .. image:: ../_images/8.0/auto_suggest_def_completions.png
296 296
297 297 3B. Press ``alt f`` (``escape``, ``f``) to accept to accept the first word of the suggestion
298 298
299 299 .. image:: ../_images/8.0/auto_suggest_def_phantom.png
300 300
301 301 3C. Press ``ctrl-f`` or ``ctrl-e`` to accept the entire suggestion
302 302
303 303 .. image:: ../_images/8.0/auto_suggest_match_parens.png
304 304
305 305
306 306 Currently, autosuggestions are only shown in the emacs or vi insert editing modes:
307 307
308 308 - The ctrl e, ctrl f, and alt f shortcuts work by default in emacs mode.
309 309 - To use these shortcuts in vi insert mode, you will have to create `custom keybindings in your config.py <https://github.com/mskar/setup/commit/2892fcee46f9f80ef7788f0749edc99daccc52f4/>`__.
310 310
311 311
312 312 Show pinfo information in ipdb using "?" and "??"
313 313 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
314 314
315 315 In IPDB, it is now possible to show the information about an object using "?"
316 316 and "??", in much the same way that it can be done when using the IPython prompt::
317 317
318 318 ipdb> partial?
319 319 Init signature: partial(self, /, *args, **kwargs)
320 320 Docstring:
321 321 partial(func, *args, **keywords) - new function with partial application
322 322 of the given arguments and keywords.
323 323 File: ~/.pyenv/versions/3.8.6/lib/python3.8/functools.py
324 324 Type: type
325 325 Subclasses:
326 326
327 327 Previously, ``pinfo`` or ``pinfo2`` command had to be used for this purpose.
328 328
329 329
330 330 Autoreload 3 feature
331 331 ~~~~~~~~~~~~~~~~~~~~
332 332
333 333 Example: When an IPython session is run with the 'autoreload' extension loaded,
334 334 you will now have the option '3' to select, which means the following:
335 335
336 336 1. replicate all functionality from option 2
337 337 2. autoload all new funcs/classes/enums/globals from the module when they are added
338 338 3. autoload all newly imported funcs/classes/enums/globals from external modules
339 339
340 340 Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload``.
341 341
342 342 For more information please see the following unit test : ``extensions/tests/test_autoreload.py:test_autoload_newly_added_objects``
343 343
344 344 Auto formatting with black in the CLI
345 345 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
346 346
347 If ``black`` is installed in the same environment as IPython, terminal IPython
348 will now *by default* reformat the code in the CLI when possible. You can
349 disable this with ``--TerminalInteractiveShell.autoformatter=None``.
350
351 347 This feature was present in 7.x, but disabled by default.
352 348
349 In 8.0, input was automatically reformatted with Black when black was installed.
350 This feature has been reverted for the time being.
351 You can re-enable it by setting ``TerminalInteractiveShell.autoformatter`` to ``"black"``
353 352
354 353 History Range Glob feature
355 354 ~~~~~~~~~~~~~~~~~~~~~~~~~~
356 355
357 356 Previously, when using ``%history``, users could specify either
358 357 a range of sessions and lines, for example:
359 358
360 359 .. code-block:: python
361 360
362 361 ~8/1-~6/5 # see history from the first line of 8 sessions ago,
363 362 # to the fifth line of 6 sessions ago.``
364 363
365 364 Or users could specify a glob pattern:
366 365
367 366 .. code-block:: python
368 367
369 368 -g <pattern> # glob ALL history for the specified pattern.
370 369
371 370 However users could *not* specify both.
372 371
373 372 If a user *did* specify both a range and a glob pattern,
374 373 then the glob pattern would be used (globbing *all* history) *and the range would be ignored*.
375 374
376 375 With this enhancement, if a user specifies both a range and a glob pattern, then the glob pattern will be applied to the specified range of history.
377 376
378 377 Don't start a multi-line cell with sunken parenthesis
379 378 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
380 379
381 380 From now on, IPython will not ask for the next line of input when given a single
382 381 line with more closing than opening brackets. For example, this means that if
383 382 you (mis)type ``]]`` instead of ``[]``, a ``SyntaxError`` will show up, instead of
384 383 the ``...:`` prompt continuation.
385 384
386 385 IPython shell for ipdb interact
387 386 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
388 387
389 388 The ipdb ``interact`` starts an IPython shell instead of Python's built-in ``code.interact()``.
390 389
391 390 Automatic Vi prompt stripping
392 391 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
393 392
394 393 When pasting code into IPython, it will strip the leading prompt characters if
395 394 there are any. For example, you can paste the following code into the console -
396 395 it will still work, even though each line is prefixed with prompts (`In`,
397 396 `Out`)::
398 397
399 398 In [1]: 2 * 2 == 4
400 399 Out[1]: True
401 400
402 401 In [2]: print("This still works as pasted")
403 402
404 403
405 404 Previously, this was not the case for the Vi-mode prompts::
406 405
407 406 In [1]: [ins] In [13]: 2 * 2 == 4
408 407 ...: Out[13]: True
409 408 ...:
410 409 File "<ipython-input-1-727bb88eaf33>", line 1
411 410 [ins] In [13]: 2 * 2 == 4
412 411 ^
413 412 SyntaxError: invalid syntax
414 413
415 414 This is now fixed, and Vi prompt prefixes - ``[ins]`` and ``[nav]`` - are
416 415 skipped just as the normal ``In`` would be.
417 416
418 417 IPython shell can be started in the Vi mode using ``ipython --TerminalInteractiveShell.editing_mode=vi``,
419 418 You should be able to change mode dynamically with ``%config TerminalInteractiveShell.editing_mode='vi'``
420 419
421 420 Empty History Ranges
422 421 ~~~~~~~~~~~~~~~~~~~~
423 422
424 423 A number of magics that take history ranges can now be used with an empty
425 424 range. These magics are:
426 425
427 426 * ``%save``
428 427 * ``%load``
429 428 * ``%pastebin``
430 429 * ``%pycat``
431 430
432 431 Using them this way will make them take the history of the current session up
433 432 to the point of the magic call (such that the magic itself will not be
434 433 included).
435 434
436 435 Therefore it is now possible to save the whole history to a file using
437 436 ``%save <filename>``, load and edit it using ``%load`` (makes for a nice usage
438 437 when followed with :kbd:`F2`), send it to `dpaste.org <http://dpast.org>`_ using
439 438 ``%pastebin``, or view the whole thing syntax-highlighted with a single
440 439 ``%pycat``.
441 440
442 441
443 442 Windows timing implementation: Switch to process_time
444 443 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
445 444 Timing on Windows, for example with ``%%time``, was changed from being based on ``time.perf_counter``
446 445 (which counted time even when the process was sleeping) to being based on ``time.process_time`` instead
447 446 (which only counts CPU time). This brings it closer to the behavior on Linux. See :ghpull:`12984`.
448 447
449 448 Miscellaneous
450 449 ~~~~~~~~~~~~~
451 450 - Non-text formatters are not disabled in the terminal, which should simplify
452 451 writing extensions displaying images or other mimetypes in supporting terminals.
453 452 :ghpull:`12315`
454 453 - It is now possible to automatically insert matching brackets in Terminal IPython using the
455 454 ``TerminalInteractiveShell.auto_match=True`` option. :ghpull:`12586`
456 455 - We are thinking of deprecating the current ``%%javascript`` magic in favor of a better replacement. See :ghpull:`13376`.
457 456 - ``~`` is now expanded when part of a path in most magics :ghpull:`13385`
458 457 - ``%/%%timeit`` magic now adds a comma every thousands to make reading a long number easier :ghpull:`13379`
459 458 - ``"info"`` messages can now be customised to hide some fields :ghpull:`13343`
460 459 - ``collections.UserList`` now pretty-prints :ghpull:`13320`
461 460 - The debugger now has a persistent history, which should make it less
462 461 annoying to retype commands :ghpull:`13246`
463 462 - ``!pip`` ``!conda`` ``!cd`` or ``!ls`` are likely doing the wrong thing. We
464 463 now warn users if they use one of those commands. :ghpull:`12954`
465 464 - Make ``%precision`` work for ``numpy.float64`` type :ghpull:`12902`
466 465
467 466 Re-added support for XDG config directories
468 467 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
469 468
470 469 XDG support through the years comes and goes. There is a tension between having
471 470 an identical location for configuration in all platforms versus having simple instructions.
472 471 After initial failures a couple of years ago, IPython was modified to automatically migrate XDG
473 472 config files back into ``~/.ipython``. That migration code has now been removed.
474 473 IPython now checks the XDG locations, so if you _manually_ move your config
475 474 files to your preferred location, IPython will not move them back.
476 475
477 476
478 477 Preparing for Python 3.10
479 478 -------------------------
480 479
481 480 To prepare for Python 3.10, we have started working on removing reliance and
482 481 any dependency that is not compatible with Python 3.10. This includes migrating our
483 482 test suite to pytest and starting to remove nose. This also means that the
484 483 ``iptest`` command is now gone and all testing is via pytest.
485 484
486 485 This was in large part thanks to the NumFOCUS Small Developer grant, which enabled us to
487 486 allocate \$4000 to hire `Nikita Kniazev (@Kojoley) <https://github.com/Kojoley>`_,
488 487 who did a fantastic job at updating our code base, migrating to pytest, pushing
489 488 our coverage, and fixing a large number of bugs. I highly recommend contacting
490 489 them if you need help with C++ and Python projects.
491 490
492 491 You can find all relevant issues and PRs with the SDG 2021 tag `<https://github.com/ipython/ipython/issues?q=label%3A%22Numfocus+SDG+2021%22+>`__
493 492
494 493 Removing support for older Python versions
495 494 ------------------------------------------
496 495
497 496
498 497 We are removing support for Python up through 3.7, allowing internal code to use the more
499 498 efficient ``pathlib`` and to make better use of type annotations.
500 499
501 500 .. image:: ../_images/8.0/pathlib_pathlib_everywhere.jpg
502 501 :alt: "Meme image of Toy Story with Woody and Buzz, with the text 'pathlib, pathlib everywhere'"
503 502
504 503
505 504 We had about 34 PRs only to update some logic to update some functions from managing strings to
506 505 using Pathlib.
507 506
508 507 The completer has also seen significant updates and now makes use of newer Jedi APIs,
509 508 offering faster and more reliable tab completion.
510 509
511 510 Misc Statistics
512 511 ---------------
513 512
514 513 Here are some numbers::
515 514
516 515 7.x: 296 files, 12561 blank lines, 20282 comments, 35142 line of code.
517 516 8.0: 252 files, 12053 blank lines, 19232 comments, 34505 line of code.
518 517
519 518 $ git diff --stat 7.x...master | tail -1
520 519 340 files changed, 13399 insertions(+), 12421 deletions(-)
521 520
522 521 We have commits from 162 authors, who contributed 1916 commits in 23 month, excluding merges (to not bias toward
523 522 maintainers pushing buttons).::
524 523
525 524 $ git shortlog -s --no-merges 7.x...master | sort -nr
526 525 535 Matthias Bussonnier
527 526 86 Nikita Kniazev
528 527 69 Blazej Michalik
529 528 49 Samuel Gaist
530 529 27 Itamar Turner-Trauring
531 530 18 Spas Kalaydzhisyki
532 531 17 Thomas Kluyver
533 532 17 Quentin Peter
534 533 17 James Morris
535 534 17 Artur Svistunov
536 535 15 Bart Skowron
537 536 14 Alex Hall
538 537 13 rushabh-v
539 538 13 Terry Davis
540 539 13 Benjamin Ragan-Kelley
541 540 8 martinRenou
542 541 8 farisachugthai
543 542 7 dswij
544 543 7 Gal B
545 544 7 Corentin Cadiou
546 545 6 yuji96
547 546 6 Martin Skarzynski
548 547 6 Justin Palmer
549 548 6 Daniel Goldfarb
550 549 6 Ben Greiner
551 550 5 Sammy Al Hashemi
552 551 5 Paul Ivanov
553 552 5 Inception95
554 553 5 Eyenpi
555 554 5 Douglas Blank
556 555 5 Coco Mishra
557 556 5 Bibo Hao
558 557 5 AndrΓ© A. Gomes
559 558 5 Ahmed Fasih
560 559 4 takuya fujiwara
561 560 4 palewire
562 561 4 Thomas A Caswell
563 562 4 Talley Lambert
564 563 4 Scott Sanderson
565 564 4 Ram Rachum
566 565 4 Nick Muoh
567 566 4 Nathan Goldbaum
568 567 4 Mithil Poojary
569 568 4 Michael T
570 569 4 Jakub Klus
571 570 4 Ian Castleden
572 571 4 Eli Rykoff
573 572 4 Ashwin Vishnu
574 573 3 谭九鼎
575 574 3 sleeping
576 575 3 Sylvain Corlay
577 576 3 Peter Corke
578 577 3 Paul Bissex
579 578 3 Matthew Feickert
580 579 3 Fernando Perez
581 580 3 Eric Wieser
582 581 3 Daniel Mietchen
583 582 3 Aditya Sathe
584 583 3 007vedant
585 584 2 rchiodo
586 585 2 nicolaslazo
587 586 2 luttik
588 587 2 gorogoroumaru
589 588 2 foobarbyte
590 589 2 bar-hen
591 590 2 Theo Ouzhinski
592 591 2 Strawkage
593 592 2 Samreen Zarroug
594 593 2 Pete Blois
595 594 2 Meysam Azad
596 595 2 Matthieu Ancellin
597 596 2 Mark Schmitz
598 597 2 Maor Kleinberger
599 598 2 MRCWirtz
600 599 2 Lumir Balhar
601 600 2 Julien Rabinow
602 601 2 Juan Luis Cano RodrΓ­guez
603 602 2 Joyce Er
604 603 2 Jakub
605 604 2 Faris A Chugthai
606 605 2 Ethan Madden
607 606 2 Dimitri Papadopoulos
608 607 2 Diego Fernandez
609 608 2 Daniel Shimon
610 609 2 Coco Bennett
611 610 2 Carlos Cordoba
612 611 2 Boyuan Liu
613 612 2 BaoGiang HoangVu
614 613 2 Augusto
615 614 2 Arthur Svistunov
616 615 2 Arthur Moreira
617 616 2 Ali Nabipour
618 617 2 Adam Hackbarth
619 618 1 richard
620 619 1 linar-jether
621 620 1 lbennett
622 621 1 juacrumar
623 622 1 gpotter2
624 623 1 digitalvirtuoso
625 624 1 dalthviz
626 625 1 Yonatan Goldschmidt
627 626 1 Tomasz KΕ‚oczko
628 627 1 Tobias Bengfort
629 628 1 Timur Kushukov
630 629 1 Thomas
631 630 1 Snir Broshi
632 631 1 Shao Yang Hong
633 632 1 Sanjana-03
634 633 1 Romulo Filho
635 634 1 Rodolfo Carvalho
636 635 1 Richard Shadrach
637 636 1 Reilly Tucker Siemens
638 637 1 Rakessh Roshan
639 638 1 Piers Titus van der Torren
640 639 1 PhanatosZou
641 640 1 Pavel Safronov
642 641 1 Paulo S. Costa
643 642 1 Paul McCarthy
644 643 1 NotWearingPants
645 644 1 Naelson Douglas
646 645 1 Michael Tiemann
647 646 1 Matt Wozniski
648 647 1 Markus Wageringel
649 648 1 Marcus Wirtz
650 649 1 Marcio Mazza
651 650 1 LumΓ­r 'Frenzy' Balhar
652 651 1 Lightyagami1
653 652 1 Leon Anavi
654 653 1 LeafyLi
655 654 1 L0uisJ0shua
656 655 1 Kyle Cutler
657 656 1 Krzysztof Cybulski
658 657 1 Kevin Kirsche
659 658 1 KIU Shueng Chuan
660 659 1 Jonathan Slenders
661 660 1 Jay Qi
662 661 1 Jake VanderPlas
663 662 1 Iwan Briquemont
664 663 1 Hussaina Begum Nandyala
665 664 1 Gordon Ball
666 665 1 Gabriel Simonetto
667 666 1 Frank Tobia
668 667 1 Erik
669 668 1 Elliott Sales de Andrade
670 669 1 Daniel Hahler
671 670 1 Dan Green-Leipciger
672 671 1 Dan Green
673 672 1 Damian Yurzola
674 673 1 Coon, Ethan T
675 674 1 Carol Willing
676 675 1 Brian Lee
677 676 1 Brendan Gerrity
678 677 1 Blake Griffin
679 678 1 Bastian Ebeling
680 679 1 Bartosz Telenczuk
681 680 1 Ankitsingh6299
682 681 1 Andrew Port
683 682 1 Andrew J. Hesford
684 683 1 Albert Zhang
685 684 1 Adam Johnson
686 685
687 686 This does not, of course, represent non-code contributions, for which we are also grateful.
688 687
689 688
690 689 API Changes using Frappuccino
691 690 -----------------------------
692 691
693 692 This is an experimental exhaustive API difference using `Frappuccino <https://pypi.org/project/frappuccino/>`_
694 693
695 694
696 695 The following items are new in IPython 8.0 ::
697 696
698 697 + IPython.core.async_helpers.get_asyncio_loop()
699 698 + IPython.core.completer.Dict
700 699 + IPython.core.completer.Pattern
701 700 + IPython.core.completer.Sequence
702 701 + IPython.core.completer.__skip_doctest__
703 702 + IPython.core.debugger.Pdb.precmd(self, line)
704 703 + IPython.core.debugger.__skip_doctest__
705 704 + IPython.core.display.__getattr__(name)
706 705 + IPython.core.display.warn
707 706 + IPython.core.display_functions
708 707 + IPython.core.display_functions.DisplayHandle
709 708 + IPython.core.display_functions.DisplayHandle.display(self, obj, **kwargs)
710 709 + IPython.core.display_functions.DisplayHandle.update(self, obj, **kwargs)
711 710 + IPython.core.display_functions.__all__
712 711 + IPython.core.display_functions.__builtins__
713 712 + IPython.core.display_functions.__cached__
714 713 + IPython.core.display_functions.__doc__
715 714 + IPython.core.display_functions.__file__
716 715 + IPython.core.display_functions.__loader__
717 716 + IPython.core.display_functions.__name__
718 717 + IPython.core.display_functions.__package__
719 718 + IPython.core.display_functions.__spec__
720 719 + IPython.core.display_functions.b2a_hex
721 720 + IPython.core.display_functions.clear_output(wait=False)
722 721 + IPython.core.display_functions.display(*objs, include='None', exclude='None', metadata='None', transient='None', display_id='None', raw=False, clear=False, **kwargs)
723 722 + IPython.core.display_functions.publish_display_data(data, metadata='None', source='<deprecated>', *, transient='None', **kwargs)
724 723 + IPython.core.display_functions.update_display(obj, *, display_id, **kwargs)
725 724 + IPython.core.extensions.BUILTINS_EXTS
726 725 + IPython.core.inputtransformer2.has_sunken_brackets(tokens)
727 726 + IPython.core.interactiveshell.Callable
728 727 + IPython.core.interactiveshell.__annotations__
729 728 + IPython.core.ultratb.List
730 729 + IPython.core.ultratb.Tuple
731 730 + IPython.lib.pretty.CallExpression
732 731 + IPython.lib.pretty.CallExpression.factory(name)
733 732 + IPython.lib.pretty.RawStringLiteral
734 733 + IPython.lib.pretty.RawText
735 734 + IPython.terminal.debugger.TerminalPdb.do_interact(self, arg)
736 735 + IPython.terminal.embed.Set
737 736
738 737 The following items have been removed (or moved to superclass)::
739 738
740 739 - IPython.core.application.BaseIPythonApplication.initialize_subcommand
741 740 - IPython.core.completer.Sentinel
742 741 - IPython.core.completer.skip_doctest
743 742 - IPython.core.debugger.Tracer
744 743 - IPython.core.display.DisplayHandle
745 744 - IPython.core.display.DisplayHandle.display
746 745 - IPython.core.display.DisplayHandle.update
747 746 - IPython.core.display.b2a_hex
748 747 - IPython.core.display.clear_output
749 748 - IPython.core.display.display
750 749 - IPython.core.display.publish_display_data
751 750 - IPython.core.display.update_display
752 751 - IPython.core.excolors.Deprec
753 752 - IPython.core.excolors.ExceptionColors
754 753 - IPython.core.history.warn
755 754 - IPython.core.hooks.late_startup_hook
756 755 - IPython.core.hooks.pre_run_code_hook
757 756 - IPython.core.hooks.shutdown_hook
758 757 - IPython.core.interactiveshell.InteractiveShell.init_deprecation_warnings
759 758 - IPython.core.interactiveshell.InteractiveShell.init_readline
760 759 - IPython.core.interactiveshell.InteractiveShell.write
761 760 - IPython.core.interactiveshell.InteractiveShell.write_err
762 761 - IPython.core.interactiveshell.get_default_colors
763 762 - IPython.core.interactiveshell.removed_co_newlocals
764 763 - IPython.core.magics.execution.ExecutionMagics.profile_missing_notice
765 764 - IPython.core.magics.script.PIPE
766 765 - IPython.core.prefilter.PrefilterManager.init_transformers
767 766 - IPython.core.release.classifiers
768 767 - IPython.core.release.description
769 768 - IPython.core.release.keywords
770 769 - IPython.core.release.long_description
771 770 - IPython.core.release.name
772 771 - IPython.core.release.platforms
773 772 - IPython.core.release.url
774 773 - IPython.core.ultratb.VerboseTB.format_records
775 774 - IPython.core.ultratb.find_recursion
776 775 - IPython.core.ultratb.findsource
777 776 - IPython.core.ultratb.fix_frame_records_filenames
778 777 - IPython.core.ultratb.inspect_error
779 778 - IPython.core.ultratb.is_recursion_error
780 779 - IPython.core.ultratb.with_patch_inspect
781 780 - IPython.external.__all__
782 781 - IPython.external.__builtins__
783 782 - IPython.external.__cached__
784 783 - IPython.external.__doc__
785 784 - IPython.external.__file__
786 785 - IPython.external.__loader__
787 786 - IPython.external.__name__
788 787 - IPython.external.__package__
789 788 - IPython.external.__path__
790 789 - IPython.external.__spec__
791 790 - IPython.kernel.KernelConnectionInfo
792 791 - IPython.kernel.__builtins__
793 792 - IPython.kernel.__cached__
794 793 - IPython.kernel.__warningregistry__
795 794 - IPython.kernel.pkg
796 795 - IPython.kernel.protocol_version
797 796 - IPython.kernel.protocol_version_info
798 797 - IPython.kernel.src
799 798 - IPython.kernel.version_info
800 799 - IPython.kernel.warn
801 800 - IPython.lib.backgroundjobs
802 801 - IPython.lib.backgroundjobs.BackgroundJobBase
803 802 - IPython.lib.backgroundjobs.BackgroundJobBase.run
804 803 - IPython.lib.backgroundjobs.BackgroundJobBase.traceback
805 804 - IPython.lib.backgroundjobs.BackgroundJobExpr
806 805 - IPython.lib.backgroundjobs.BackgroundJobExpr.call
807 806 - IPython.lib.backgroundjobs.BackgroundJobFunc
808 807 - IPython.lib.backgroundjobs.BackgroundJobFunc.call
809 808 - IPython.lib.backgroundjobs.BackgroundJobManager
810 809 - IPython.lib.backgroundjobs.BackgroundJobManager.flush
811 810 - IPython.lib.backgroundjobs.BackgroundJobManager.new
812 811 - IPython.lib.backgroundjobs.BackgroundJobManager.remove
813 812 - IPython.lib.backgroundjobs.BackgroundJobManager.result
814 813 - IPython.lib.backgroundjobs.BackgroundJobManager.status
815 814 - IPython.lib.backgroundjobs.BackgroundJobManager.traceback
816 815 - IPython.lib.backgroundjobs.__builtins__
817 816 - IPython.lib.backgroundjobs.__cached__
818 817 - IPython.lib.backgroundjobs.__doc__
819 818 - IPython.lib.backgroundjobs.__file__
820 819 - IPython.lib.backgroundjobs.__loader__
821 820 - IPython.lib.backgroundjobs.__name__
822 821 - IPython.lib.backgroundjobs.__package__
823 822 - IPython.lib.backgroundjobs.__spec__
824 823 - IPython.lib.kernel.__builtins__
825 824 - IPython.lib.kernel.__cached__
826 825 - IPython.lib.kernel.__doc__
827 826 - IPython.lib.kernel.__file__
828 827 - IPython.lib.kernel.__loader__
829 828 - IPython.lib.kernel.__name__
830 829 - IPython.lib.kernel.__package__
831 830 - IPython.lib.kernel.__spec__
832 831 - IPython.lib.kernel.__warningregistry__
833 832 - IPython.paths.fs_encoding
834 833 - IPython.terminal.debugger.DEFAULT_BUFFER
835 834 - IPython.terminal.debugger.cursor_in_leading_ws
836 835 - IPython.terminal.debugger.emacs_insert_mode
837 836 - IPython.terminal.debugger.has_selection
838 837 - IPython.terminal.debugger.vi_insert_mode
839 838 - IPython.terminal.interactiveshell.DISPLAY_BANNER_DEPRECATED
840 839 - IPython.terminal.ipapp.TerminalIPythonApp.parse_command_line
841 840 - IPython.testing.test
842 841 - IPython.utils.contexts.NoOpContext
843 842 - IPython.utils.io.IOStream
844 843 - IPython.utils.io.IOStream.close
845 844 - IPython.utils.io.IOStream.write
846 845 - IPython.utils.io.IOStream.writelines
847 846 - IPython.utils.io.__warningregistry__
848 847 - IPython.utils.io.atomic_writing
849 848 - IPython.utils.io.stderr
850 849 - IPython.utils.io.stdin
851 850 - IPython.utils.io.stdout
852 851 - IPython.utils.io.unicode_std_stream
853 852 - IPython.utils.path.get_ipython_cache_dir
854 853 - IPython.utils.path.get_ipython_dir
855 854 - IPython.utils.path.get_ipython_module_path
856 855 - IPython.utils.path.get_ipython_package_dir
857 856 - IPython.utils.path.locate_profile
858 857 - IPython.utils.path.unquote_filename
859 858 - IPython.utils.py3compat.PY2
860 859 - IPython.utils.py3compat.PY3
861 860 - IPython.utils.py3compat.buffer_to_bytes
862 861 - IPython.utils.py3compat.builtin_mod_name
863 862 - IPython.utils.py3compat.cast_bytes
864 863 - IPython.utils.py3compat.getcwd
865 864 - IPython.utils.py3compat.isidentifier
866 865 - IPython.utils.py3compat.u_format
867 866
868 867 The following signatures differ between 7.x and 8.0::
869 868
870 869 - IPython.core.completer.IPCompleter.unicode_name_matches(self, text)
871 870 + IPython.core.completer.IPCompleter.unicode_name_matches(text)
872 871
873 872 - IPython.core.completer.match_dict_keys(keys, prefix, delims)
874 873 + IPython.core.completer.match_dict_keys(keys, prefix, delims, extra_prefix='None')
875 874
876 875 - IPython.core.interactiveshell.InteractiveShell.object_inspect_mime(self, oname, detail_level=0)
877 876 + IPython.core.interactiveshell.InteractiveShell.object_inspect_mime(self, oname, detail_level=0, omit_sections='()')
878 877
879 878 - IPython.core.interactiveshell.InteractiveShell.set_hook(self, name, hook, priority=50, str_key='None', re_key='None', _warn_deprecated=True)
880 879 + IPython.core.interactiveshell.InteractiveShell.set_hook(self, name, hook, priority=50, str_key='None', re_key='None')
881 880
882 881 - IPython.core.oinspect.Inspector.info(self, obj, oname='', formatter='None', info='None', detail_level=0)
883 882 + IPython.core.oinspect.Inspector.info(self, obj, oname='', info='None', detail_level=0)
884 883
885 884 - IPython.core.oinspect.Inspector.pinfo(self, obj, oname='', formatter='None', info='None', detail_level=0, enable_html_pager=True)
886 885 + IPython.core.oinspect.Inspector.pinfo(self, obj, oname='', formatter='None', info='None', detail_level=0, enable_html_pager=True, omit_sections='()')
887 886
888 887 - IPython.core.profiledir.ProfileDir.copy_config_file(self, config_file, path='None', overwrite=False)
889 888 + IPython.core.profiledir.ProfileDir.copy_config_file(self, config_file, path, overwrite=False)
890 889
891 890 - IPython.core.ultratb.VerboseTB.format_record(self, frame, file, lnum, func, lines, index)
892 891 + IPython.core.ultratb.VerboseTB.format_record(self, frame_info)
893 892
894 893 - IPython.terminal.embed.InteractiveShellEmbed.mainloop(self, local_ns='None', module='None', stack_depth=0, display_banner='None', global_ns='None', compile_flags='None')
895 894 + IPython.terminal.embed.InteractiveShellEmbed.mainloop(self, local_ns='None', module='None', stack_depth=0, compile_flags='None')
896 895
897 896 - IPython.terminal.embed.embed(**kwargs)
898 897 + IPython.terminal.embed.embed(*, header='', compile_flags='None', **kwargs)
899 898
900 899 - IPython.terminal.interactiveshell.TerminalInteractiveShell.interact(self, display_banner='<object object at 0xffffff>')
901 900 + IPython.terminal.interactiveshell.TerminalInteractiveShell.interact(self)
902 901
903 902 - IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop(self, display_banner='<object object at 0xffffff>')
904 903 + IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop(self)
905 904
906 905 - IPython.utils.path.get_py_filename(name, force_win32='None')
907 906 + IPython.utils.path.get_py_filename(name)
908 907
909 908 The following are new attributes (that might be inherited)::
910 909
911 910 + IPython.core.completer.IPCompleter.unicode_names
912 911 + IPython.core.debugger.InterruptiblePdb.precmd
913 912 + IPython.core.debugger.Pdb.precmd
914 913 + IPython.core.ultratb.AutoFormattedTB.has_colors
915 914 + IPython.core.ultratb.ColorTB.has_colors
916 915 + IPython.core.ultratb.FormattedTB.has_colors
917 916 + IPython.core.ultratb.ListTB.has_colors
918 917 + IPython.core.ultratb.SyntaxTB.has_colors
919 918 + IPython.core.ultratb.TBTools.has_colors
920 919 + IPython.core.ultratb.VerboseTB.has_colors
921 920 + IPython.terminal.debugger.TerminalPdb.do_interact
922 921 + IPython.terminal.debugger.TerminalPdb.precmd
923 922
924 923 The following attribute/methods have been removed::
925 924
926 925 - IPython.core.application.BaseIPythonApplication.deprecated_subcommands
927 926 - IPython.core.ultratb.AutoFormattedTB.format_records
928 927 - IPython.core.ultratb.ColorTB.format_records
929 928 - IPython.core.ultratb.FormattedTB.format_records
930 929 - IPython.terminal.embed.InteractiveShellEmbed.init_deprecation_warnings
931 930 - IPython.terminal.embed.InteractiveShellEmbed.init_readline
932 931 - IPython.terminal.embed.InteractiveShellEmbed.write
933 932 - IPython.terminal.embed.InteractiveShellEmbed.write_err
934 933 - IPython.terminal.interactiveshell.TerminalInteractiveShell.init_deprecation_warnings
935 934 - IPython.terminal.interactiveshell.TerminalInteractiveShell.init_readline
936 935 - IPython.terminal.interactiveshell.TerminalInteractiveShell.write
937 936 - IPython.terminal.interactiveshell.TerminalInteractiveShell.write_err
938 937 - IPython.terminal.ipapp.LocateIPythonApp.deprecated_subcommands
939 938 - IPython.terminal.ipapp.LocateIPythonApp.initialize_subcommand
940 939 - IPython.terminal.ipapp.TerminalIPythonApp.deprecated_subcommands
941 940 - IPython.terminal.ipapp.TerminalIPythonApp.initialize_subcommand
General Comments 0
You need to be logged in to leave comments. Login now