##// END OF EJS Templates
Merge pull request #11933 from gerrit8143/master...
Matthias Bussonnier -
r25235:cc1abb8c merge
parent child Browse files
Show More
@@ -0,0 +1,14 b''
1 Prompt Rendering Performance improvements
2 =========================================
3
4 Pull Request :ghpull:`11933` introduced an optimisation in the prompt rendering
5 logic that should decrease the resource usage of IPython when using the
6 _default_ configuration but could potentially introduce a regression of
7 functionalities if you are using a custom prompt.
8
9 We know assume if you haven't changed the default keybindings that the prompt
10 **will not change** during the duration of your input – which is for example
11 not true when using vi insert mode that switches between `[ins]` and `[nor]`
12 for the current mode.
13
14 If you are experiencing any issue let us know.
@@ -1,563 +1,573 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2
3 3 import os
4 4 import sys
5 5 import warnings
6 6 from warnings import warn
7 7
8 8 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
9 9 from IPython.utils import io
10 10 from IPython.utils.py3compat import input
11 11 from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
12 12 from IPython.utils.process import abbrev_cwd
13 13 from traitlets import (
14 14 Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union,
15 15 Any, validate
16 16 )
17 17
18 18 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
19 19 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
20 20 from prompt_toolkit.formatted_text import PygmentsTokens
21 21 from prompt_toolkit.history import InMemoryHistory
22 22 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
23 23 from prompt_toolkit.output import ColorDepth
24 24 from prompt_toolkit.patch_stdout import patch_stdout
25 25 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
26 26 from prompt_toolkit.styles import DynamicStyle, merge_styles
27 27 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
28 28
29 29 from pygments.styles import get_style_by_name
30 30 from pygments.style import Style
31 31 from pygments.token import Token
32 32
33 33 from .debugger import TerminalPdb, Pdb
34 34 from .magics import TerminalMagics
35 35 from .pt_inputhooks import get_inputhook_name_and_func
36 36 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
37 37 from .ptutils import IPythonPTCompleter, IPythonPTLexer
38 38 from .shortcuts import create_ipython_shortcuts
39 39
40 40 DISPLAY_BANNER_DEPRECATED = object()
41 41
42 42
43 43 class _NoStyle(Style): pass
44 44
45 45
46 46
47 47 _style_overrides_light_bg = {
48 48 Token.Prompt: '#0000ff',
49 49 Token.PromptNum: '#0000ee bold',
50 50 Token.OutPrompt: '#cc0000',
51 51 Token.OutPromptNum: '#bb0000 bold',
52 52 }
53 53
54 54 _style_overrides_linux = {
55 55 Token.Prompt: '#00cc00',
56 56 Token.PromptNum: '#00bb00 bold',
57 57 Token.OutPrompt: '#cc0000',
58 58 Token.OutPromptNum: '#bb0000 bold',
59 59 }
60 60
61 61 def get_default_editor():
62 62 try:
63 63 return os.environ['EDITOR']
64 64 except KeyError:
65 65 pass
66 66 except UnicodeError:
67 67 warn("$EDITOR environment variable is not pure ASCII. Using platform "
68 68 "default editor.")
69 69
70 70 if os.name == 'posix':
71 71 return 'vi' # the only one guaranteed to be there!
72 72 else:
73 73 return 'notepad' # same in Windows!
74 74
75 75 # conservatively check for tty
76 76 # overridden streams can result in things like:
77 77 # - sys.stdin = None
78 78 # - no isatty method
79 79 for _name in ('stdin', 'stdout', 'stderr'):
80 80 _stream = getattr(sys, _name)
81 81 if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
82 82 _is_tty = False
83 83 break
84 84 else:
85 85 _is_tty = True
86 86
87 87
88 88 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
89 89
90 90 class TerminalInteractiveShell(InteractiveShell):
91 91 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
92 92 'to reserve for the completion menu'
93 93 ).tag(config=True)
94 94
95 95 pt_app = None
96 96 debugger_history = None
97 97
98 98 simple_prompt = Bool(_use_simple_prompt,
99 99 help="""Use `raw_input` for the REPL, without completion and prompt colors.
100 100
101 101 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
102 102 IPython own testing machinery, and emacs inferior-shell integration through elpy.
103 103
104 104 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
105 105 environment variable is set, or the current terminal is not a tty."""
106 106 ).tag(config=True)
107 107
108 108 @property
109 109 def debugger_cls(self):
110 110 return Pdb if self.simple_prompt else TerminalPdb
111 111
112 112 confirm_exit = Bool(True,
113 113 help="""
114 114 Set to confirm when you try to exit IPython with an EOF (Control-D
115 115 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
116 116 you can force a direct exit without any confirmation.""",
117 117 ).tag(config=True)
118 118
119 119 editing_mode = Unicode('emacs',
120 120 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
121 121 ).tag(config=True)
122 122
123 123 mouse_support = Bool(False,
124 124 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
125 125 ).tag(config=True)
126 126
127 127 # We don't load the list of styles for the help string, because loading
128 128 # Pygments plugins takes time and can cause unexpected errors.
129 129 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
130 130 help="""The name or class of a Pygments style to use for syntax
131 131 highlighting. To see available styles, run `pygmentize -L styles`."""
132 132 ).tag(config=True)
133 133
134 134 @validate('editing_mode')
135 135 def _validate_editing_mode(self, proposal):
136 136 if proposal['value'].lower() == 'vim':
137 137 proposal['value']= 'vi'
138 138 elif proposal['value'].lower() == 'default':
139 139 proposal['value']= 'emacs'
140 140
141 141 if hasattr(EditingMode, proposal['value'].upper()):
142 142 return proposal['value'].lower()
143 143
144 144 return self.editing_mode
145 145
146 146
147 147 @observe('editing_mode')
148 148 def _editing_mode(self, change):
149 149 u_mode = change.new.upper()
150 150 if self.pt_app:
151 151 self.pt_app.editing_mode = u_mode
152 152
153 153 @observe('highlighting_style')
154 154 @observe('colors')
155 155 def _highlighting_style_changed(self, change):
156 156 self.refresh_style()
157 157
158 158 def refresh_style(self):
159 159 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
160 160
161 161
162 162 highlighting_style_overrides = Dict(
163 163 help="Override highlighting format for specific tokens"
164 164 ).tag(config=True)
165 165
166 166 true_color = Bool(False,
167 167 help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
168 168 "If your terminal supports true color, the following command "
169 169 "should print 'TRUECOLOR' in orange: "
170 170 "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
171 171 ).tag(config=True)
172 172
173 173 editor = Unicode(get_default_editor(),
174 174 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
175 175 ).tag(config=True)
176 176
177 177 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
178 178
179 179 prompts = Instance(Prompts)
180 180
181 181 @default('prompts')
182 182 def _prompts_default(self):
183 183 return self.prompts_class(self)
184 184
185 185 # @observe('prompts')
186 186 # def _(self, change):
187 187 # self._update_layout()
188 188
189 189 @default('displayhook_class')
190 190 def _displayhook_class_default(self):
191 191 return RichPromptDisplayHook
192 192
193 193 term_title = Bool(True,
194 194 help="Automatically set the terminal title"
195 195 ).tag(config=True)
196 196
197 197 term_title_format = Unicode("IPython: {cwd}",
198 198 help="Customize the terminal title format. This is a python format string. " +
199 199 "Available substitutions are: {cwd}."
200 200 ).tag(config=True)
201 201
202 202 display_completions = Enum(('column', 'multicolumn','readlinelike'),
203 203 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
204 204 "'readlinelike'. These options are for `prompt_toolkit`, see "
205 205 "`prompt_toolkit` documentation for more information."
206 206 ),
207 207 default_value='multicolumn').tag(config=True)
208 208
209 209 highlight_matching_brackets = Bool(True,
210 210 help="Highlight matching brackets.",
211 211 ).tag(config=True)
212 212
213 213 extra_open_editor_shortcuts = Bool(False,
214 214 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
215 215 "This is in addition to the F2 binding, which is always enabled."
216 216 ).tag(config=True)
217 217
218 218 handle_return = Any(None,
219 219 help="Provide an alternative handler to be called when the user presses "
220 220 "Return. This is an advanced option intended for debugging, which "
221 221 "may be changed or removed in later releases."
222 222 ).tag(config=True)
223 223
224 224 enable_history_search = Bool(True,
225 225 help="Allows to enable/disable the prompt toolkit history search"
226 226 ).tag(config=True)
227 227
228 228 prompt_includes_vi_mode = Bool(True,
229 229 help="Display the current vi mode (when using vi editing mode)."
230 230 ).tag(config=True)
231 231
232 232 @observe('term_title')
233 233 def init_term_title(self, change=None):
234 234 # Enable or disable the terminal title.
235 235 if self.term_title:
236 236 toggle_set_term_title(True)
237 237 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
238 238 else:
239 239 toggle_set_term_title(False)
240 240
241 241 def restore_term_title(self):
242 242 if self.term_title:
243 243 restore_term_title()
244 244
245 245 def init_display_formatter(self):
246 246 super(TerminalInteractiveShell, self).init_display_formatter()
247 247 # terminal only supports plain text
248 248 self.display_formatter.active_types = ['text/plain']
249 249 # disable `_ipython_display_`
250 250 self.display_formatter.ipython_display_formatter.enabled = False
251 251
252 252 def init_prompt_toolkit_cli(self):
253 253 if self.simple_prompt:
254 254 # Fall back to plain non-interactive output for tests.
255 255 # This is very limited.
256 256 def prompt():
257 257 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
258 258 lines = [input(prompt_text)]
259 259 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
260 260 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
261 261 lines.append( input(prompt_continuation) )
262 262 return '\n'.join(lines)
263 263 self.prompt_for_code = prompt
264 264 return
265 265
266 266 # Set up keyboard shortcuts
267 267 key_bindings = create_ipython_shortcuts(self)
268 268
269 269 # Pre-populate history from IPython's history database
270 270 history = InMemoryHistory()
271 271 last_cell = u""
272 272 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
273 273 include_latest=True):
274 274 # Ignore blank lines and consecutive duplicates
275 275 cell = cell.rstrip()
276 276 if cell and (cell != last_cell):
277 277 history.append_string(cell)
278 278 last_cell = cell
279 279
280 280 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
281 281 self.style = DynamicStyle(lambda: self._style)
282 282
283 283 editing_mode = getattr(EditingMode, self.editing_mode.upper())
284 284
285 285 self.pt_app = PromptSession(
286 286 editing_mode=editing_mode,
287 287 key_bindings=key_bindings,
288 288 history=history,
289 289 completer=IPythonPTCompleter(shell=self),
290 290 enable_history_search = self.enable_history_search,
291 291 style=self.style,
292 292 include_default_pygments_style=False,
293 293 mouse_support=self.mouse_support,
294 294 enable_open_in_editor=self.extra_open_editor_shortcuts,
295 295 color_depth=self.color_depth,
296 296 **self._extra_prompt_options())
297 297
298 298 def _make_style_from_name_or_cls(self, name_or_cls):
299 299 """
300 300 Small wrapper that make an IPython compatible style from a style name
301 301
302 302 We need that to add style for prompt ... etc.
303 303 """
304 304 style_overrides = {}
305 305 if name_or_cls == 'legacy':
306 306 legacy = self.colors.lower()
307 307 if legacy == 'linux':
308 308 style_cls = get_style_by_name('monokai')
309 309 style_overrides = _style_overrides_linux
310 310 elif legacy == 'lightbg':
311 311 style_overrides = _style_overrides_light_bg
312 312 style_cls = get_style_by_name('pastie')
313 313 elif legacy == 'neutral':
314 314 # The default theme needs to be visible on both a dark background
315 315 # and a light background, because we can't tell what the terminal
316 316 # looks like. These tweaks to the default theme help with that.
317 317 style_cls = get_style_by_name('default')
318 318 style_overrides.update({
319 319 Token.Number: '#007700',
320 320 Token.Operator: 'noinherit',
321 321 Token.String: '#BB6622',
322 322 Token.Name.Function: '#2080D0',
323 323 Token.Name.Class: 'bold #2080D0',
324 324 Token.Name.Namespace: 'bold #2080D0',
325 325 Token.Prompt: '#009900',
326 326 Token.PromptNum: '#ansibrightgreen bold',
327 327 Token.OutPrompt: '#990000',
328 328 Token.OutPromptNum: '#ansibrightred bold',
329 329 })
330 330
331 331 # Hack: Due to limited color support on the Windows console
332 332 # the prompt colors will be wrong without this
333 333 if os.name == 'nt':
334 334 style_overrides.update({
335 335 Token.Prompt: '#ansidarkgreen',
336 336 Token.PromptNum: '#ansigreen bold',
337 337 Token.OutPrompt: '#ansidarkred',
338 338 Token.OutPromptNum: '#ansired bold',
339 339 })
340 340 elif legacy =='nocolor':
341 341 style_cls=_NoStyle
342 342 style_overrides = {}
343 343 else :
344 344 raise ValueError('Got unknown colors: ', legacy)
345 345 else :
346 346 if isinstance(name_or_cls, str):
347 347 style_cls = get_style_by_name(name_or_cls)
348 348 else:
349 349 style_cls = name_or_cls
350 350 style_overrides = {
351 351 Token.Prompt: '#009900',
352 352 Token.PromptNum: '#ansibrightgreen bold',
353 353 Token.OutPrompt: '#990000',
354 354 Token.OutPromptNum: '#ansibrightred bold',
355 355 }
356 356 style_overrides.update(self.highlighting_style_overrides)
357 357 style = merge_styles([
358 358 style_from_pygments_cls(style_cls),
359 359 style_from_pygments_dict(style_overrides),
360 360 ])
361 361
362 362 return style
363 363
364 364 @property
365 365 def pt_complete_style(self):
366 366 return {
367 367 'multicolumn': CompleteStyle.MULTI_COLUMN,
368 368 'column': CompleteStyle.COLUMN,
369 369 'readlinelike': CompleteStyle.READLINE_LIKE,
370 370 }[self.display_completions]
371 371
372 372 @property
373 373 def color_depth(self):
374 374 return (ColorDepth.TRUE_COLOR if self.true_color else None)
375 375
376 376 def _extra_prompt_options(self):
377 377 """
378 378 Return the current layout option for the current Terminal InteractiveShell
379 379 """
380 380 def get_message():
381 381 return PygmentsTokens(self.prompts.in_prompt_tokens())
382 382
383 if self.editing_mode == 'emacs':
384 # with emacs mode the prompt is (usually) static, so we call only
385 # the function once. With VI mode it can toggle between [ins] and
386 # [nor] so we can't precompute.
387 # here I'm going to favor the default keybinding which almost
388 # everybody uses to decrease CPU usage.
389 # if we have issues with users with custom Prompts we can see how to
390 # work around this.
391 get_message = get_message()
392
383 393 return {
384 394 'complete_in_thread': False,
385 395 'lexer':IPythonPTLexer(),
386 396 'reserve_space_for_menu':self.space_for_menu,
387 397 'message': get_message,
388 398 'prompt_continuation': (
389 399 lambda width, lineno, is_soft_wrap:
390 400 PygmentsTokens(self.prompts.continuation_prompt_tokens(width))),
391 401 'multiline': True,
392 402 'complete_style': self.pt_complete_style,
393 403
394 404 # Highlight matching brackets, but only when this setting is
395 405 # enabled, and only when the DEFAULT_BUFFER has the focus.
396 406 'input_processors': [ConditionalProcessor(
397 407 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
398 408 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
399 409 Condition(lambda: self.highlight_matching_brackets))],
400 410 'inputhook': self.inputhook,
401 411 }
402 412
403 413 def prompt_for_code(self):
404 414 if self.rl_next_input:
405 415 default = self.rl_next_input
406 416 self.rl_next_input = None
407 417 else:
408 418 default = ''
409 419
410 420 with patch_stdout(raw=True):
411 421 text = self.pt_app.prompt(
412 422 default=default,
413 423 # pre_run=self.pre_prompt,# reset_current_buffer=True,
414 424 **self._extra_prompt_options())
415 425 return text
416 426
417 427 def enable_win_unicode_console(self):
418 428 if sys.version_info >= (3, 6):
419 429 # Since PEP 528, Python uses the unicode APIs for the Windows
420 430 # console by default, so WUC shouldn't be needed.
421 431 return
422 432
423 433 import win_unicode_console
424 434 win_unicode_console.enable()
425 435
426 436 def init_io(self):
427 437 if sys.platform not in {'win32', 'cli'}:
428 438 return
429 439
430 440 self.enable_win_unicode_console()
431 441
432 442 import colorama
433 443 colorama.init()
434 444
435 445 # For some reason we make these wrappers around stdout/stderr.
436 446 # For now, we need to reset them so all output gets coloured.
437 447 # https://github.com/ipython/ipython/issues/8669
438 448 # io.std* are deprecated, but don't show our own deprecation warnings
439 449 # during initialization of the deprecated API.
440 450 with warnings.catch_warnings():
441 451 warnings.simplefilter('ignore', DeprecationWarning)
442 452 io.stdout = io.IOStream(sys.stdout)
443 453 io.stderr = io.IOStream(sys.stderr)
444 454
445 455 def init_magics(self):
446 456 super(TerminalInteractiveShell, self).init_magics()
447 457 self.register_magics(TerminalMagics)
448 458
449 459 def init_alias(self):
450 460 # The parent class defines aliases that can be safely used with any
451 461 # frontend.
452 462 super(TerminalInteractiveShell, self).init_alias()
453 463
454 464 # Now define aliases that only make sense on the terminal, because they
455 465 # need direct access to the console in a way that we can't emulate in
456 466 # GUI or web frontend
457 467 if os.name == 'posix':
458 468 for cmd in ('clear', 'more', 'less', 'man'):
459 469 self.alias_manager.soft_define_alias(cmd, cmd)
460 470
461 471
462 472 def __init__(self, *args, **kwargs):
463 473 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
464 474 self.init_prompt_toolkit_cli()
465 475 self.init_term_title()
466 476 self.keep_running = True
467 477
468 478 self.debugger_history = InMemoryHistory()
469 479
470 480 def ask_exit(self):
471 481 self.keep_running = False
472 482
473 483 rl_next_input = None
474 484
475 485 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
476 486
477 487 if display_banner is not DISPLAY_BANNER_DEPRECATED:
478 488 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
479 489
480 490 self.keep_running = True
481 491 while self.keep_running:
482 492 print(self.separate_in, end='')
483 493
484 494 try:
485 495 code = self.prompt_for_code()
486 496 except EOFError:
487 497 if (not self.confirm_exit) \
488 498 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
489 499 self.ask_exit()
490 500
491 501 else:
492 502 if code:
493 503 self.run_cell(code, store_history=True)
494 504
495 505 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
496 506 # An extra layer of protection in case someone mashing Ctrl-C breaks
497 507 # out of our internal code.
498 508 if display_banner is not DISPLAY_BANNER_DEPRECATED:
499 509 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
500 510 while True:
501 511 try:
502 512 self.interact()
503 513 break
504 514 except KeyboardInterrupt as e:
505 515 print("\n%s escaped interact()\n" % type(e).__name__)
506 516 finally:
507 517 # An interrupt during the eventloop will mess up the
508 518 # internal state of the prompt_toolkit library.
509 519 # Stopping the eventloop fixes this, see
510 520 # https://github.com/ipython/ipython/pull/9867
511 521 if hasattr(self, '_eventloop'):
512 522 self._eventloop.stop()
513 523
514 524 self.restore_term_title()
515 525
516 526
517 527 _inputhook = None
518 528 def inputhook(self, context):
519 529 if self._inputhook is not None:
520 530 self._inputhook(context)
521 531
522 532 active_eventloop = None
523 533 def enable_gui(self, gui=None):
524 534 if gui:
525 535 self.active_eventloop, self._inputhook =\
526 536 get_inputhook_name_and_func(gui)
527 537 else:
528 538 self.active_eventloop = self._inputhook = None
529 539
530 540 # Run !system commands directly, not through pipes, so terminal programs
531 541 # work correctly.
532 542 system = InteractiveShell.system_raw
533 543
534 544 def auto_rewrite_input(self, cmd):
535 545 """Overridden from the parent class to use fancy rewriting prompt"""
536 546 if not self.show_rewritten_input:
537 547 return
538 548
539 549 tokens = self.prompts.rewrite_prompt_tokens()
540 550 if self.pt_app:
541 551 print_formatted_text(PygmentsTokens(tokens), end='',
542 552 style=self.pt_app.app.style)
543 553 print(cmd)
544 554 else:
545 555 prompt = ''.join(s for t, s in tokens)
546 556 print(prompt, cmd, sep='')
547 557
548 558 _prompts_before = None
549 559 def switch_doctest_mode(self, mode):
550 560 """Switch prompts to classic for %doctest_mode"""
551 561 if mode:
552 562 self._prompts_before = self.prompts
553 563 self.prompts = ClassicPrompts(self)
554 564 elif self._prompts_before:
555 565 self.prompts = self._prompts_before
556 566 self._prompts_before = None
557 567 # self._update_layout()
558 568
559 569
560 570 InteractiveShellABC.register(TerminalInteractiveShell)
561 571
562 572 if __name__ == '__main__':
563 573 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now