##// END OF EJS Templates
Fix switching prompts in doctest mode
Thomas Kluyver -
Show More
@@ -1,571 +1,572 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2 from __future__ import print_function
3 3
4 4 import os
5 5 import sys
6 6 import signal
7 7 from warnings import warn
8 8
9 9 from IPython.core.error import TryNext
10 10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
11 11 from IPython.utils.py3compat import PY3, cast_unicode_py2, input
12 12 from IPython.utils.terminal import toggle_set_term_title, set_term_title
13 13 from IPython.utils.process import abbrev_cwd
14 14 from traitlets import Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum
15 15
16 16 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode
17 17 from prompt_toolkit.filters import (HasFocus, HasSelection, Condition,
18 18 ViInsertMode, EmacsInsertMode, IsDone, HasCompletions)
19 19 from prompt_toolkit.filters.cli import ViMode
20 20 from prompt_toolkit.history import InMemoryHistory
21 21 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout
22 22 from prompt_toolkit.interface import CommandLineInterface
23 23 from prompt_toolkit.key_binding.manager import KeyBindingManager
24 24 from prompt_toolkit.keys import Keys
25 25 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
26 26 from prompt_toolkit.styles import PygmentsStyle, DynamicStyle
27 27 from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline
28 28
29 29 from pygments.styles import get_style_by_name, get_all_styles
30 30 from pygments.token import Token
31 31
32 32 from .debugger import TerminalPdb, Pdb
33 33 from .magics import TerminalMagics
34 34 from .pt_inputhooks import get_inputhook_func
35 35 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
36 36 from .ptutils import IPythonPTCompleter, IPythonPTLexer
37 37
38 38 DISPLAY_BANNER_DEPRECATED = object()
39 39
40 40
41 41 from pygments.style import Style
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
62 62
63 63 def get_default_editor():
64 64 try:
65 65 ed = os.environ['EDITOR']
66 66 if not PY3:
67 67 ed = ed.decode()
68 68 return ed
69 69 except KeyError:
70 70 pass
71 71 except UnicodeError:
72 72 warn("$EDITOR environment variable is not pure ASCII. Using platform "
73 73 "default editor.")
74 74
75 75 if os.name == 'posix':
76 76 return 'vi' # the only one guaranteed to be there!
77 77 else:
78 78 return 'notepad' # same in Windows!
79 79
80 80
81 81 if sys.stdin and sys.stdout and sys.stderr:
82 82 _is_tty = (sys.stdin.isatty()) and (sys.stdout.isatty()) and (sys.stderr.isatty())
83 83 else:
84 84 _is_tty = False
85 85
86 86
87 87 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
88 88
89 89 class TerminalInteractiveShell(InteractiveShell):
90 90 colors_force = True
91 91
92 92 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
93 93 'to reserve for the completion menu'
94 94 ).tag(config=True)
95 95
96 96 def _space_for_menu_changed(self, old, new):
97 97 self._update_layout()
98 98
99 99 pt_cli = None
100 100 debugger_history = None
101 _pt_app = None
101 102
102 103 simple_prompt = Bool(_use_simple_prompt,
103 104 help="""Use `raw_input` for the REPL, without completion, multiline input, and prompt colors.
104 105
105 106 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
106 107 IPython own testing machinery, and emacs inferior-shell integration through elpy.
107 108
108 109 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
109 110 environment variable is set, or the current terminal is not a tty.
110 111
111 112 """
112 113 ).tag(config=True)
113 114
114 115 @property
115 116 def debugger_cls(self):
116 117 return Pdb if self.simple_prompt else TerminalPdb
117 118
118 119 confirm_exit = Bool(True,
119 120 help="""
120 121 Set to confirm when you try to exit IPython with an EOF (Control-D
121 122 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
122 123 you can force a direct exit without any confirmation.""",
123 124 ).tag(config=True)
124 125
125 126 editing_mode = Unicode('emacs',
126 127 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
127 128 ).tag(config=True)
128 129
129 130 mouse_support = Bool(False,
130 131 help="Enable mouse support in the prompt"
131 132 ).tag(config=True)
132 133
133 134 highlighting_style = Unicode('legacy',
134 135 help="The name of a Pygments style to use for syntax highlighting: \n %s" % ', '.join(get_all_styles())
135 136 ).tag(config=True)
136 137
137 138
138 139 @observe('highlighting_style')
139 140 @observe('colors')
140 141 def _highlighting_style_changed(self, change):
141 142 self.refresh_style()
142 143
143 144 def refresh_style(self):
144 145 self._style = self._make_style_from_name(self.highlighting_style)
145 146
146 147
147 148 highlighting_style_overrides = Dict(
148 149 help="Override highlighting format for specific tokens"
149 150 ).tag(config=True)
150 151
151 152 editor = Unicode(get_default_editor(),
152 153 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
153 154 ).tag(config=True)
154 155
155 156 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
156 157
157 158 prompts = Instance(Prompts)
158 159
159 160 @default('prompts')
160 161 def _prompts_default(self):
161 162 return self.prompts_class(self)
162 163
163 164 @observe('prompts')
164 165 def _(self, change):
165 166 self._update_layout()
166 167
167 168 @default('displayhook_class')
168 169 def _displayhook_class_default(self):
169 170 return RichPromptDisplayHook
170 171
171 172 term_title = Bool(True,
172 173 help="Automatically set the terminal title"
173 174 ).tag(config=True)
174 175
175 176 # Leaving that for beta/rc tester, shoudl remove for 5.0.0 final.
176 177 display_completions_in_columns = Bool(None,
177 178 help="DEPRECATED", allow_none=True
178 179 ).tag(config=True)
179 180
180 181 @observe('display_completions_in_columns')
181 182 def _display_completions_in_columns_changed(self, new):
182 183 raise DeprecationWarning("The `display_completions_in_columns` Boolean has been replaced by the enum `display_completions`"
183 184 "with the following acceptable value: 'column', 'multicolumn','readlinelike'. ")
184 185
185 186 display_completions = Enum(('column', 'multicolumn','readlinelike'), default_value='multicolumn').tag(config=True)
186 187
187 188 highlight_matching_brackets = Bool(True,
188 189 help="Highlight matching brackets .",
189 190 ).tag(config=True)
190 191
191 192 @observe('term_title')
192 193 def init_term_title(self, change=None):
193 194 # Enable or disable the terminal title.
194 195 if self.term_title:
195 196 toggle_set_term_title(True)
196 197 set_term_title('IPython: ' + abbrev_cwd())
197 198 else:
198 199 toggle_set_term_title(False)
199 200
200 201 def init_display_formatter(self):
201 202 super(TerminalInteractiveShell, self).init_display_formatter()
202 203 # terminal only supports plain text
203 204 self.display_formatter.active_types = ['text/plain']
204 205
205 206 def init_prompt_toolkit_cli(self):
206 self._app = None
207 207 if self.simple_prompt:
208 208 # Fall back to plain non-interactive output for tests.
209 209 # This is very limited, and only accepts a single line.
210 210 def prompt():
211 211 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
212 212 self.prompt_for_code = prompt
213 213 return
214 214
215 215 kbmanager = KeyBindingManager.for_prompt()
216 216 insert_mode = ViInsertMode() | EmacsInsertMode()
217 217 # Ctrl+J == Enter, seemingly
218 218 @kbmanager.registry.add_binding(Keys.ControlJ,
219 219 filter=(HasFocus(DEFAULT_BUFFER)
220 220 & ~HasSelection()
221 221 & insert_mode
222 222 ))
223 223 def _(event):
224 224 b = event.current_buffer
225 225 d = b.document
226 226
227 227 if b.complete_state:
228 228 cc = b.complete_state.current_completion
229 229 if cc:
230 230 b.apply_completion(cc)
231 231 else:
232 232 b.cancel_completion()
233 233 return
234 234
235 235 if not (d.on_last_line or d.cursor_position_row >= d.line_count
236 236 - d.empty_line_count_at_the_end()):
237 237 b.newline()
238 238 return
239 239
240 240 status, indent = self.input_splitter.check_complete(d.text + '\n')
241 241
242 242 if (status != 'incomplete') and b.accept_action.is_returnable:
243 243 b.accept_action.validate_and_handle(event.cli, b)
244 244 else:
245 245 b.insert_text('\n' + (' ' * (indent or 0)))
246 246
247 247 @kbmanager.registry.add_binding(Keys.ControlP, filter=(ViInsertMode() & HasFocus(DEFAULT_BUFFER)))
248 248 def _previous_history_or_previous_completion(event):
249 249 """
250 250 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
251 251
252 252 If completer is open this still select previous completion.
253 253 """
254 254 event.current_buffer.auto_up()
255 255
256 256 @kbmanager.registry.add_binding(Keys.ControlN, filter=(ViInsertMode() & HasFocus(DEFAULT_BUFFER)))
257 257 def _next_history_or_next_completion(event):
258 258 """
259 259 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
260 260
261 261 If completer is open this still select next completion.
262 262 """
263 263 event.current_buffer.auto_down()
264 264
265 265 @kbmanager.registry.add_binding(Keys.ControlG, filter=(
266 266 HasFocus(DEFAULT_BUFFER) & HasCompletions()
267 267 ))
268 268 def _dismiss_completion(event):
269 269 b = event.current_buffer
270 270 if b.complete_state:
271 271 b.cancel_completion()
272 272
273 273 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(DEFAULT_BUFFER))
274 274 def _reset_buffer(event):
275 275 b = event.current_buffer
276 276 if b.complete_state:
277 277 b.cancel_completion()
278 278 else:
279 279 b.reset()
280 280
281 281 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(SEARCH_BUFFER))
282 282 def _reset_search_buffer(event):
283 283 if event.current_buffer.document.text:
284 284 event.current_buffer.reset()
285 285 else:
286 286 event.cli.push_focus(DEFAULT_BUFFER)
287 287
288 288 supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP'))
289 289
290 290 @kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend)
291 291 def _suspend_to_bg(event):
292 292 event.cli.suspend_to_background()
293 293
294 294 @Condition
295 295 def cursor_in_leading_ws(cli):
296 296 before = cli.application.buffer.document.current_line_before_cursor
297 297 return (not before) or before.isspace()
298 298
299 299 # Ctrl+I == Tab
300 300 @kbmanager.registry.add_binding(Keys.ControlI,
301 301 filter=(HasFocus(DEFAULT_BUFFER)
302 302 & ~HasSelection()
303 303 & insert_mode
304 304 & cursor_in_leading_ws
305 305 ))
306 306 def _indent_buffer(event):
307 307 event.current_buffer.insert_text(' ' * 4)
308 308
309 309
310 310 if self.display_completions == 'readlinelike':
311 311 @kbmanager.registry.add_binding(Keys.ControlI,
312 312 filter=(HasFocus(DEFAULT_BUFFER)
313 313 & ~HasSelection()
314 314 & insert_mode
315 315 & ~cursor_in_leading_ws
316 316 ))
317 317 def _disaply_compl(ev):
318 318 display_completions_like_readline(ev)
319 319
320 320
321 321 if sys.platform == 'win32':
322 322 from IPython.lib.clipboard import (ClipboardEmpty,
323 323 win32_clipboard_get, tkinter_clipboard_get)
324 324 @kbmanager.registry.add_binding(Keys.ControlV,
325 325 filter=(HasFocus(DEFAULT_BUFFER) & ~ViMode()))
326 326 def _paste(event):
327 327 try:
328 328 text = win32_clipboard_get()
329 329 except TryNext:
330 330 try:
331 331 text = tkinter_clipboard_get()
332 332 except (TryNext, ClipboardEmpty):
333 333 return
334 334 except ClipboardEmpty:
335 335 return
336 336 event.current_buffer.insert_text(text.replace('\t', ' ' * 4))
337 337
338 338 # Pre-populate history from IPython's history database
339 339 history = InMemoryHistory()
340 340 last_cell = u""
341 341 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
342 342 include_latest=True):
343 343 # Ignore blank lines and consecutive duplicates
344 344 cell = cell.rstrip()
345 345 if cell and (cell != last_cell):
346 346 history.append(cell)
347 347
348 348 self._style = self._make_style_from_name(self.highlighting_style)
349 349 style = DynamicStyle(lambda: self._style)
350 350
351 351 editing_mode = getattr(EditingMode, self.editing_mode.upper())
352 352
353 self._app = create_prompt_application(
353 self._pt_app = create_prompt_application(
354 354 editing_mode=editing_mode,
355 355 key_bindings_registry=kbmanager.registry,
356 356 history=history,
357 357 completer=IPythonPTCompleter(self.Completer),
358 358 enable_history_search=True,
359 359 style=style,
360 360 mouse_support=self.mouse_support,
361 361 **self._layout_options()
362 362 )
363 363 self._eventloop = create_eventloop(self.inputhook)
364 self.pt_cli = CommandLineInterface(self._app, eventloop=self._eventloop)
364 self.pt_cli = CommandLineInterface(self._pt_app, eventloop=self._eventloop)
365 365
366 366 def _make_style_from_name(self, name):
367 367 """
368 368 Small wrapper that make an IPython compatible style from a style name
369 369
370 370 We need that to add style for prompt ... etc.
371 371 """
372 372 if name == 'legacy':
373 373 legacy = self.colors.lower()
374 374 if legacy == 'linux':
375 375 style_cls = get_style_by_name('monokai')
376 376 style_overrides = _style_overrides_linux
377 377 elif legacy == 'lightbg':
378 378 style_overrides = _style_overrides_light_bg
379 379 style_cls = get_style_by_name('default')
380 380 # The default theme needs to be visible on both a dark background
381 381 # and a light background, because we can't tell what the terminal
382 382 # looks like. These tweaks to the default theme help with that.
383 383 style_overrides.update({
384 384 Token.Number: '#007700',
385 385 Token.Operator: 'noinherit',
386 386 Token.String: '#BB6622',
387 387 Token.Name.Function: '#2080D0',
388 388 Token.Name.Class: 'bold #2080D0',
389 389 Token.Name.Namespace: 'bold #2080D0',
390 390 })
391 391 elif legacy =='nocolor':
392 392 style_cls=_NoStyle
393 393 style_overrides = {}
394 394 else :
395 395 raise ValueError('Got unknown colors: ', legacy)
396 396 else :
397 397 style_cls = get_style_by_name(name)
398 398 style_overrides = {
399 399 Token.Prompt: '#009900',
400 400 Token.PromptNum: '#00ff00 bold',
401 401 Token.OutPrompt: '#990000',
402 402 Token.OutPromptNum: '#ff0000 bold',
403 403 }
404 404 style_overrides.update(self.highlighting_style_overrides)
405 405 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
406 406 style_dict=style_overrides)
407 407
408 408 return style
409 409
410 410 def _layout_options(self):
411 411 """
412 412 Return the current layout option for the current Terminal InteractiveShell
413 413 """
414 414 return {
415 415 'lexer':IPythonPTLexer(),
416 416 'reserve_space_for_menu':self.space_for_menu,
417 417 'get_prompt_tokens':self.prompts.in_prompt_tokens,
418 418 'get_continuation_tokens':self.prompts.continuation_prompt_tokens,
419 419 'multiline':True,
420 420 'display_completions_in_columns': (self.display_completions == 'multicolumn'),
421 421
422 422 # Highlight matching brackets, but only when this setting is
423 423 # enabled, and only when the DEFAULT_BUFFER has the focus.
424 424 'extra_input_processors': [ConditionalProcessor(
425 425 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
426 426 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
427 427 Condition(lambda cli: self.highlight_matching_brackets))],
428 428 }
429 429
430 430 def _update_layout(self):
431 431 """
432 432 Ask for a re computation of the application layout, if for example ,
433 433 some configuration options have changed.
434 434 """
435 if getattr(self, '._app', None):
436 self._app.layout = create_prompt_layout(**self._layout_options())
435 if self._pt_app:
436 self._pt_app.layout = create_prompt_layout(**self._layout_options())
437 437
438 438 def prompt_for_code(self):
439 439 document = self.pt_cli.run(
440 440 pre_run=self.pre_prompt, reset_current_buffer=True)
441 441 return document.text
442 442
443 443 def init_io(self):
444 444 if sys.platform not in {'win32', 'cli'}:
445 445 return
446 446
447 447 import win_unicode_console
448 448 import colorama
449 449
450 450 win_unicode_console.enable()
451 451 colorama.init()
452 452
453 453 # For some reason we make these wrappers around stdout/stderr.
454 454 # For now, we need to reset them so all output gets coloured.
455 455 # https://github.com/ipython/ipython/issues/8669
456 456 from IPython.utils import io
457 457 io.stdout = io.IOStream(sys.stdout)
458 458 io.stderr = io.IOStream(sys.stderr)
459 459
460 460 def init_magics(self):
461 461 super(TerminalInteractiveShell, self).init_magics()
462 462 self.register_magics(TerminalMagics)
463 463
464 464 def init_alias(self):
465 465 # The parent class defines aliases that can be safely used with any
466 466 # frontend.
467 467 super(TerminalInteractiveShell, self).init_alias()
468 468
469 469 # Now define aliases that only make sense on the terminal, because they
470 470 # need direct access to the console in a way that we can't emulate in
471 471 # GUI or web frontend
472 472 if os.name == 'posix':
473 473 for cmd in ['clear', 'more', 'less', 'man']:
474 474 self.alias_manager.soft_define_alias(cmd, cmd)
475 475
476 476
477 477 def __init__(self, *args, **kwargs):
478 478 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
479 479 self.init_prompt_toolkit_cli()
480 480 self.init_term_title()
481 481 self.keep_running = True
482 482
483 483 self.debugger_history = InMemoryHistory()
484 484
485 485 def ask_exit(self):
486 486 self.keep_running = False
487 487
488 488 rl_next_input = None
489 489
490 490 def pre_prompt(self):
491 491 if self.rl_next_input:
492 492 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
493 493 self.rl_next_input = None
494 494
495 495 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
496 496
497 497 if display_banner is not DISPLAY_BANNER_DEPRECATED:
498 498 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
499 499
500 500 while self.keep_running:
501 501 print(self.separate_in, end='')
502 502
503 503 try:
504 504 code = self.prompt_for_code()
505 505 except EOFError:
506 506 if (not self.confirm_exit) \
507 507 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
508 508 self.ask_exit()
509 509
510 510 else:
511 511 if code:
512 512 self.run_cell(code, store_history=True)
513 513
514 514 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
515 515 # An extra layer of protection in case someone mashing Ctrl-C breaks
516 516 # out of our internal code.
517 517 if display_banner is not DISPLAY_BANNER_DEPRECATED:
518 518 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
519 519 while True:
520 520 try:
521 521 self.interact()
522 522 break
523 523 except KeyboardInterrupt:
524 524 print("\nKeyboardInterrupt escaped interact()\n")
525 525
526 526 if hasattr(self, '_eventloop'):
527 527 self._eventloop.close()
528 528
529 529 _inputhook = None
530 530 def inputhook(self, context):
531 531 if self._inputhook is not None:
532 532 self._inputhook(context)
533 533
534 534 def enable_gui(self, gui=None):
535 535 if gui:
536 536 self._inputhook = get_inputhook_func(gui)
537 537 else:
538 538 self._inputhook = None
539 539
540 540 # Run !system commands directly, not through pipes, so terminal programs
541 541 # work correctly.
542 542 system = InteractiveShell.system_raw
543 543
544 544 def auto_rewrite_input(self, cmd):
545 545 """Overridden from the parent class to use fancy rewriting prompt"""
546 546 if not self.show_rewritten_input:
547 547 return
548 548
549 549 tokens = self.prompts.rewrite_prompt_tokens()
550 550 if self.pt_cli:
551 551 self.pt_cli.print_tokens(tokens)
552 552 print(cmd)
553 553 else:
554 554 prompt = ''.join(s for t, s in tokens)
555 555 print(prompt, cmd, sep='')
556 556
557 557 _prompts_before = None
558 558 def switch_doctest_mode(self, mode):
559 559 """Switch prompts to classic for %doctest_mode"""
560 560 if mode:
561 561 self._prompts_before = self.prompts
562 562 self.prompts = ClassicPrompts(self)
563 563 elif self._prompts_before:
564 564 self.prompts = self._prompts_before
565 565 self._prompts_before = None
566 self._update_layout()
566 567
567 568
568 569 InteractiveShellABC.register(TerminalInteractiveShell)
569 570
570 571 if __name__ == '__main__':
571 572 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now