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