##// END OF EJS Templates
Merge pull request #9671 from takluyver/fix-doctest-mode-prompts...
Min RK -
r22614:ff7baeac merge
parent child Browse files
Show More
@@ -1,578 +1,579 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 style_overrides = {}
373 373 if name == 'legacy':
374 374 legacy = self.colors.lower()
375 375 if legacy == 'linux':
376 376 style_cls = get_style_by_name('monokai')
377 377 style_overrides = _style_overrides_linux
378 378 elif legacy == 'lightbg':
379 379 style_overrides = _style_overrides_light_bg
380 380 style_cls = get_style_by_name('pastie')
381 381 elif legacy == 'neutral':
382 382 # The default theme needs to be visible on both a dark background
383 383 # and a light background, because we can't tell what the terminal
384 384 # looks like. These tweaks to the default theme help with that.
385 385 style_cls = get_style_by_name('default')
386 386 style_overrides.update({
387 387 Token.Number: '#007700',
388 388 Token.Operator: 'noinherit',
389 389 Token.String: '#BB6622',
390 390 Token.Name.Function: '#2080D0',
391 391 Token.Name.Class: 'bold #2080D0',
392 392 Token.Name.Namespace: 'bold #2080D0',
393 393 Token.Prompt: '#009900',
394 394 Token.PromptNum: '#00ff00 bold',
395 395 Token.OutPrompt: '#990000',
396 396 Token.OutPromptNum: '#ff0000 bold',
397 397 })
398 398 elif legacy =='nocolor':
399 399 style_cls=_NoStyle
400 400 style_overrides = {}
401 401 else :
402 402 raise ValueError('Got unknown colors: ', legacy)
403 403 else :
404 404 style_cls = get_style_by_name(name)
405 405 style_overrides = {
406 406 Token.Prompt: '#009900',
407 407 Token.PromptNum: '#00ff00 bold',
408 408 Token.OutPrompt: '#990000',
409 409 Token.OutPromptNum: '#ff0000 bold',
410 410 }
411 411 style_overrides.update(self.highlighting_style_overrides)
412 412 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
413 413 style_dict=style_overrides)
414 414
415 415 return style
416 416
417 417 def _layout_options(self):
418 418 """
419 419 Return the current layout option for the current Terminal InteractiveShell
420 420 """
421 421 return {
422 422 'lexer':IPythonPTLexer(),
423 423 'reserve_space_for_menu':self.space_for_menu,
424 424 'get_prompt_tokens':self.prompts.in_prompt_tokens,
425 425 'get_continuation_tokens':self.prompts.continuation_prompt_tokens,
426 426 'multiline':True,
427 427 'display_completions_in_columns': (self.display_completions == 'multicolumn'),
428 428
429 429 # Highlight matching brackets, but only when this setting is
430 430 # enabled, and only when the DEFAULT_BUFFER has the focus.
431 431 'extra_input_processors': [ConditionalProcessor(
432 432 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
433 433 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
434 434 Condition(lambda cli: self.highlight_matching_brackets))],
435 435 }
436 436
437 437 def _update_layout(self):
438 438 """
439 439 Ask for a re computation of the application layout, if for example ,
440 440 some configuration options have changed.
441 441 """
442 if getattr(self, '._app', None):
443 self._app.layout = create_prompt_layout(**self._layout_options())
442 if self._pt_app:
443 self._pt_app.layout = create_prompt_layout(**self._layout_options())
444 444
445 445 def prompt_for_code(self):
446 446 document = self.pt_cli.run(
447 447 pre_run=self.pre_prompt, reset_current_buffer=True)
448 448 return document.text
449 449
450 450 def init_io(self):
451 451 if sys.platform not in {'win32', 'cli'}:
452 452 return
453 453
454 454 import win_unicode_console
455 455 import colorama
456 456
457 457 win_unicode_console.enable()
458 458 colorama.init()
459 459
460 460 # For some reason we make these wrappers around stdout/stderr.
461 461 # For now, we need to reset them so all output gets coloured.
462 462 # https://github.com/ipython/ipython/issues/8669
463 463 from IPython.utils import io
464 464 io.stdout = io.IOStream(sys.stdout)
465 465 io.stderr = io.IOStream(sys.stderr)
466 466
467 467 def init_magics(self):
468 468 super(TerminalInteractiveShell, self).init_magics()
469 469 self.register_magics(TerminalMagics)
470 470
471 471 def init_alias(self):
472 472 # The parent class defines aliases that can be safely used with any
473 473 # frontend.
474 474 super(TerminalInteractiveShell, self).init_alias()
475 475
476 476 # Now define aliases that only make sense on the terminal, because they
477 477 # need direct access to the console in a way that we can't emulate in
478 478 # GUI or web frontend
479 479 if os.name == 'posix':
480 480 for cmd in ['clear', 'more', 'less', 'man']:
481 481 self.alias_manager.soft_define_alias(cmd, cmd)
482 482
483 483
484 484 def __init__(self, *args, **kwargs):
485 485 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
486 486 self.init_prompt_toolkit_cli()
487 487 self.init_term_title()
488 488 self.keep_running = True
489 489
490 490 self.debugger_history = InMemoryHistory()
491 491
492 492 def ask_exit(self):
493 493 self.keep_running = False
494 494
495 495 rl_next_input = None
496 496
497 497 def pre_prompt(self):
498 498 if self.rl_next_input:
499 499 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
500 500 self.rl_next_input = None
501 501
502 502 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
503 503
504 504 if display_banner is not DISPLAY_BANNER_DEPRECATED:
505 505 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
506 506
507 507 while self.keep_running:
508 508 print(self.separate_in, end='')
509 509
510 510 try:
511 511 code = self.prompt_for_code()
512 512 except EOFError:
513 513 if (not self.confirm_exit) \
514 514 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
515 515 self.ask_exit()
516 516
517 517 else:
518 518 if code:
519 519 self.run_cell(code, store_history=True)
520 520
521 521 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
522 522 # An extra layer of protection in case someone mashing Ctrl-C breaks
523 523 # out of our internal code.
524 524 if display_banner is not DISPLAY_BANNER_DEPRECATED:
525 525 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
526 526 while True:
527 527 try:
528 528 self.interact()
529 529 break
530 530 except KeyboardInterrupt:
531 531 print("\nKeyboardInterrupt escaped interact()\n")
532 532
533 533 if hasattr(self, '_eventloop'):
534 534 self._eventloop.close()
535 535
536 536 _inputhook = None
537 537 def inputhook(self, context):
538 538 if self._inputhook is not None:
539 539 self._inputhook(context)
540 540
541 541 def enable_gui(self, gui=None):
542 542 if gui:
543 543 self._inputhook = get_inputhook_func(gui)
544 544 else:
545 545 self._inputhook = None
546 546
547 547 # Run !system commands directly, not through pipes, so terminal programs
548 548 # work correctly.
549 549 system = InteractiveShell.system_raw
550 550
551 551 def auto_rewrite_input(self, cmd):
552 552 """Overridden from the parent class to use fancy rewriting prompt"""
553 553 if not self.show_rewritten_input:
554 554 return
555 555
556 556 tokens = self.prompts.rewrite_prompt_tokens()
557 557 if self.pt_cli:
558 558 self.pt_cli.print_tokens(tokens)
559 559 print(cmd)
560 560 else:
561 561 prompt = ''.join(s for t, s in tokens)
562 562 print(prompt, cmd, sep='')
563 563
564 564 _prompts_before = None
565 565 def switch_doctest_mode(self, mode):
566 566 """Switch prompts to classic for %doctest_mode"""
567 567 if mode:
568 568 self._prompts_before = self.prompts
569 569 self.prompts = ClassicPrompts(self)
570 570 elif self._prompts_before:
571 571 self.prompts = self._prompts_before
572 572 self._prompts_before = None
573 self._update_layout()
573 574
574 575
575 576 InteractiveShellABC.register(TerminalInteractiveShell)
576 577
577 578 if __name__ == '__main__':
578 579 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now