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