ptshell.py
456 lines
| 16.8 KiB
| text/x-python
|
PythonLexer
Thomas Kluyver
|
r21930 | """IPython terminal interface using prompt_toolkit in place of readline""" | ||
from __future__ import print_function | ||||
Thomas Kluyver
|
r22128 | import os | ||
Thomas Kluyver
|
r21950 | import sys | ||
Jonathan Slenders
|
r22194 | import signal | ||
Thomas Kluyver
|
r22204 | from warnings import warn | ||
Thomas Kluyver
|
r21950 | |||
Thomas Kluyver
|
r22204 | from IPython.core.error import TryNext | ||
Thomas Kluyver
|
r21911 | from IPython.core.interactiveshell import InteractiveShell | ||
Thomas Kluyver
|
r22388 | from IPython.utils.py3compat import cast_unicode_py2, input | ||
Min RK
|
r22124 | from IPython.utils.terminal import toggle_set_term_title, set_term_title | ||
from IPython.utils.process import abbrev_cwd | ||||
Thomas Kluyver
|
r22421 | from traitlets import Bool, Unicode, Dict, Integer, observe, Instance | ||
Thomas Kluyver
|
r21911 | |||
Jonathan Slenders
|
r22296 | from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode | ||
Jonathan Slenders
|
r22368 | from prompt_toolkit.filters import HasFocus, HasSelection, Condition, ViInsertMode, EmacsInsertMode, IsDone | ||
Thomas Kluyver
|
r21914 | from prompt_toolkit.history import InMemoryHistory | ||
Matthias Bussonnier
|
r22277 | from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout | ||
Thomas Kluyver
|
r21912 | from prompt_toolkit.interface import CommandLineInterface | ||
from prompt_toolkit.key_binding.manager import KeyBindingManager | ||||
from prompt_toolkit.keys import Keys | ||||
Jonathan Slenders
|
r22368 | from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor | ||
Matthias Bussonnier
|
r22277 | from prompt_toolkit.styles import PygmentsStyle, DynamicStyle | ||
Thomas Kluyver
|
r21911 | |||
Matthias Bussonnier
|
r22277 | from pygments.styles import get_style_by_name, get_all_styles | ||
Thomas Kluyver
|
r21911 | from pygments.token import Token | ||
Matthias Bussonnier
|
r22417 | from .debugger import TerminalPdb, Pdb | ||
Thomas Kluyver
|
r21934 | from .pt_inputhooks import get_inputhook_func | ||
Thomas Kluyver
|
r22128 | from .interactiveshell import get_default_editor, TerminalMagics | ||
Thomas Kluyver
|
r22422 | from .prompts import Prompts, RichPromptDisplayHook | ||
Thomas Kluyver
|
r22388 | from .ptutils import IPythonPTCompleter, IPythonPTLexer | ||
Jonathan Slenders
|
r22197 | |||
Matthias Bussonnier
|
r22416 | _use_simple_prompt = 'IPY_TEST_SIMPLE_PROMPT' in os.environ or not sys.stdin.isatty() | ||
Jonathan Slenders
|
r22197 | |||
Thomas Kluyver
|
r22112 | class TerminalInteractiveShell(InteractiveShell): | ||
Thomas Kluyver
|
r21920 | colors_force = True | ||
Matthias Bussonnier
|
r22338 | space_for_menu = Integer(6, help='Number of line at the bottom of the screen ' | ||
'to reserve for the completion menu' | ||||
).tag(config=True) | ||||
Matthias Bussonnier
|
r22276 | |||
Matthias Bussonnier
|
r22277 | def _space_for_menu_changed(self, old, new): | ||
Matthias Bussonnier
|
r22279 | self._update_layout() | ||
Matthias Bussonnier
|
r22277 | |||
Thomas Kluyver
|
r21911 | pt_cli = None | ||
Thomas Kluyver
|
r22387 | debugger_history = None | ||
Thomas Kluyver
|
r21911 | |||
Matthias Bussonnier
|
r22416 | simple_prompt = Bool(_use_simple_prompt, | ||
help="""Use `raw_input` for the REPL, without completion, multiline input, and prompt colors. | ||||
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` | ||||
environment variable is set, or the current terminal is not a tty. | ||||
""" | ||||
).tag(config=True) | ||||
Thomas Kluyver
|
r22419 | @property | ||
def debugger_cls(self): | ||||
return Pdb if self.simple_prompt else TerminalPdb | ||||
Matthias Bussonnier
|
r22417 | |||
Min RK
|
r22340 | autoedit_syntax = Bool(False, | ||
help="auto editing of files with syntax errors.", | ||||
).tag(config=True) | ||||
Thomas Kluyver
|
r22204 | |||
Min RK
|
r22340 | confirm_exit = Bool(True, | ||
Thomas Kluyver
|
r22186 | 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.""", | ||||
Min RK
|
r22340 | ).tag(config=True) | ||
Matthias Bussonnier
|
r22338 | editing_mode = Unicode('emacs', | ||
Thomas Kluyver
|
r22301 | help="Shortcut style to use at the prompt. 'vi' or 'emacs'.", | ||
Matthias Bussonnier
|
r22338 | ).tag(config=True) | ||
Thomas Kluyver
|
r21928 | |||
Matthias Bussonnier
|
r22338 | mouse_support = Bool(False, | ||
Thomas Kluyver
|
r22055 | help="Enable mouse support in the prompt" | ||
Matthias Bussonnier
|
r22338 | ).tag(config=True) | ||
Thomas Kluyver
|
r22055 | |||
Matthias Bussonnier
|
r22338 | highlighting_style = Unicode('default', | ||
Matthias Bussonnier
|
r22277 | help="The name of a Pygments style to use for syntax highlighting: \n %s" % ', '.join(get_all_styles()) | ||
Matthias Bussonnier
|
r22338 | ).tag(config=True) | ||
Thomas Kluyver
|
r21929 | |||
Min RK
|
r22340 | |||
@observe('highlighting_style') | ||||
def _highlighting_style_changed(self, change): | ||||
Matthias Bussonnier
|
r22277 | self._style = self._make_style_from_name(self.highlighting_style) | ||
Matthias Bussonnier
|
r22338 | highlighting_style_overrides = Dict( | ||
Thomas Kluyver
|
r21929 | help="Override highlighting format for specific tokens" | ||
Matthias Bussonnier
|
r22338 | ).tag(config=True) | ||
Thomas Kluyver
|
r21929 | |||
Matthias Bussonnier
|
r22338 | editor = Unicode(get_default_editor(), | ||
Thomas Kluyver
|
r21969 | help="Set the editor used by IPython (default to $EDITOR/vi/notepad)." | ||
Matthias Bussonnier
|
r22338 | ).tag(config=True) | ||
Thomas Kluyver
|
r22421 | |||
prompts = Instance(Prompts) | ||||
def _prompts_default(self): | ||||
return Prompts(self) | ||||
Thomas Kluyver
|
r22422 | |||
def _displayhook_class_default(self): | ||||
return RichPromptDisplayHook | ||||
Matthias Bussonnier
|
r22338 | term_title = Bool(True, | ||
Min RK
|
r22124 | help="Automatically set the terminal title" | ||
Matthias Bussonnier
|
r22338 | ).tag(config=True) | ||
Jonathan Slenders
|
r22302 | |||
Matthias Bussonnier
|
r22338 | display_completions_in_columns = Bool(False, | ||
Jonathan Slenders
|
r22302 | help="Display a multi column completion menu.", | ||
Matthias Bussonnier
|
r22338 | ).tag(config=True) | ||
Jonathan Slenders
|
r22302 | |||
Thomas Kluyver
|
r22374 | highlight_matching_brackets = Bool(True, | ||
Jonathan Slenders
|
r22368 | help="Highlight matching brackets .", | ||
).tag(config=True) | ||||
Min RK
|
r22340 | @observe('term_title') | ||
def init_term_title(self, change=None): | ||||
Min RK
|
r22124 | # Enable or disable the terminal title. | ||
if self.term_title: | ||||
toggle_set_term_title(True) | ||||
set_term_title('IPython: ' + abbrev_cwd()) | ||||
else: | ||||
toggle_set_term_title(False) | ||||
Thomas Kluyver
|
r21969 | |||
Thomas Kluyver
|
r21911 | def init_prompt_toolkit_cli(self): | ||
Matthias Bussonnier
|
r22416 | if self.simple_prompt: | ||
Thomas Kluyver
|
r22132 | # Fall back to plain non-interactive output for tests. | ||
# This is very limited, and only accepts a single line. | ||||
Thomas Kluyver
|
r22107 | def prompt(): | ||
return cast_unicode_py2(input('In [%d]: ' % self.execution_count)) | ||||
self.prompt_for_code = prompt | ||||
return | ||||
Jonathan Slenders
|
r22296 | kbmanager = KeyBindingManager.for_prompt() | ||
insert_mode = ViInsertMode() | EmacsInsertMode() | ||||
Thomas Kluyver
|
r21923 | # Ctrl+J == Enter, seemingly | ||
@kbmanager.registry.add_binding(Keys.ControlJ, | ||||
Thomas Kluyver
|
r21928 | filter=(HasFocus(DEFAULT_BUFFER) | ||
& ~HasSelection() | ||||
& insert_mode | ||||
)) | ||||
Thomas Kluyver
|
r21912 | def _(event): | ||
b = event.current_buffer | ||||
Thomas Kluyver
|
r22012 | d = b.document | ||
if not (d.on_last_line or d.cursor_position_row >= d.line_count | ||||
- d.empty_line_count_at_the_end()): | ||||
Thomas Kluyver
|
r21912 | b.newline() | ||
return | ||||
Thomas Kluyver
|
r22012 | status, indent = self.input_splitter.check_complete(d.text) | ||
Thomas Kluyver
|
r21912 | |||
if (status != 'incomplete') and b.accept_action.is_returnable: | ||||
b.accept_action.validate_and_handle(event.cli, b) | ||||
else: | ||||
Thomas Kluyver
|
r21914 | b.insert_text('\n' + (' ' * (indent or 0))) | ||
Thomas Kluyver
|
r22162 | @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(DEFAULT_BUFFER)) | ||
Matthias Bussonnier
|
r22332 | def _reset_buffer(event): | ||
Thomas Kluyver
|
r21915 | event.current_buffer.reset() | ||
Gil Forsyth
|
r22241 | @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(SEARCH_BUFFER)) | ||
Matthias Bussonnier
|
r22332 | def _reset_search_buffer(event): | ||
Gil Forsyth
|
r22241 | if event.current_buffer.document.text: | ||
event.current_buffer.reset() | ||||
else: | ||||
event.cli.push_focus(DEFAULT_BUFFER) | ||||
Jonathan Slenders
|
r22194 | supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP')) | ||
@kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend) | ||||
Matthias Bussonnier
|
r22332 | def _suspend_to_bg(event): | ||
Jonathan Slenders
|
r22194 | event.cli.suspend_to_background() | ||
Thomas Kluyver
|
r22136 | @Condition | ||
def cursor_in_leading_ws(cli): | ||||
before = cli.application.buffer.document.current_line_before_cursor | ||||
return (not before) or before.isspace() | ||||
# Ctrl+I == Tab | ||||
@kbmanager.registry.add_binding(Keys.ControlI, | ||||
filter=(HasFocus(DEFAULT_BUFFER) | ||||
& ~HasSelection() | ||||
& insert_mode | ||||
& cursor_in_leading_ws | ||||
)) | ||||
Matthias Bussonnier
|
r22332 | def _indent_buffer(event): | ||
Thomas Kluyver
|
r22136 | event.current_buffer.insert_text(' ' * 4) | ||
Thomas Kluyver
|
r21914 | # Pre-populate history from IPython's history database | ||
history = InMemoryHistory() | ||||
last_cell = u"" | ||||
Matthias Bussonnier
|
r22332 | for __, ___, cell in self.history_manager.get_tail(self.history_load_length, | ||
Thomas Kluyver
|
r21914 | include_latest=True): | ||
# Ignore blank lines and consecutive duplicates | ||||
cell = cell.rstrip() | ||||
if cell and (cell != last_cell): | ||||
history.append(cell) | ||||
Thomas Kluyver
|
r21912 | |||
Matthias Bussonnier
|
r22277 | self._style = self._make_style_from_name(self.highlighting_style) | ||
style = DynamicStyle(lambda: self._style) | ||||
Thomas Kluyver
|
r22301 | editing_mode = getattr(EditingMode, self.editing_mode.upper()) | ||
Jonathan Slenders
|
r22296 | |||
Matthias Bussonnier
|
r22277 | self._app = create_prompt_application( | ||
Jonathan Slenders
|
r22296 | editing_mode=editing_mode, | ||
Matthias Bussonnier
|
r22277 | key_bindings_registry=kbmanager.registry, | ||
history=history, | ||||
completer=IPythonPTCompleter(self.Completer), | ||||
enable_history_search=True, | ||||
style=style, | ||||
mouse_support=self.mouse_support, | ||||
**self._layout_options() | ||||
) | ||||
Thomas Kluyver
|
r22319 | self._eventloop = create_eventloop(self.inputhook) | ||
self.pt_cli = CommandLineInterface(self._app, eventloop=self._eventloop) | ||||
Matthias Bussonnier
|
r22277 | |||
def _make_style_from_name(self, name): | ||||
""" | ||||
Small wrapper that make an IPython compatible style from a style name | ||||
We need that to add style for prompt ... etc. | ||||
""" | ||||
style_cls = get_style_by_name(name) | ||||
Thomas Kluyver
|
r21929 | style_overrides = { | ||
Thomas Kluyver
|
r21918 | Token.Prompt: '#009900', | ||
Token.PromptNum: '#00ff00 bold', | ||||
Thomas Kluyver
|
r22422 | Token.OutPrompt: '#990000', | ||
Token.OutPromptNum: '#ff0000 bold', | ||||
Thomas Kluyver
|
r21929 | } | ||
Adrian
|
r22320 | if name == 'default': | ||
Thomas Kluyver
|
r21929 | style_cls = get_style_by_name('default') | ||
Thomas Kluyver
|
r21939 | # 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. | ||||
Thomas Kluyver
|
r21929 | style_overrides.update({ | ||
Token.Number: '#007700', | ||||
Token.Operator: 'noinherit', | ||||
Token.String: '#BB6622', | ||||
Thomas Kluyver
|
r21939 | Token.Name.Function: '#2080D0', | ||
Token.Name.Class: 'bold #2080D0', | ||||
Token.Name.Namespace: 'bold #2080D0', | ||||
Thomas Kluyver
|
r21929 | }) | ||
style_overrides.update(self.highlighting_style_overrides) | ||||
style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls, | ||||
style_dict=style_overrides) | ||||
Thomas Kluyver
|
r21918 | |||
Matthias Bussonnier
|
r22277 | 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, | ||||
Thomas Kluyver
|
r22421 | 'get_prompt_tokens':self.prompts.in_prompt_tokens, | ||
'get_continuation_tokens':self.prompts.continuation_prompt_tokens, | ||||
Matthias Bussonnier
|
r22283 | 'multiline':True, | ||
Jonathan Slenders
|
r22302 | 'display_completions_in_columns': self.display_completions_in_columns, | ||
Jonathan Slenders
|
r22368 | |||
# 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))], | ||||
Matthias Bussonnier
|
r22277 | } | ||
Thomas Kluyver
|
r21912 | |||
Matthias Bussonnier
|
r22279 | def _update_layout(self): | ||
Matthias Bussonnier
|
r22277 | """ | ||
Ask for a re computation of the application layout, if for example , | ||||
some configuration options have changed. | ||||
""" | ||||
self._app.layout = create_prompt_layout(**self._layout_options()) | ||||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r22107 | def prompt_for_code(self): | ||
Jonathan Slenders
|
r22296 | document = self.pt_cli.run( | ||
pre_run=self.pre_prompt, reset_current_buffer=True) | ||||
Thomas Kluyver
|
r22107 | return document.text | ||
Thomas Kluyver
|
r21950 | def init_io(self): | ||
if sys.platform not in {'win32', 'cli'}: | ||||
return | ||||
import colorama | ||||
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 | ||||
from IPython.utils import io | ||||
io.stdout = io.IOStream(sys.stdout) | ||||
io.stderr = io.IOStream(sys.stderr) | ||||
Thomas Kluyver
|
r22128 | 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) | ||||
Thomas Kluyver
|
r21911 | def __init__(self, *args, **kwargs): | ||
Thomas Kluyver
|
r22112 | super(TerminalInteractiveShell, self).__init__(*args, **kwargs) | ||
Thomas Kluyver
|
r21911 | self.init_prompt_toolkit_cli() | ||
Min RK
|
r22124 | self.init_term_title() | ||
Thomas Kluyver
|
r21911 | self.keep_running = True | ||
Thomas Kluyver
|
r22387 | self.debugger_history = InMemoryHistory() | ||
Thomas Kluyver
|
r21911 | def ask_exit(self): | ||
self.keep_running = False | ||||
Thomas Kluyver
|
r21948 | rl_next_input = None | ||
def pre_prompt(self): | ||||
if self.rl_next_input: | ||||
Thomas Kluyver
|
r22011 | self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input) | ||
Thomas Kluyver
|
r21948 | self.rl_next_input = None | ||
Thomas Kluyver
|
r21911 | def interact(self): | ||
while self.keep_running: | ||||
Thomas Kluyver
|
r21921 | print(self.separate_in, end='') | ||
Thomas Kluyver
|
r21948 | |||
Thomas Kluyver
|
r21915 | try: | ||
Thomas Kluyver
|
r22107 | code = self.prompt_for_code() | ||
Thomas Kluyver
|
r21915 | except EOFError: | ||
Thomas Kluyver
|
r22186 | if (not self.confirm_exit) \ | ||
or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): | ||||
Thomas Kluyver
|
r21915 | self.ask_exit() | ||
else: | ||||
Thomas Kluyver
|
r22107 | if code: | ||
self.run_cell(code, store_history=True) | ||||
Thomas Kluyver
|
r22204 | if self.autoedit_syntax and self.SyntaxTB.last_syntax_error: | ||
self.edit_syntax_error() | ||||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r22054 | def mainloop(self): | ||
# An extra layer of protection in case someone mashing Ctrl-C breaks | ||||
# out of our internal code. | ||||
while True: | ||||
try: | ||||
self.interact() | ||||
break | ||||
except KeyboardInterrupt: | ||||
print("\nKeyboardInterrupt escaped interact()\n") | ||||
Matthias Bussonnier
|
r22344 | |||
if hasattr(self, '_eventloop'): | ||||
self._eventloop.close() | ||||
Thomas Kluyver
|
r22054 | |||
Thomas Kluyver
|
r21934 | _inputhook = None | ||
def inputhook(self, context): | ||||
if self._inputhook is not None: | ||||
self._inputhook(context) | ||||
def enable_gui(self, gui=None): | ||||
if gui: | ||||
self._inputhook = get_inputhook_func(gui) | ||||
else: | ||||
self._inputhook = None | ||||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r22204 | # Methods to support auto-editing of SyntaxErrors: | ||
def edit_syntax_error(self): | ||||
"""The bottom half of the syntax error handler called in the main loop. | ||||
Loop until syntax error is fixed or user cancels. | ||||
""" | ||||
while self.SyntaxTB.last_syntax_error: | ||||
# copy and clear last_syntax_error | ||||
err = self.SyntaxTB.clear_err_state() | ||||
if not self._should_recompile(err): | ||||
return | ||||
try: | ||||
# may set last_syntax_error again if a SyntaxError is raised | ||||
self.safe_execfile(err.filename, self.user_ns) | ||||
except: | ||||
self.showtraceback() | ||||
else: | ||||
try: | ||||
Thomas Kluyver
|
r22205 | with open(err.filename) as f: | ||
Thomas Kluyver
|
r22204 | # This should be inside a display_trap block and I | ||
# think it is. | ||||
sys.displayhook(f.read()) | ||||
except: | ||||
self.showtraceback() | ||||
def _should_recompile(self, e): | ||||
"""Utility routine for edit_syntax_error""" | ||||
if e.filename in ('<ipython console>', '<input>', '<string>', | ||||
'<console>', '<BackgroundJob compilation>', | ||||
None): | ||||
return False | ||||
try: | ||||
if (self.autoedit_syntax and | ||||
not self.ask_yes_no( | ||||
'Return to editor to correct syntax error? ' | ||||
'[Y/n] ', 'y')): | ||||
return False | ||||
except EOFError: | ||||
return False | ||||
def int0(x): | ||||
try: | ||||
return int(x) | ||||
except TypeError: | ||||
return 0 | ||||
# always pass integer line and offset values to editor hook | ||||
try: | ||||
self.hooks.fix_error_editor(e.filename, | ||||
int0(e.lineno), int0(e.offset), | ||||
e.msg) | ||||
except TryNext: | ||||
warn('Could not open editor') | ||||
return False | ||||
return True | ||||
Thomas Kluyver
|
r22239 | # Run !system commands directly, not through pipes, so terminal programs | ||
# work correctly. | ||||
system = InteractiveShell.system_raw | ||||
Thomas Kluyver
|
r22421 | 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='') | ||||
Thomas Kluyver
|
r22239 | |||
Thomas Kluyver
|
r21911 | if __name__ == '__main__': | ||
Thomas Kluyver
|
r22112 | TerminalInteractiveShell.instance().interact() | ||