interactiveshell.py
537 lines
| 20.3 KiB
| text/x-python
|
PythonLexer
|
r22557 | """IPython terminal interface using prompt_toolkit""" | ||
|
r21846 | |||
|
r22549 | import os | ||
import sys | ||||
|
r22742 | import warnings | ||
|
r22092 | from warnings import warn | ||
|
r2761 | |||
|
r22549 | from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC | ||
|
r22742 | from IPython.utils import io | ||
|
r23707 | from IPython.utils.py3compat import input, cast_unicode_py2 | ||
|
r22549 | from IPython.utils.terminal import toggle_set_term_title, set_term_title | ||
from IPython.utils.process import abbrev_cwd | ||||
|
r23580 | from traitlets import ( | ||
Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union, | ||||
Any, | ||||
) | ||||
|
r22549 | |||
|
r23289 | from prompt_toolkit.document import Document | ||
|
r22642 | from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode | ||
from prompt_toolkit.filters import (HasFocus, Condition, IsDone) | ||||
|
r22549 | from prompt_toolkit.history import InMemoryHistory | ||
|
r22682 | from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout, create_output | ||
|
r22549 | from prompt_toolkit.interface import CommandLineInterface | ||
from prompt_toolkit.key_binding.manager import KeyBindingManager | ||||
from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor | ||||
from prompt_toolkit.styles import PygmentsStyle, DynamicStyle | ||||
from pygments.styles import get_style_by_name, get_all_styles | ||||
|
r22766 | from pygments.style import Style | ||
|
r22549 | from pygments.token import Token | ||
from .debugger import TerminalPdb, Pdb | ||||
from .magics import TerminalMagics | ||||
|
r22913 | from .pt_inputhooks import get_inputhook_name_and_func | ||
|
r22549 | from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook | ||
from .ptutils import IPythonPTCompleter, IPythonPTLexer | ||||
|
r22642 | from .shortcuts import register_ipython_shortcuts | ||
|
r22549 | |||
|
r22562 | DISPLAY_BANNER_DEPRECATED = object() | ||
|
r22549 | |||
|
r22578 | class _NoStyle(Style): pass | ||
_style_overrides_light_bg = { | ||||
Token.Prompt: '#0000ff', | ||||
Token.PromptNum: '#0000ee bold', | ||||
Token.OutPrompt: '#cc0000', | ||||
Token.OutPromptNum: '#bb0000 bold', | ||||
} | ||||
_style_overrides_linux = { | ||||
Token.Prompt: '#00cc00', | ||||
Token.PromptNum: '#00bb00 bold', | ||||
Token.OutPrompt: '#cc0000', | ||||
Token.OutPromptNum: '#bb0000 bold', | ||||
} | ||||
|
r22549 | def get_default_editor(): | ||
try: | ||||
|
r23085 | return os.environ['EDITOR'] | ||
|
r22549 | except KeyError: | ||
pass | ||||
except UnicodeError: | ||||
warn("$EDITOR environment variable is not pure ASCII. Using platform " | ||||
"default editor.") | ||||
if os.name == 'posix': | ||||
return 'vi' # the only one guaranteed to be there! | ||||
else: | ||||
return 'notepad' # same in Windows! | ||||
|
r22758 | # conservatively check for tty | ||
# overridden streams can result in things like: | ||||
# - sys.stdin = None | ||||
# - no isatty method | ||||
for _name in ('stdin', 'stdout', 'stderr'): | ||||
_stream = getattr(sys, _name) | ||||
if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty(): | ||||
_is_tty = False | ||||
break | ||||
|
r22549 | else: | ||
|
r22758 | _is_tty = True | ||
|
r22549 | |||
_use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty) | ||||
class TerminalInteractiveShell(InteractiveShell): | ||||
space_for_menu = Integer(6, help='Number of line at the bottom of the screen ' | ||||
'to reserve for the completion menu' | ||||
).tag(config=True) | ||||
def _space_for_menu_changed(self, old, new): | ||||
self._update_layout() | ||||
pt_cli = None | ||||
debugger_history = None | ||||
|
r22606 | _pt_app = None | ||
|
r22549 | |||
simple_prompt = Bool(_use_simple_prompt, | ||||
|
r23704 | help="""Use `raw_input` for the REPL, without completion and prompt colors. | ||
|
r22549 | |||
Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are: | ||||
IPython own testing machinery, and emacs inferior-shell integration through elpy. | ||||
This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT` | ||||
|
r23477 | environment variable is set, or the current terminal is not a tty.""" | ||
|
r22549 | ).tag(config=True) | ||
@property | ||||
def debugger_cls(self): | ||||
return Pdb if self.simple_prompt else TerminalPdb | ||||
confirm_exit = Bool(True, | ||||
help=""" | ||||
Set to confirm when you try to exit IPython with an EOF (Control-D | ||||
in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit', | ||||
you can force a direct exit without any confirmation.""", | ||||
).tag(config=True) | ||||
editing_mode = Unicode('emacs', | ||||
help="Shortcut style to use at the prompt. 'vi' or 'emacs'.", | ||||
).tag(config=True) | ||||
mouse_support = Bool(False, | ||||
help="Enable mouse support in the prompt" | ||||
).tag(config=True) | ||||
|
r22766 | highlighting_style = Union([Unicode('legacy'), Type(klass=Style)], | ||
help="""The name or class of a Pygments style to use for syntax | ||||
highlighting: \n %s""" % ', '.join(get_all_styles()) | ||||
|
r22549 | ).tag(config=True) | ||
|
r22859 | |||
|
r22549 | @observe('highlighting_style') | ||
|
r22578 | @observe('colors') | ||
|
r22549 | def _highlighting_style_changed(self, change): | ||
|
r22578 | self.refresh_style() | ||
def refresh_style(self): | ||||
|
r22766 | self._style = self._make_style_from_name_or_cls(self.highlighting_style) | ||
|
r22549 | |||
|
r22578 | |||
|
r22549 | highlighting_style_overrides = Dict( | ||
help="Override highlighting format for specific tokens" | ||||
).tag(config=True) | ||||
|
r22682 | true_color = Bool(False, | ||
help=("Use 24bit colors instead of 256 colors in prompt highlighting. " | ||||
"If your terminal supports true color, the following command " | ||||
"should print 'TRUECOLOR' in orange: " | ||||
"printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"") | ||||
).tag(config=True) | ||||
|
r22549 | editor = Unicode(get_default_editor(), | ||
help="Set the editor used by IPython (default to $EDITOR/vi/notepad)." | ||||
).tag(config=True) | ||||
prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True) | ||||
prompts = Instance(Prompts) | ||||
@default('prompts') | ||||
def _prompts_default(self): | ||||
return self.prompts_class(self) | ||||
@observe('prompts') | ||||
def _(self, change): | ||||
self._update_layout() | ||||
@default('displayhook_class') | ||||
def _displayhook_class_default(self): | ||||
return RichPromptDisplayHook | ||||
term_title = Bool(True, | ||||
help="Automatically set the terminal title" | ||||
).tag(config=True) | ||||
|
r23623 | term_title_format = Unicode("IPython: {cwd}", | ||
help="Customize the terminal title format. This is a python format string. " + | ||||
"Available substitutions are: {cwd}." | ||||
).tag(config=True) | ||||
|
r22859 | display_completions = Enum(('column', 'multicolumn','readlinelike'), | ||
|
r22726 | help= ( "Options for displaying tab completions, 'column', 'multicolumn', and " | ||
"'readlinelike'. These options are for `prompt_toolkit`, see " | ||||
"`prompt_toolkit` documentation for more information." | ||||
|
r22720 | ), | ||
default_value='multicolumn').tag(config=True) | ||||
|
r22554 | |||
|
r22549 | highlight_matching_brackets = Bool(True, | ||
|
r23409 | help="Highlight matching brackets.", | ||
).tag(config=True) | ||||
extra_open_editor_shortcuts = Bool(False, | ||||
help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. " | ||||
"This is in addition to the F2 binding, which is always enabled." | ||||
|
r22549 | ).tag(config=True) | ||
|
r23580 | handle_return = Any(None, | ||
help="Provide an alternative handler to be called when the user presses " | ||||
"Return. This is an advanced option intended for debugging, which " | ||||
"may be changed or removed in later releases." | ||||
).tag(config=True) | ||||
|
r22549 | @observe('term_title') | ||
def init_term_title(self, change=None): | ||||
# Enable or disable the terminal title. | ||||
if self.term_title: | ||||
toggle_set_term_title(True) | ||||
|
r23623 | set_term_title(self.term_title_format.format(cwd=abbrev_cwd())) | ||
|
r22549 | else: | ||
toggle_set_term_title(False) | ||||
def init_display_formatter(self): | ||||
super(TerminalInteractiveShell, self).init_display_formatter() | ||||
# terminal only supports plain text | ||||
self.display_formatter.active_types = ['text/plain'] | ||||
|
r23307 | # disable `_ipython_display_` | ||
self.display_formatter.ipython_display_formatter.enabled = False | ||||
|
r22549 | |||
def init_prompt_toolkit_cli(self): | ||||
if self.simple_prompt: | ||||
# Fall back to plain non-interactive output for tests. | ||||
# This is very limited, and only accepts a single line. | ||||
def prompt(): | ||||
|
r23704 | isp = self.input_splitter | ||
|
r23705 | prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens()) | ||
prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens()) | ||||
|
r23704 | while isp.push_accepts_more(): | ||
line = cast_unicode_py2(input(prompt_text)) | ||||
isp.push(line) | ||||
prompt_text = prompt_continuation | ||||
return isp.source_reset() | ||||
|
r22549 | self.prompt_for_code = prompt | ||
return | ||||
|
r22642 | # Set up keyboard shortcuts | ||
|
r23409 | kbmanager = KeyBindingManager.for_prompt( | ||
enable_open_in_editor=self.extra_open_editor_shortcuts, | ||||
) | ||||
|
r22642 | register_ipython_shortcuts(kbmanager.registry, self) | ||
|
r22549 | |||
# Pre-populate history from IPython's history database | ||||
history = InMemoryHistory() | ||||
last_cell = u"" | ||||
for __, ___, cell in self.history_manager.get_tail(self.history_load_length, | ||||
include_latest=True): | ||||
# Ignore blank lines and consecutive duplicates | ||||
cell = cell.rstrip() | ||||
if cell and (cell != last_cell): | ||||
history.append(cell) | ||||
|
r22859 | last_cell = cell | ||
|
r22549 | |||
|
r22766 | self._style = self._make_style_from_name_or_cls(self.highlighting_style) | ||
|
r23571 | self.style = DynamicStyle(lambda: self._style) | ||
|
r22549 | |||
editing_mode = getattr(EditingMode, self.editing_mode.upper()) | ||||
|
r23274 | def patch_stdout(**kwargs): | ||
return self.pt_cli.patch_stdout_context(**kwargs) | ||||
|
r22606 | self._pt_app = create_prompt_application( | ||
|
r22549 | editing_mode=editing_mode, | ||
key_bindings_registry=kbmanager.registry, | ||||
history=history, | ||||
|
r23274 | completer=IPythonPTCompleter(shell=self, | ||
patch_stdout=patch_stdout), | ||||
|
r22549 | enable_history_search=True, | ||
|
r23571 | style=self.style, | ||
|
r22549 | mouse_support=self.mouse_support, | ||
**self._layout_options() | ||||
) | ||||
self._eventloop = create_eventloop(self.inputhook) | ||||
|
r22682 | self.pt_cli = CommandLineInterface( | ||
self._pt_app, eventloop=self._eventloop, | ||||
output=create_output(true_color=self.true_color)) | ||||
|
r22549 | |||
|
r22766 | def _make_style_from_name_or_cls(self, name_or_cls): | ||
|
r22549 | """ | ||
Small wrapper that make an IPython compatible style from a style name | ||||
|
r22859 | We need that to add style for prompt ... etc. | ||
|
r22549 | """ | ||
|
r22609 | style_overrides = {} | ||
|
r22766 | if name_or_cls == 'legacy': | ||
|
r22578 | legacy = self.colors.lower() | ||
if legacy == 'linux': | ||||
style_cls = get_style_by_name('monokai') | ||||
style_overrides = _style_overrides_linux | ||||
elif legacy == 'lightbg': | ||||
style_overrides = _style_overrides_light_bg | ||||
|
r22609 | style_cls = get_style_by_name('pastie') | ||
elif legacy == 'neutral': | ||||
|
r22578 | # The default theme needs to be visible on both a dark background | ||
# and a light background, because we can't tell what the terminal | ||||
# looks like. These tweaks to the default theme help with that. | ||||
|
r22609 | style_cls = get_style_by_name('default') | ||
|
r22578 | style_overrides.update({ | ||
Token.Number: '#007700', | ||||
Token.Operator: 'noinherit', | ||||
Token.String: '#BB6622', | ||||
Token.Name.Function: '#2080D0', | ||||
Token.Name.Class: 'bold #2080D0', | ||||
Token.Name.Namespace: 'bold #2080D0', | ||||
|
r22609 | Token.Prompt: '#009900', | ||
Token.PromptNum: '#00ff00 bold', | ||||
Token.OutPrompt: '#990000', | ||||
Token.OutPromptNum: '#ff0000 bold', | ||||
|
r22578 | }) | ||
|
r23317 | |||
# Hack: Due to limited color support on the Windows console | ||||
# the prompt colors will be wrong without this | ||||
if os.name == 'nt': | ||||
style_overrides.update({ | ||||
Token.Prompt: '#ansidarkgreen', | ||||
Token.PromptNum: '#ansigreen bold', | ||||
Token.OutPrompt: '#ansidarkred', | ||||
Token.OutPromptNum: '#ansired bold', | ||||
}) | ||||
|
r22578 | elif legacy =='nocolor': | ||
style_cls=_NoStyle | ||||
style_overrides = {} | ||||
else : | ||||
raise ValueError('Got unknown colors: ', legacy) | ||||
else : | ||||
|
r23037 | if isinstance(name_or_cls, str): | ||
|
r22766 | style_cls = get_style_by_name(name_or_cls) | ||
else: | ||||
style_cls = name_or_cls | ||||
|
r22578 | style_overrides = { | ||
Token.Prompt: '#009900', | ||||
Token.PromptNum: '#00ff00 bold', | ||||
Token.OutPrompt: '#990000', | ||||
Token.OutPromptNum: '#ff0000 bold', | ||||
} | ||||
|
r22549 | style_overrides.update(self.highlighting_style_overrides) | ||
style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls, | ||||
style_dict=style_overrides) | ||||
return style | ||||
def _layout_options(self): | ||||
""" | ||||
Return the current layout option for the current Terminal InteractiveShell | ||||
""" | ||||
return { | ||||
'lexer':IPythonPTLexer(), | ||||
'reserve_space_for_menu':self.space_for_menu, | ||||
'get_prompt_tokens':self.prompts.in_prompt_tokens, | ||||
'get_continuation_tokens':self.prompts.continuation_prompt_tokens, | ||||
'multiline':True, | ||||
|
r22554 | 'display_completions_in_columns': (self.display_completions == 'multicolumn'), | ||
|
r22549 | |||
# Highlight matching brackets, but only when this setting is | ||||
# enabled, and only when the DEFAULT_BUFFER has the focus. | ||||
'extra_input_processors': [ConditionalProcessor( | ||||
processor=HighlightMatchingBracketProcessor(chars='[](){}'), | ||||
filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & | ||||
Condition(lambda cli: self.highlight_matching_brackets))], | ||||
} | ||||
def _update_layout(self): | ||||
""" | ||||
Ask for a re computation of the application layout, if for example , | ||||
some configuration options have changed. | ||||
""" | ||||
|
r22606 | if self._pt_app: | ||
self._pt_app.layout = create_prompt_layout(**self._layout_options()) | ||||
|
r22549 | |||
def prompt_for_code(self): | ||||
document = self.pt_cli.run( | ||||
pre_run=self.pre_prompt, reset_current_buffer=True) | ||||
return document.text | ||||
|
r22701 | def enable_win_unicode_console(self): | ||
|
r22867 | if sys.version_info >= (3, 6): | ||
# Since PEP 528, Python uses the unicode APIs for the Windows | ||||
# console by default, so WUC shouldn't be needed. | ||||
return | ||||
|
r22701 | import win_unicode_console | ||
|
r23098 | win_unicode_console.enable() | ||
|
r22701 | |||
|
r22549 | def init_io(self): | ||
if sys.platform not in {'win32', 'cli'}: | ||||
return | ||||
|
r22701 | self.enable_win_unicode_console() | ||
|
r22549 | |||
|
r22701 | import colorama | ||
|
r22549 | colorama.init() | ||
# For some reason we make these wrappers around stdout/stderr. | ||||
# For now, we need to reset them so all output gets coloured. | ||||
# https://github.com/ipython/ipython/issues/8669 | ||||
|
r22742 | # io.std* are deprecated, but don't show our own deprecation warnings | ||
# during initialization of the deprecated API. | ||||
with warnings.catch_warnings(): | ||||
warnings.simplefilter('ignore', DeprecationWarning) | ||||
io.stdout = io.IOStream(sys.stdout) | ||||
io.stderr = io.IOStream(sys.stderr) | ||||
|
r22549 | |||
def init_magics(self): | ||||
super(TerminalInteractiveShell, self).init_magics() | ||||
self.register_magics(TerminalMagics) | ||||
def init_alias(self): | ||||
# The parent class defines aliases that can be safely used with any | ||||
# frontend. | ||||
super(TerminalInteractiveShell, self).init_alias() | ||||
# Now define aliases that only make sense on the terminal, because they | ||||
# need direct access to the console in a way that we can't emulate in | ||||
# GUI or web frontend | ||||
if os.name == 'posix': | ||||
for cmd in ['clear', 'more', 'less', 'man']: | ||||
self.alias_manager.soft_define_alias(cmd, cmd) | ||||
|
r5435 | |||
|
r22442 | |||
|
r22436 | def __init__(self, *args, **kwargs): | ||
|
r22549 | super(TerminalInteractiveShell, self).__init__(*args, **kwargs) | ||
self.init_prompt_toolkit_cli() | ||||
self.init_term_title() | ||||
self.keep_running = True | ||||
self.debugger_history = InMemoryHistory() | ||||
def ask_exit(self): | ||||
self.keep_running = False | ||||
rl_next_input = None | ||||
def pre_prompt(self): | ||||
if self.rl_next_input: | ||||
|
r23289 | # We can't set the buffer here, because it will be reset just after | ||
# this. Adding a callable to pre_run_callables does what we need | ||||
# after the buffer is reset. | ||||
|
r23669 | s = self.rl_next_input | ||
|
r23289 | def set_doc(): | ||
self.pt_cli.application.buffer.document = Document(s) | ||||
|
r23292 | if hasattr(self.pt_cli, 'pre_run_callables'): | ||
self.pt_cli.pre_run_callables.append(set_doc) | ||||
else: | ||||
# Older version of prompt_toolkit; it's OK to set the document | ||||
# directly here. | ||||
set_doc() | ||||
|
r22549 | self.rl_next_input = None | ||
|
r22562 | def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED): | ||
if display_banner is not DISPLAY_BANNER_DEPRECATED: | ||||
warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2) | ||||
|
r22764 | self.keep_running = True | ||
|
r22549 | while self.keep_running: | ||
print(self.separate_in, end='') | ||||
try: | ||||
code = self.prompt_for_code() | ||||
except EOFError: | ||||
if (not self.confirm_exit) \ | ||||
or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): | ||||
self.ask_exit() | ||||
else: | ||||
if code: | ||||
self.run_cell(code, store_history=True) | ||||
|
r22562 | def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED): | ||
|
r22549 | # An extra layer of protection in case someone mashing Ctrl-C breaks | ||
# out of our internal code. | ||||
|
r22562 | if display_banner is not DISPLAY_BANNER_DEPRECATED: | ||
warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2) | ||||
|
r22549 | while True: | ||
try: | ||||
self.interact() | ||||
break | ||||
|
r23258 | except KeyboardInterrupt as e: | ||
print("\n%s escaped interact()\n" % type(e).__name__) | ||||
|
r23259 | finally: | ||
|
r23258 | # An interrupt during the eventloop will mess up the | ||
# internal state of the prompt_toolkit library. | ||||
# Stopping the eventloop fixes this, see | ||||
# https://github.com/ipython/ipython/pull/9867 | ||||
if hasattr(self, '_eventloop'): | ||||
self._eventloop.stop() | ||||
|
r22549 | |||
_inputhook = None | ||||
def inputhook(self, context): | ||||
if self._inputhook is not None: | ||||
self._inputhook(context) | ||||
|
r22913 | active_eventloop = None | ||
|
r22549 | def enable_gui(self, gui=None): | ||
if gui: | ||||
|
r22913 | self.active_eventloop, self._inputhook =\ | ||
get_inputhook_name_and_func(gui) | ||||
|
r22549 | else: | ||
|
r22913 | self.active_eventloop = self._inputhook = None | ||
|
r22549 | |||
# Run !system commands directly, not through pipes, so terminal programs | ||||
# work correctly. | ||||
system = InteractiveShell.system_raw | ||||
def auto_rewrite_input(self, cmd): | ||||
"""Overridden from the parent class to use fancy rewriting prompt""" | ||||
if not self.show_rewritten_input: | ||||
return | ||||
tokens = self.prompts.rewrite_prompt_tokens() | ||||
if self.pt_cli: | ||||
self.pt_cli.print_tokens(tokens) | ||||
print(cmd) | ||||
else: | ||||
prompt = ''.join(s for t, s in tokens) | ||||
print(prompt, cmd, sep='') | ||||
_prompts_before = None | ||||
def switch_doctest_mode(self, mode): | ||||
"""Switch prompts to classic for %doctest_mode""" | ||||
if mode: | ||||
self._prompts_before = self.prompts | ||||
self.prompts = ClassicPrompts(self) | ||||
elif self._prompts_before: | ||||
self.prompts = self._prompts_before | ||||
self._prompts_before = None | ||||
|
r22606 | self._update_layout() | ||
|
r22549 | |||
InteractiveShellABC.register(TerminalInteractiveShell) | ||||
if __name__ == '__main__': | ||||
TerminalInteractiveShell.instance().interact() | ||||