##// END OF EJS Templates
Better support for KeyboardInterrupt during prompt
Jeroen Demeyer -
Show More
@@ -1,478 +1,484 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 cast_unicode_py2, input
11 11 from IPython.utils.terminal import toggle_set_term_title, set_term_title
12 12 from IPython.utils.process import abbrev_cwd
13 13 from traitlets import Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union
14 14
15 15 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
16 16 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
17 17 from prompt_toolkit.history import InMemoryHistory
18 18 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout, create_output
19 19 from prompt_toolkit.interface import CommandLineInterface
20 20 from prompt_toolkit.key_binding.manager import KeyBindingManager
21 21 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
22 22 from prompt_toolkit.styles import PygmentsStyle, DynamicStyle
23 23
24 24 from pygments.styles import get_style_by_name, get_all_styles
25 25 from pygments.style import Style
26 26 from pygments.token import Token
27 27
28 28 from .debugger import TerminalPdb, Pdb
29 29 from .magics import TerminalMagics
30 30 from .pt_inputhooks import get_inputhook_name_and_func
31 31 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
32 32 from .ptutils import IPythonPTCompleter, IPythonPTLexer
33 33 from .shortcuts import register_ipython_shortcuts
34 34
35 35 DISPLAY_BANNER_DEPRECATED = object()
36 36
37 37
38 38 from pygments.style import Style
39 39
40 40 class _NoStyle(Style): pass
41 41
42 42
43 43
44 44 _style_overrides_light_bg = {
45 45 Token.Prompt: '#0000ff',
46 46 Token.PromptNum: '#0000ee bold',
47 47 Token.OutPrompt: '#cc0000',
48 48 Token.OutPromptNum: '#bb0000 bold',
49 49 }
50 50
51 51 _style_overrides_linux = {
52 52 Token.Prompt: '#00cc00',
53 53 Token.PromptNum: '#00bb00 bold',
54 54 Token.OutPrompt: '#cc0000',
55 55 Token.OutPromptNum: '#bb0000 bold',
56 56 }
57 57
58 58
59 59
60 60 def get_default_editor():
61 61 try:
62 62 return os.environ['EDITOR']
63 63 except KeyError:
64 64 pass
65 65 except UnicodeError:
66 66 warn("$EDITOR environment variable is not pure ASCII. Using platform "
67 67 "default editor.")
68 68
69 69 if os.name == 'posix':
70 70 return 'vi' # the only one guaranteed to be there!
71 71 else:
72 72 return 'notepad' # same in Windows!
73 73
74 74 # conservatively check for tty
75 75 # overridden streams can result in things like:
76 76 # - sys.stdin = None
77 77 # - no isatty method
78 78 for _name in ('stdin', 'stdout', 'stderr'):
79 79 _stream = getattr(sys, _name)
80 80 if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
81 81 _is_tty = False
82 82 break
83 83 else:
84 84 _is_tty = True
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 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
91 91 'to reserve for the completion menu'
92 92 ).tag(config=True)
93 93
94 94 def _space_for_menu_changed(self, old, new):
95 95 self._update_layout()
96 96
97 97 pt_cli = None
98 98 debugger_history = None
99 99 _pt_app = None
100 100
101 101 simple_prompt = Bool(_use_simple_prompt,
102 102 help="""Use `raw_input` for the REPL, without completion, multiline input, and prompt colors.
103 103
104 104 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
105 105 IPython own testing machinery, and emacs inferior-shell integration through elpy.
106 106
107 107 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
108 108 environment variable is set, or the current terminal is not a tty.
109 109
110 110 """
111 111 ).tag(config=True)
112 112
113 113 @property
114 114 def debugger_cls(self):
115 115 return Pdb if self.simple_prompt else TerminalPdb
116 116
117 117 confirm_exit = Bool(True,
118 118 help="""
119 119 Set to confirm when you try to exit IPython with an EOF (Control-D
120 120 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
121 121 you can force a direct exit without any confirmation.""",
122 122 ).tag(config=True)
123 123
124 124 editing_mode = Unicode('emacs',
125 125 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
126 126 ).tag(config=True)
127 127
128 128 mouse_support = Bool(False,
129 129 help="Enable mouse support in the prompt"
130 130 ).tag(config=True)
131 131
132 132 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
133 133 help="""The name or class of a Pygments style to use for syntax
134 134 highlighting: \n %s""" % ', '.join(get_all_styles())
135 135 ).tag(config=True)
136 136
137 137
138 138 @observe('highlighting_style')
139 139 @observe('colors')
140 140 def _highlighting_style_changed(self, change):
141 141 self.refresh_style()
142 142
143 143 def refresh_style(self):
144 144 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
145 145
146 146
147 147 highlighting_style_overrides = Dict(
148 148 help="Override highlighting format for specific tokens"
149 149 ).tag(config=True)
150 150
151 151 true_color = Bool(False,
152 152 help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
153 153 "If your terminal supports true color, the following command "
154 154 "should print 'TRUECOLOR' in orange: "
155 155 "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
156 156 ).tag(config=True)
157 157
158 158 editor = Unicode(get_default_editor(),
159 159 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
160 160 ).tag(config=True)
161 161
162 162 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
163 163
164 164 prompts = Instance(Prompts)
165 165
166 166 @default('prompts')
167 167 def _prompts_default(self):
168 168 return self.prompts_class(self)
169 169
170 170 @observe('prompts')
171 171 def _(self, change):
172 172 self._update_layout()
173 173
174 174 @default('displayhook_class')
175 175 def _displayhook_class_default(self):
176 176 return RichPromptDisplayHook
177 177
178 178 term_title = Bool(True,
179 179 help="Automatically set the terminal title"
180 180 ).tag(config=True)
181 181
182 182 display_completions = Enum(('column', 'multicolumn','readlinelike'),
183 183 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
184 184 "'readlinelike'. These options are for `prompt_toolkit`, see "
185 185 "`prompt_toolkit` documentation for more information."
186 186 ),
187 187 default_value='multicolumn').tag(config=True)
188 188
189 189 highlight_matching_brackets = Bool(True,
190 190 help="Highlight matching brackets .",
191 191 ).tag(config=True)
192 192
193 193 @observe('term_title')
194 194 def init_term_title(self, change=None):
195 195 # Enable or disable the terminal title.
196 196 if self.term_title:
197 197 toggle_set_term_title(True)
198 198 set_term_title('IPython: ' + abbrev_cwd())
199 199 else:
200 200 toggle_set_term_title(False)
201 201
202 202 def init_display_formatter(self):
203 203 super(TerminalInteractiveShell, self).init_display_formatter()
204 204 # terminal only supports plain text
205 205 self.display_formatter.active_types = ['text/plain']
206 206
207 207 def init_prompt_toolkit_cli(self):
208 208 if self.simple_prompt:
209 209 # Fall back to plain non-interactive output for tests.
210 210 # This is very limited, and only accepts a single line.
211 211 def prompt():
212 212 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
213 213 self.prompt_for_code = prompt
214 214 return
215 215
216 216 # Set up keyboard shortcuts
217 217 kbmanager = KeyBindingManager.for_prompt()
218 218 register_ipython_shortcuts(kbmanager.registry, self)
219 219
220 220 # Pre-populate history from IPython's history database
221 221 history = InMemoryHistory()
222 222 last_cell = u""
223 223 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
224 224 include_latest=True):
225 225 # Ignore blank lines and consecutive duplicates
226 226 cell = cell.rstrip()
227 227 if cell and (cell != last_cell):
228 228 history.append(cell)
229 229 last_cell = cell
230 230
231 231 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
232 232 style = DynamicStyle(lambda: self._style)
233 233
234 234 editing_mode = getattr(EditingMode, self.editing_mode.upper())
235 235
236 236 self._pt_app = create_prompt_application(
237 237 editing_mode=editing_mode,
238 238 key_bindings_registry=kbmanager.registry,
239 239 history=history,
240 240 completer=IPythonPTCompleter(shell=self),
241 241 enable_history_search=True,
242 242 style=style,
243 243 mouse_support=self.mouse_support,
244 244 **self._layout_options()
245 245 )
246 246 self._eventloop = create_eventloop(self.inputhook)
247 247 self.pt_cli = CommandLineInterface(
248 248 self._pt_app, eventloop=self._eventloop,
249 249 output=create_output(true_color=self.true_color))
250 250
251 251 def _make_style_from_name_or_cls(self, name_or_cls):
252 252 """
253 253 Small wrapper that make an IPython compatible style from a style name
254 254
255 255 We need that to add style for prompt ... etc.
256 256 """
257 257 style_overrides = {}
258 258 if name_or_cls == 'legacy':
259 259 legacy = self.colors.lower()
260 260 if legacy == 'linux':
261 261 style_cls = get_style_by_name('monokai')
262 262 style_overrides = _style_overrides_linux
263 263 elif legacy == 'lightbg':
264 264 style_overrides = _style_overrides_light_bg
265 265 style_cls = get_style_by_name('pastie')
266 266 elif legacy == 'neutral':
267 267 # The default theme needs to be visible on both a dark background
268 268 # and a light background, because we can't tell what the terminal
269 269 # looks like. These tweaks to the default theme help with that.
270 270 style_cls = get_style_by_name('default')
271 271 style_overrides.update({
272 272 Token.Number: '#007700',
273 273 Token.Operator: 'noinherit',
274 274 Token.String: '#BB6622',
275 275 Token.Name.Function: '#2080D0',
276 276 Token.Name.Class: 'bold #2080D0',
277 277 Token.Name.Namespace: 'bold #2080D0',
278 278 Token.Prompt: '#009900',
279 279 Token.PromptNum: '#00ff00 bold',
280 280 Token.OutPrompt: '#990000',
281 281 Token.OutPromptNum: '#ff0000 bold',
282 282 })
283 283 elif legacy =='nocolor':
284 284 style_cls=_NoStyle
285 285 style_overrides = {}
286 286 else :
287 287 raise ValueError('Got unknown colors: ', legacy)
288 288 else :
289 289 if isinstance(name_or_cls, str):
290 290 style_cls = get_style_by_name(name_or_cls)
291 291 else:
292 292 style_cls = name_or_cls
293 293 style_overrides = {
294 294 Token.Prompt: '#009900',
295 295 Token.PromptNum: '#00ff00 bold',
296 296 Token.OutPrompt: '#990000',
297 297 Token.OutPromptNum: '#ff0000 bold',
298 298 }
299 299 style_overrides.update(self.highlighting_style_overrides)
300 300 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
301 301 style_dict=style_overrides)
302 302
303 303 return style
304 304
305 305 def _layout_options(self):
306 306 """
307 307 Return the current layout option for the current Terminal InteractiveShell
308 308 """
309 309 return {
310 310 'lexer':IPythonPTLexer(),
311 311 'reserve_space_for_menu':self.space_for_menu,
312 312 'get_prompt_tokens':self.prompts.in_prompt_tokens,
313 313 'get_continuation_tokens':self.prompts.continuation_prompt_tokens,
314 314 'multiline':True,
315 315 'display_completions_in_columns': (self.display_completions == 'multicolumn'),
316 316
317 317 # Highlight matching brackets, but only when this setting is
318 318 # enabled, and only when the DEFAULT_BUFFER has the focus.
319 319 'extra_input_processors': [ConditionalProcessor(
320 320 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
321 321 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
322 322 Condition(lambda cli: self.highlight_matching_brackets))],
323 323 }
324 324
325 325 def _update_layout(self):
326 326 """
327 327 Ask for a re computation of the application layout, if for example ,
328 328 some configuration options have changed.
329 329 """
330 330 if self._pt_app:
331 331 self._pt_app.layout = create_prompt_layout(**self._layout_options())
332 332
333 333 def prompt_for_code(self):
334 334 document = self.pt_cli.run(
335 335 pre_run=self.pre_prompt, reset_current_buffer=True)
336 336 return document.text
337 337
338 338 def enable_win_unicode_console(self):
339 339 if sys.version_info >= (3, 6):
340 340 # Since PEP 528, Python uses the unicode APIs for the Windows
341 341 # console by default, so WUC shouldn't be needed.
342 342 return
343 343
344 344 import win_unicode_console
345 345 win_unicode_console.enable()
346 346
347 347 def init_io(self):
348 348 if sys.platform not in {'win32', 'cli'}:
349 349 return
350 350
351 351 self.enable_win_unicode_console()
352 352
353 353 import colorama
354 354 colorama.init()
355 355
356 356 # For some reason we make these wrappers around stdout/stderr.
357 357 # For now, we need to reset them so all output gets coloured.
358 358 # https://github.com/ipython/ipython/issues/8669
359 359 # io.std* are deprecated, but don't show our own deprecation warnings
360 360 # during initialization of the deprecated API.
361 361 with warnings.catch_warnings():
362 362 warnings.simplefilter('ignore', DeprecationWarning)
363 363 io.stdout = io.IOStream(sys.stdout)
364 364 io.stderr = io.IOStream(sys.stderr)
365 365
366 366 def init_magics(self):
367 367 super(TerminalInteractiveShell, self).init_magics()
368 368 self.register_magics(TerminalMagics)
369 369
370 370 def init_alias(self):
371 371 # The parent class defines aliases that can be safely used with any
372 372 # frontend.
373 373 super(TerminalInteractiveShell, self).init_alias()
374 374
375 375 # Now define aliases that only make sense on the terminal, because they
376 376 # need direct access to the console in a way that we can't emulate in
377 377 # GUI or web frontend
378 378 if os.name == 'posix':
379 379 for cmd in ['clear', 'more', 'less', 'man']:
380 380 self.alias_manager.soft_define_alias(cmd, cmd)
381 381
382 382
383 383 def __init__(self, *args, **kwargs):
384 384 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
385 385 self.init_prompt_toolkit_cli()
386 386 self.init_term_title()
387 387 self.keep_running = True
388 388
389 389 self.debugger_history = InMemoryHistory()
390 390
391 391 def ask_exit(self):
392 392 self.keep_running = False
393 393
394 394 rl_next_input = None
395 395
396 396 def pre_prompt(self):
397 397 if self.rl_next_input:
398 398 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
399 399 self.rl_next_input = None
400 400
401 401 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
402 402
403 403 if display_banner is not DISPLAY_BANNER_DEPRECATED:
404 404 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
405 405
406 406 self.keep_running = True
407 407 while self.keep_running:
408 408 print(self.separate_in, end='')
409 409
410 410 try:
411 411 code = self.prompt_for_code()
412 412 except EOFError:
413 413 if (not self.confirm_exit) \
414 414 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
415 415 self.ask_exit()
416 416
417 417 else:
418 418 if code:
419 419 self.run_cell(code, store_history=True)
420 420
421 421 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
422 422 # An extra layer of protection in case someone mashing Ctrl-C breaks
423 423 # out of our internal code.
424 424 if display_banner is not DISPLAY_BANNER_DEPRECATED:
425 425 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
426 426 while True:
427 427 try:
428 428 self.interact()
429 429 break
430 except KeyboardInterrupt:
431 print("\nKeyboardInterrupt escaped interact()\n")
430 except KeyboardInterrupt as e:
431 print("\n%s escaped interact()\n" % type(e).__name__)
432 # An interrupt during the eventloop will mess up the
433 # internal state of the prompt_toolkit library.
434 # Stopping the eventloop fixes this, see
435 # https://github.com/ipython/ipython/pull/9867
436 if hasattr(self, '_eventloop'):
437 self._eventloop.stop()
432 438
433 439 _inputhook = None
434 440 def inputhook(self, context):
435 441 if self._inputhook is not None:
436 442 self._inputhook(context)
437 443
438 444 active_eventloop = None
439 445 def enable_gui(self, gui=None):
440 446 if gui:
441 447 self.active_eventloop, self._inputhook =\
442 448 get_inputhook_name_and_func(gui)
443 449 else:
444 450 self.active_eventloop = self._inputhook = None
445 451
446 452 # Run !system commands directly, not through pipes, so terminal programs
447 453 # work correctly.
448 454 system = InteractiveShell.system_raw
449 455
450 456 def auto_rewrite_input(self, cmd):
451 457 """Overridden from the parent class to use fancy rewriting prompt"""
452 458 if not self.show_rewritten_input:
453 459 return
454 460
455 461 tokens = self.prompts.rewrite_prompt_tokens()
456 462 if self.pt_cli:
457 463 self.pt_cli.print_tokens(tokens)
458 464 print(cmd)
459 465 else:
460 466 prompt = ''.join(s for t, s in tokens)
461 467 print(prompt, cmd, sep='')
462 468
463 469 _prompts_before = None
464 470 def switch_doctest_mode(self, mode):
465 471 """Switch prompts to classic for %doctest_mode"""
466 472 if mode:
467 473 self._prompts_before = self.prompts
468 474 self.prompts = ClassicPrompts(self)
469 475 elif self._prompts_before:
470 476 self.prompts = self._prompts_before
471 477 self._prompts_before = None
472 478 self._update_layout()
473 479
474 480
475 481 InteractiveShellABC.register(TerminalInteractiveShell)
476 482
477 483 if __name__ == '__main__':
478 484 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now