From e14fb804dd2bee43505580450c0b28f786a218fb 2016-06-02 11:14:24 From: Thomas Kluyver Date: 2016-06-02 11:14:24 Subject: [PATCH] Remove the readline shell machinery IPython now uses the prompt_toolkit based TerminalInteractiveShell instead, so this removes the readline code we're no longer using. --- diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 1c1eaef..a294a9d 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -1,810 +1,18 @@ # -*- coding: utf-8 -*- -"""Subclass of InteractiveShell for terminal based frontends.""" +"""DEPRECATED: old import location of TerminalInteractiveShell""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -from __future__ import print_function - -import bdb -import os -import sys - -from IPython.core.error import TryNext, UsageError -from IPython.core.usage import interactive_usage -from IPython.core.inputsplitter import IPythonInputSplitter, ESC_MAGIC -from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC -from IPython.core.magic import Magics, magics_class, line_magic -from IPython.lib.clipboard import ClipboardEmpty -from IPython.utils.contexts import NoOpContext -from IPython.utils.decorators import undoc -from IPython.utils.encoding import get_stream_enc -from IPython.utils import py3compat -from IPython.utils.terminal import toggle_set_term_title, set_term_title -from IPython.utils.process import abbrev_cwd from warnings import warn -from logging import error -from IPython.utils.text import num_ini_spaces, SList, strip_email_quotes -from traitlets import Integer, CBool, Unicode - -def get_default_editor(): - try: - ed = os.environ['EDITOR'] - if not py3compat.PY3: - ed = ed.decode() - return ed - 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! - -def get_pasted_lines(sentinel, l_input=py3compat.input, quiet=False): - """ Yield pasted lines until the user enters the given sentinel value. - """ - if not quiet: - print("Pasting code; enter '%s' alone on the line to stop or use Ctrl-D." \ - % sentinel) - prompt = ":" - else: - prompt = "" - while True: - try: - l = py3compat.str_to_unicode(l_input(prompt)) - if l == sentinel: - return - else: - yield l - except EOFError: - print('') - return +from IPython.utils.decorators import undoc +from .ptshell import TerminalInteractiveShell as PromptToolkitShell @undoc -def no_op(*a, **kw): pass - - -class ReadlineNoRecord(object): - """Context manager to execute some code, then reload readline history - so that interactive input to the code doesn't appear when pressing up.""" - def __init__(self, shell): - self.shell = shell - self._nested_level = 0 - - def __enter__(self): - if self._nested_level == 0: - try: - self.orig_length = self.current_length() - self.readline_tail = self.get_readline_tail() - except (AttributeError, IndexError): # Can fail with pyreadline - self.orig_length, self.readline_tail = 999999, [] - self._nested_level += 1 - - def __exit__(self, type, value, traceback): - self._nested_level -= 1 - if self._nested_level == 0: - # Try clipping the end if it's got longer - try: - e = self.current_length() - self.orig_length - if e > 0: - for _ in range(e): - self.shell.readline.remove_history_item(self.orig_length) - - # If it still doesn't match, just reload readline history. - if self.current_length() != self.orig_length \ - or self.get_readline_tail() != self.readline_tail: - self.shell.refill_readline_hist() - except (AttributeError, IndexError): - pass - # Returning False will cause exceptions to propagate - return False - - def current_length(self): - return self.shell.readline.get_current_history_length() - - def get_readline_tail(self, n=10): - """Get the last n items in readline history.""" - end = self.shell.readline.get_current_history_length() + 1 - start = max(end-n, 1) - ghi = self.shell.readline.get_history_item - return [ghi(x) for x in range(start, end)] - - -@magics_class -class TerminalMagics(Magics): - def __init__(self, shell): - super(TerminalMagics, self).__init__(shell) - self.input_splitter = IPythonInputSplitter() - - def store_or_execute(self, block, name): - """ Execute a block, or store it in a variable, per the user's request. - """ - if name: - # If storing it for further editing - self.shell.user_ns[name] = SList(block.splitlines()) - print("Block assigned to '%s'" % name) - else: - b = self.preclean_input(block) - self.shell.user_ns['pasted_block'] = b - self.shell.using_paste_magics = True - try: - self.shell.run_cell(b) - finally: - self.shell.using_paste_magics = False - - def preclean_input(self, block): - lines = block.splitlines() - while lines and not lines[0].strip(): - lines = lines[1:] - return strip_email_quotes('\n'.join(lines)) - - def rerun_pasted(self, name='pasted_block'): - """ Rerun a previously pasted command. - """ - b = self.shell.user_ns.get(name) - - # Sanity checks - if b is None: - raise UsageError('No previous pasted block available') - if not isinstance(b, py3compat.string_types): - raise UsageError( - "Variable 'pasted_block' is not a string, can't execute") - - print("Re-executing '%s...' (%d chars)"% (b.split('\n',1)[0], len(b))) - self.shell.run_cell(b) - - @line_magic - def autoindent(self, parameter_s = ''): - """Toggle autoindent on/off (if available).""" - - self.shell.set_autoindent() - print("Automatic indentation is:",['OFF','ON'][self.shell.autoindent]) - - @line_magic - def cpaste(self, parameter_s=''): - """Paste & execute a pre-formatted code block from clipboard. - - You must terminate the block with '--' (two minus-signs) or Ctrl-D - alone on the line. You can also provide your own sentinel with '%paste - -s %%' ('%%' is the new sentinel for this operation). - - The block is dedented prior to execution to enable execution of method - definitions. '>' and '+' characters at the beginning of a line are - ignored, to allow pasting directly from e-mails, diff files and - doctests (the '...' continuation prompt is also stripped). The - executed block is also assigned to variable named 'pasted_block' for - later editing with '%edit pasted_block'. - - You can also pass a variable name as an argument, e.g. '%cpaste foo'. - This assigns the pasted block to variable 'foo' as string, without - dedenting or executing it (preceding >>> and + is still stripped) - - '%cpaste -r' re-executes the block previously entered by cpaste. - '%cpaste -q' suppresses any additional output messages. - - Do not be alarmed by garbled output on Windows (it's a readline bug). - Just press enter and type -- (and press enter again) and the block - will be what was just pasted. - - IPython statements (magics, shell escapes) are not supported (yet). - - See also - -------- - paste: automatically pull code from clipboard. - - Examples - -------- - :: - - In [8]: %cpaste - Pasting code; enter '--' alone on the line to stop. - :>>> a = ["world!", "Hello"] - :>>> print " ".join(sorted(a)) - :-- - Hello world! - """ - opts, name = self.parse_options(parameter_s, 'rqs:', mode='string') - if 'r' in opts: - self.rerun_pasted() - return - - quiet = ('q' in opts) - - sentinel = opts.get('s', u'--') - block = '\n'.join(get_pasted_lines(sentinel, quiet=quiet)) - self.store_or_execute(block, name) - - @line_magic - def paste(self, parameter_s=''): - """Paste & execute a pre-formatted code block from clipboard. - - The text is pulled directly from the clipboard without user - intervention and printed back on the screen before execution (unless - the -q flag is given to force quiet mode). - - The block is dedented prior to execution to enable execution of method - definitions. '>' and '+' characters at the beginning of a line are - ignored, to allow pasting directly from e-mails, diff files and - doctests (the '...' continuation prompt is also stripped). The - executed block is also assigned to variable named 'pasted_block' for - later editing with '%edit pasted_block'. - - You can also pass a variable name as an argument, e.g. '%paste foo'. - This assigns the pasted block to variable 'foo' as string, without - executing it (preceding >>> and + is still stripped). - - Options: - - -r: re-executes the block previously entered by cpaste. - - -q: quiet mode: do not echo the pasted text back to the terminal. - - IPython statements (magics, shell escapes) are not supported (yet). - - See also - -------- - cpaste: manually paste code into terminal until you mark its end. - """ - opts, name = self.parse_options(parameter_s, 'rq', mode='string') - if 'r' in opts: - self.rerun_pasted() - return - try: - block = self.shell.hooks.clipboard_get() - except TryNext as clipboard_exc: - message = getattr(clipboard_exc, 'args') - if message: - error(message[0]) - else: - error('Could not get text from the clipboard.') - return - except ClipboardEmpty: - raise UsageError("The clipboard appears to be empty") - - # By default, echo back to terminal unless quiet mode is requested - if 'q' not in opts: - write = self.shell.write - write(self.shell.pycolorize(block)) - if not block.endswith('\n'): - write('\n') - write("## -- End pasted text --\n") - - self.store_or_execute(block, name) - - # Class-level: add a '%cls' magic only on Windows - if sys.platform == 'win32': - @line_magic - def cls(self, s): - """Clear screen. - """ - os.system("cls") - - -class TerminalInteractiveShell(InteractiveShell): - - autoedit_syntax = CBool(False, config=True, - help="auto editing of files with syntax errors.") - confirm_exit = CBool(True, config=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.""", - ) - # This display_banner only controls whether or not self.show_banner() - # is called when mainloop/interact are called. The default is False - # because for the terminal based application, the banner behavior - # is controlled by the application. - display_banner = CBool(False) # This isn't configurable! - embedded = CBool(False) - embedded_active = CBool(False) - editor = Unicode(get_default_editor(), config=True, - help="Set the editor used by IPython (default to $EDITOR/vi/notepad)." - ) - pager = Unicode('less', config=True, - help="The shell program to be used for paging.") - - screen_length = Integer(0, config=True, - help= - """Number of lines of your screen, used to control printing of very - long strings. Strings longer than this number of lines will be sent - through a pager instead of directly printed. The default value for - this is 0, which means IPython will auto-detect your screen size every - time it needs to print certain potentially long strings (this doesn't - change the behavior of the 'print' keyword, it's only triggered - internally). If for some reason this isn't working well (it needs - curses support), specify it yourself. Otherwise don't change the - default.""", - ) - term_title = CBool(False, config=True, - help="Enable auto setting the terminal title." - ) - usage = Unicode(interactive_usage) - - # This `using_paste_magics` is used to detect whether the code is being - # executed via paste magics functions - using_paste_magics = CBool(False) - - # In the terminal, GUI control is done via PyOS_InputHook - @staticmethod - def enable_gui(gui=None, app=None): - """Switch amongst GUI input hooks by name. - """ - # Deferred import - from IPython.lib.inputhook import enable_gui as real_enable_gui - try: - return real_enable_gui(gui, app) - except ValueError as e: - raise UsageError("%s" % e) - - system = InteractiveShell.system_raw - - #------------------------------------------------------------------------- - # Overrides of init stages - #------------------------------------------------------------------------- - - def init_display_formatter(self): - super(TerminalInteractiveShell, self).init_display_formatter() - # terminal only supports plaintext - self.display_formatter.active_types = ['text/plain'] - - #------------------------------------------------------------------------- - # Things related to readline - #------------------------------------------------------------------------- - - def init_readline(self): - """Command history completion/saving/reloading.""" - - if self.readline_use: - import IPython.utils.rlineimpl as readline - - self.rl_next_input = None - self.rl_do_indent = False - - if not self.readline_use or not readline.have_readline: - self.readline = None - # Set a number of methods that depend on readline to be no-op - self.readline_no_record = NoOpContext() - self.set_readline_completer = no_op - self.set_custom_completer = no_op - if self.readline_use: - warn('Readline services not available or not loaded.') - else: - self.has_readline = True - self.readline = readline - sys.modules['readline'] = readline - - # Platform-specific configuration - if os.name == 'nt': - # FIXME - check with Frederick to see if we can harmonize - # naming conventions with pyreadline to avoid this - # platform-dependent check - self.readline_startup_hook = readline.set_pre_input_hook - else: - self.readline_startup_hook = readline.set_startup_hook - - # Readline config order: - # - IPython config (default value) - # - custom inputrc - # - IPython config (user customized) - - # load IPython config before inputrc if default - # skip if libedit because parse_and_bind syntax is different - if not self._custom_readline_config and not readline.uses_libedit: - for rlcommand in self.readline_parse_and_bind: - readline.parse_and_bind(rlcommand) - - # Load user's initrc file (readline config) - # Or if libedit is used, load editrc. - inputrc_name = os.environ.get('INPUTRC') - if inputrc_name is None: - inputrc_name = '.inputrc' - if readline.uses_libedit: - inputrc_name = '.editrc' - inputrc_name = os.path.join(self.home_dir, inputrc_name) - if os.path.isfile(inputrc_name): - try: - readline.read_init_file(inputrc_name) - except: - warn('Problems reading readline initialization file <%s>' - % inputrc_name) - - # load IPython config after inputrc if user has customized - if self._custom_readline_config: - for rlcommand in self.readline_parse_and_bind: - readline.parse_and_bind(rlcommand) - - # Remove some chars from the delimiters list. If we encounter - # unicode chars, discard them. - delims = readline.get_completer_delims() - if not py3compat.PY3: - delims = delims.encode("ascii", "ignore") - for d in self.readline_remove_delims: - delims = delims.replace(d, "") - delims = delims.replace(ESC_MAGIC, '') - readline.set_completer_delims(delims) - # Store these so we can restore them if something like rpy2 modifies - # them. - self.readline_delims = delims - # otherwise we end up with a monster history after a while: - readline.set_history_length(self.history_length) - - self.refill_readline_hist() - self.readline_no_record = ReadlineNoRecord(self) - - # Configure auto-indent for all platforms - self.set_autoindent(self.autoindent) - - def init_completer(self): - super(TerminalInteractiveShell, self).init_completer() - - # Only configure readline if we truly are using readline. - if self.has_readline: - self.set_readline_completer() - - def set_readline_completer(self): - """Reset readline's completer to be our own.""" - self.readline.set_completer(self.Completer.rlcomplete) - - - def pre_readline(self): - """readline hook to be used at the start of each line. - - It handles auto-indent and text from set_next_input.""" - - if self.rl_do_indent: - self.readline.insert_text(self._indent_current_str()) - if self.rl_next_input is not None: - self.readline.insert_text(self.rl_next_input) - self.rl_next_input = None - - def refill_readline_hist(self): - # Load the last 1000 lines from history - self.readline.clear_history() - stdin_encoding = sys.stdin.encoding or "utf-8" - 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): - try: - if self.multiline_history: - self.readline.add_history(py3compat.unicode_to_str(cell, - stdin_encoding)) - else: - for line in cell.splitlines(): - self.readline.add_history(py3compat.unicode_to_str(line, - stdin_encoding)) - last_cell = cell - - except (TypeError, ValueError) as e: - # The history DB can get corrupted so it returns strings - # containing null bytes, which readline objects to. - warn(("Failed to add string to readline history.\n" - "Error: {}\n" - "Cell: {!r}").format(e, cell)) - - #------------------------------------------------------------------------- - # Things related to the terminal - #------------------------------------------------------------------------- - - @property - def usable_screen_length(self): - if self.screen_length == 0: - return 0 - else: - num_lines_bot = self.separate_in.count('\n')+1 - return self.screen_length - num_lines_bot - - def _term_title_changed(self, name, new_value): - self.init_term_title() - - def init_term_title(self): - # 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) - - #------------------------------------------------------------------------- - # Things related to aliases - #------------------------------------------------------------------------- - - 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': - aliases = [('clear', 'clear'), ('more', 'more'), ('less', 'less'), - ('man', 'man')] - else : - aliases = [] - - for name, cmd in aliases: - self.alias_manager.soft_define_alias(name, cmd) - - #------------------------------------------------------------------------- - # Mainloop and code execution logic - #------------------------------------------------------------------------- - - def mainloop(self, display_banner=None): - """Start the mainloop. - - If an optional banner argument is given, it will override the - internally created default banner. - """ - - with self.builtin_trap, self.display_trap: - - while 1: - try: - self.interact(display_banner=display_banner) - #self.interact_with_readline() - # XXX for testing of a readline-decoupled repl loop, call - # interact_with_readline above - break - except KeyboardInterrupt: - # this should not be necessary, but KeyboardInterrupt - # handling seems rather unpredictable... - self.write("\nKeyboardInterrupt in interact()\n") - - def _replace_rlhist_multiline(self, source_raw, hlen_before_cell): - """Store multiple lines as a single entry in history""" - - # do nothing without readline or disabled multiline - if not self.has_readline or not self.multiline_history: - return hlen_before_cell - - # windows rl has no remove_history_item - if not hasattr(self.readline, "remove_history_item"): - return hlen_before_cell - - # skip empty cells - if not source_raw.rstrip(): - return hlen_before_cell - - # nothing changed do nothing, e.g. when rl removes consecutive dups - hlen = self.readline.get_current_history_length() - if hlen == hlen_before_cell: - return hlen_before_cell - - for i in range(hlen - hlen_before_cell): - self.readline.remove_history_item(hlen - i - 1) - stdin_encoding = get_stream_enc(sys.stdin, 'utf-8') - self.readline.add_history(py3compat.unicode_to_str(source_raw.rstrip(), - stdin_encoding)) - return self.readline.get_current_history_length() - - def interact(self, display_banner=None): - """Closely emulate the interactive Python console.""" - - # batch run -> do not interact - if self.exit_now: - return - - if display_banner is None: - display_banner = self.display_banner - - if isinstance(display_banner, py3compat.string_types): - self.show_banner(display_banner) - elif display_banner: - self.show_banner() - - more = False - - if self.has_readline: - self.readline_startup_hook(self.pre_readline) - hlen_b4_cell = self.readline.get_current_history_length() - else: - hlen_b4_cell = 0 - # exit_now is set by a call to %Exit or %Quit, through the - # ask_exit callback. - - while not self.exit_now: - self.hooks.pre_prompt_hook() - if more: - try: - prompt = ' ...: ' - except: - self.showtraceback() - if self.autoindent: - self.rl_do_indent = True - - else: - try: - prompt = self.separate_in + 'In [{}]: '.format(self.execution_count) - except: - self.showtraceback() - try: - line = self.raw_input(prompt) - if self.exit_now: - # quick exit on sys.std[in|out] close - break - if self.autoindent: - self.rl_do_indent = False - - except KeyboardInterrupt: - #double-guard against keyboardinterrupts during kbdint handling - try: - self.write('\n' + self.get_exception_only()) - source_raw = self.input_splitter.raw_reset() - hlen_b4_cell = \ - self._replace_rlhist_multiline(source_raw, hlen_b4_cell) - more = False - except KeyboardInterrupt: - pass - except EOFError: - if self.autoindent: - self.rl_do_indent = False - if self.has_readline: - self.readline_startup_hook(None) - self.write('\n') - self.exit() - except bdb.BdbQuit: - warn('The Python debugger has exited with a BdbQuit exception.\n' - 'Because of how pdb handles the stack, it is impossible\n' - 'for IPython to properly format this particular exception.\n' - 'IPython will resume normal operation.') - except: - # exceptions here are VERY RARE, but they can be triggered - # asynchronously by signal handlers, for example. - self.showtraceback() - else: - try: - self.input_splitter.push(line) - more = self.input_splitter.push_accepts_more() - except SyntaxError: - # Run the code directly - run_cell takes care of displaying - # the exception. - more = False - if (self.SyntaxTB.last_syntax_error and - self.autoedit_syntax): - self.edit_syntax_error() - if not more: - source_raw = self.input_splitter.raw_reset() - self.run_cell(source_raw, store_history=True) - hlen_b4_cell = \ - self._replace_rlhist_multiline(source_raw, hlen_b4_cell) - - # Turn off the exit flag, so the mainloop can be restarted if desired - self.exit_now = False - - def raw_input(self, prompt=''): - """Write a prompt and read a line. - - The returned line does not include the trailing newline. - When the user enters the EOF key sequence, EOFError is raised. - - Parameters - ---------- - - prompt : str, optional - A string to be printed to prompt the user. - """ - # raw_input expects str, but we pass it unicode sometimes - prompt = py3compat.cast_bytes_py2(prompt) - - try: - line = py3compat.cast_unicode_py2(self.raw_input_original(prompt)) - except ValueError: - warn("\n********\nYou or a %run:ed script called sys.stdin.close()" - " or sys.stdout.close()!\nExiting IPython!\n") - self.ask_exit() - return "" - - # Try to be reasonably smart about not re-indenting pasted input more - # than necessary. We do this by trimming out the auto-indent initial - # spaces, if the user's actual input started itself with whitespace. - if self.autoindent: - if num_ini_spaces(line) > self.indent_current_nsp: - line = line[self.indent_current_nsp:] - self.indent_current_nsp = 0 - - return line - - #------------------------------------------------------------------------- - # 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: - f = open(err.filename) - try: - # This should be inside a display_trap block and I - # think it is. - sys.displayhook(f.read()) - finally: - f.close() - except: - self.showtraceback() - - def _should_recompile(self,e): - """Utility routine for edit_syntax_error""" - - if e.filename in ('','','', - '','', - 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 - - #------------------------------------------------------------------------- - # Things related to exiting - #------------------------------------------------------------------------- - - def ask_exit(self): - """ Ask the shell to exit. Can be overiden and used as a callback. """ - self.exit_now = True - - def exit(self): - """Handle interactive exit. - - This method calls the ask_exit callback.""" - if self.confirm_exit: - if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): - self.ask_exit() - else: - self.ask_exit() - - #------------------------------------------------------------------------- - # Things related to magics - #------------------------------------------------------------------------- - - def init_magics(self): - super(TerminalInteractiveShell, self).init_magics() - self.register_magics(TerminalMagics) - - def showindentationerror(self): - super(TerminalInteractiveShell, self).showindentationerror() - if not self.using_paste_magics: - print("If you want to paste code into IPython, try the " - "%paste and %cpaste magic functions.") - - -InteractiveShellABC.register(TerminalInteractiveShell) +class TerminalInteractiveShell(PromptToolkitShell): + def __init__(self, *args, **kwargs): + warn("This is a deprecated alias for IPython.terminal.ptshell.TerminalInteractiveShell. " + "The terminal interface of this class now uses prompt_toolkit instead of readline.", + DeprecationWarning, stacklevel=2) + PromptToolkitShell.__init__(self, *args, **kwargs) diff --git a/IPython/terminal/magics.py b/IPython/terminal/magics.py new file mode 100644 index 0000000..d5ea51f --- /dev/null +++ b/IPython/terminal/magics.py @@ -0,0 +1,207 @@ +"""Extra magics for terminal use.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from __future__ import print_function + +from logging import error +import os +import sys + +from IPython.core.error import TryNext, UsageError +from IPython.core.inputsplitter import IPythonInputSplitter +from IPython.core.magic import Magics, magics_class, line_magic +from IPython.lib.clipboard import ClipboardEmpty +from IPython.utils.text import SList, strip_email_quotes +from IPython.utils import py3compat + +def get_pasted_lines(sentinel, l_input=py3compat.input, quiet=False): + """ Yield pasted lines until the user enters the given sentinel value. + """ + if not quiet: + print("Pasting code; enter '%s' alone on the line to stop or use Ctrl-D." \ + % sentinel) + prompt = ":" + else: + prompt = "" + while True: + try: + l = py3compat.str_to_unicode(l_input(prompt)) + if l == sentinel: + return + else: + yield l + except EOFError: + print('') + return + + +@magics_class +class TerminalMagics(Magics): + def __init__(self, shell): + super(TerminalMagics, self).__init__(shell) + self.input_splitter = IPythonInputSplitter() + + def store_or_execute(self, block, name): + """ Execute a block, or store it in a variable, per the user's request. + """ + if name: + # If storing it for further editing + self.shell.user_ns[name] = SList(block.splitlines()) + print("Block assigned to '%s'" % name) + else: + b = self.preclean_input(block) + self.shell.user_ns['pasted_block'] = b + self.shell.using_paste_magics = True + try: + self.shell.run_cell(b) + finally: + self.shell.using_paste_magics = False + + def preclean_input(self, block): + lines = block.splitlines() + while lines and not lines[0].strip(): + lines = lines[1:] + return strip_email_quotes('\n'.join(lines)) + + def rerun_pasted(self, name='pasted_block'): + """ Rerun a previously pasted command. + """ + b = self.shell.user_ns.get(name) + + # Sanity checks + if b is None: + raise UsageError('No previous pasted block available') + if not isinstance(b, py3compat.string_types): + raise UsageError( + "Variable 'pasted_block' is not a string, can't execute") + + print("Re-executing '%s...' (%d chars)"% (b.split('\n',1)[0], len(b))) + self.shell.run_cell(b) + + @line_magic + def autoindent(self, parameter_s = ''): + """Toggle autoindent on/off (if available).""" + + self.shell.set_autoindent() + print("Automatic indentation is:",['OFF','ON'][self.shell.autoindent]) + + @line_magic + def cpaste(self, parameter_s=''): + """Paste & execute a pre-formatted code block from clipboard. + + You must terminate the block with '--' (two minus-signs) or Ctrl-D + alone on the line. You can also provide your own sentinel with '%paste + -s %%' ('%%' is the new sentinel for this operation). + + The block is dedented prior to execution to enable execution of method + definitions. '>' and '+' characters at the beginning of a line are + ignored, to allow pasting directly from e-mails, diff files and + doctests (the '...' continuation prompt is also stripped). The + executed block is also assigned to variable named 'pasted_block' for + later editing with '%edit pasted_block'. + + You can also pass a variable name as an argument, e.g. '%cpaste foo'. + This assigns the pasted block to variable 'foo' as string, without + dedenting or executing it (preceding >>> and + is still stripped) + + '%cpaste -r' re-executes the block previously entered by cpaste. + '%cpaste -q' suppresses any additional output messages. + + Do not be alarmed by garbled output on Windows (it's a readline bug). + Just press enter and type -- (and press enter again) and the block + will be what was just pasted. + + IPython statements (magics, shell escapes) are not supported (yet). + + See also + -------- + paste: automatically pull code from clipboard. + + Examples + -------- + :: + + In [8]: %cpaste + Pasting code; enter '--' alone on the line to stop. + :>>> a = ["world!", "Hello"] + :>>> print " ".join(sorted(a)) + :-- + Hello world! + """ + opts, name = self.parse_options(parameter_s, 'rqs:', mode='string') + if 'r' in opts: + self.rerun_pasted() + return + + quiet = ('q' in opts) + + sentinel = opts.get('s', u'--') + block = '\n'.join(get_pasted_lines(sentinel, quiet=quiet)) + self.store_or_execute(block, name) + + @line_magic + def paste(self, parameter_s=''): + """Paste & execute a pre-formatted code block from clipboard. + + The text is pulled directly from the clipboard without user + intervention and printed back on the screen before execution (unless + the -q flag is given to force quiet mode). + + The block is dedented prior to execution to enable execution of method + definitions. '>' and '+' characters at the beginning of a line are + ignored, to allow pasting directly from e-mails, diff files and + doctests (the '...' continuation prompt is also stripped). The + executed block is also assigned to variable named 'pasted_block' for + later editing with '%edit pasted_block'. + + You can also pass a variable name as an argument, e.g. '%paste foo'. + This assigns the pasted block to variable 'foo' as string, without + executing it (preceding >>> and + is still stripped). + + Options: + + -r: re-executes the block previously entered by cpaste. + + -q: quiet mode: do not echo the pasted text back to the terminal. + + IPython statements (magics, shell escapes) are not supported (yet). + + See also + -------- + cpaste: manually paste code into terminal until you mark its end. + """ + opts, name = self.parse_options(parameter_s, 'rq', mode='string') + if 'r' in opts: + self.rerun_pasted() + return + try: + block = self.shell.hooks.clipboard_get() + except TryNext as clipboard_exc: + message = getattr(clipboard_exc, 'args') + if message: + error(message[0]) + else: + error('Could not get text from the clipboard.') + return + except ClipboardEmpty: + raise UsageError("The clipboard appears to be empty") + + # By default, echo back to terminal unless quiet mode is requested + if 'q' not in opts: + write = self.shell.write + write(self.shell.pycolorize(block)) + if not block.endswith('\n'): + write('\n') + write("## -- End pasted text --\n") + + self.store_or_execute(block, name) + + # Class-level: add a '%cls' magic only on Windows + if sys.platform == 'win32': + @line_magic + def cls(self, s): + """Clear screen. + """ + os.system("cls") diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 3c34d7c..8af3627 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -7,8 +7,8 @@ import signal from warnings import warn from IPython.core.error import TryNext -from IPython.core.interactiveshell import InteractiveShell -from IPython.utils.py3compat import cast_unicode_py2, input +from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC +from IPython.utils.py3compat import PY3, cast_unicode_py2, input from IPython.utils.terminal import toggle_set_term_title, set_term_title from IPython.utils.process import abbrev_cwd from traitlets import Bool, Unicode, Dict, Integer, observe, Instance @@ -27,11 +27,29 @@ from pygments.styles import get_style_by_name, get_all_styles from pygments.token import Token from .debugger import TerminalPdb, Pdb +from .magics import TerminalMagics from .pt_inputhooks import get_inputhook_func -from .interactiveshell import get_default_editor, TerminalMagics from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook from .ptutils import IPythonPTCompleter, IPythonPTLexer + +def get_default_editor(): + try: + ed = os.environ['EDITOR'] + if not PY3: + ed = ed.decode() + return ed + 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! + _use_simple_prompt = 'IPY_TEST_SIMPLE_PROMPT' in os.environ or not sys.stdin.isatty() class TerminalInteractiveShell(InteractiveShell): @@ -466,5 +484,7 @@ class TerminalInteractiveShell(InteractiveShell): self._prompts_before = None +InteractiveShellABC.register(TerminalInteractiveShell) + if __name__ == '__main__': TerminalInteractiveShell.instance().interact() diff --git a/IPython/terminal/tests/test_interactivshell.py b/IPython/terminal/tests/test_interactivshell.py index 9437aff..6eebf79 100644 --- a/IPython/terminal/tests/test_interactivshell.py +++ b/IPython/terminal/tests/test_interactivshell.py @@ -1,10 +1,5 @@ # -*- coding: utf-8 -*- -"""Tests for the key interactiveshell module. - -Authors -------- -* Julian Taylor -""" +"""Tests for the TerminalInteractiveShell and related pieces.""" #----------------------------------------------------------------------------- # Copyright (C) 2011 The IPython Development Team # @@ -12,17 +7,10 @@ Authors # the file COPYING, distributed as part of this software. #----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- -# stdlib import sys -import types import unittest from IPython.core.inputtransformer import InputTransformer -from IPython.testing.decorators import skipif -from IPython.utils import py3compat from IPython.testing import tools as tt # Decorator for interaction loop tests ----------------------------------------- @@ -38,22 +26,22 @@ class mock_input_helper(object): self.ip = get_ipython() def __enter__(self): - self.orig_raw_input = self.ip.raw_input - self.ip.raw_input = self.fake_input + self.orig_prompt_for_code = self.ip.prompt_for_code + self.ip.prompt_for_code = self.fake_input return self def __exit__(self, etype, value, tb): - self.ip.raw_input = self.orig_raw_input + self.ip.prompt_for_code = self.orig_prompt_for_code - def fake_input(self, prompt): + def fake_input(self): try: return next(self.testgen) except StopIteration: - self.ip.exit_now = True + self.ip.keep_running = False return u'' except: self.exception = sys.exc_info() - self.ip.exit_now = True + self.ip.keep_running = False return u'' def mock_input(testfunc): @@ -65,7 +53,7 @@ def mock_input(testfunc): def test_method(self): testgen = testfunc(self) with mock_input_helper(testgen) as mih: - mih.ip.interact(display_banner=False) + mih.ip.interact() if mih.exception is not None: # Re-raise captured exception @@ -84,148 +72,6 @@ class InteractiveShellTestCase(unittest.TestCase): """Get last n readline history entries as a list""" return [rl.get_history_item(rl.get_current_history_length() - x) for x in range(n - 1, -1, -1)] - - def test_runs_without_rl(self): - """Test that function does not throw without readline""" - ip = get_ipython() - ip.has_readline = False - ip.readline = None - ip._replace_rlhist_multiline(u'source', 0) - - @skipif(not get_ipython().has_readline, 'no readline') - def test_runs_without_remove_history_item(self): - """Test that function does not throw on windows without - remove_history_item""" - ip = get_ipython() - if hasattr(ip.readline, 'remove_history_item'): - del ip.readline.remove_history_item - ip._replace_rlhist_multiline(u'source', 0) - - @skipif(not get_ipython().has_readline, 'no readline') - @skipif(not hasattr(get_ipython().readline, 'remove_history_item'), - 'no remove_history_item') - def test_replace_multiline_hist_disabled(self): - """Test that multiline replace does nothing if disabled""" - ip = get_ipython() - ip.multiline_history = False - - ghist = [u'line1', u'line2'] - for h in ghist: - ip.readline.add_history(h) - hlen_b4_cell = ip.readline.get_current_history_length() - hlen_b4_cell = ip._replace_rlhist_multiline(u'sourc€\nsource2', - hlen_b4_cell) - - self.assertEqual(ip.readline.get_current_history_length(), - hlen_b4_cell) - hist = self.rl_hist_entries(ip.readline, 2) - self.assertEqual(hist, ghist) - - @skipif(not get_ipython().has_readline, 'no readline') - @skipif(not hasattr(get_ipython().readline, 'remove_history_item'), - 'no remove_history_item') - def test_replace_multiline_hist_adds(self): - """Test that multiline replace function adds history""" - ip = get_ipython() - - hlen_b4_cell = ip.readline.get_current_history_length() - hlen_b4_cell = ip._replace_rlhist_multiline(u'sourc€', hlen_b4_cell) - - self.assertEqual(hlen_b4_cell, - ip.readline.get_current_history_length()) - - @skipif(not get_ipython().has_readline, 'no readline') - @skipif(not hasattr(get_ipython().readline, 'remove_history_item'), - 'no remove_history_item') - def test_replace_multiline_hist_keeps_history(self): - """Test that multiline replace does not delete history""" - ip = get_ipython() - ip.multiline_history = True - - ghist = [u'line1', u'line2'] - for h in ghist: - ip.readline.add_history(h) - - #start cell - hlen_b4_cell = ip.readline.get_current_history_length() - # nothing added to rl history, should do nothing - hlen_b4_cell = ip._replace_rlhist_multiline(u'sourc€\nsource2', - hlen_b4_cell) - - self.assertEqual(ip.readline.get_current_history_length(), - hlen_b4_cell) - hist = self.rl_hist_entries(ip.readline, 2) - self.assertEqual(hist, ghist) - - - @skipif(not get_ipython().has_readline, 'no readline') - @skipif(not hasattr(get_ipython().readline, 'remove_history_item'), - 'no remove_history_item') - def test_replace_multiline_hist_replaces_twice(self): - """Test that multiline entries are replaced twice""" - ip = get_ipython() - ip.multiline_history = True - - ip.readline.add_history(u'line0') - #start cell - hlen_b4_cell = ip.readline.get_current_history_length() - ip.readline.add_history('l€ne1') - ip.readline.add_history('line2') - #replace cell with single line - hlen_b4_cell = ip._replace_rlhist_multiline(u'l€ne1\nline2', - hlen_b4_cell) - ip.readline.add_history('l€ne3') - ip.readline.add_history('line4') - #replace cell with single line - hlen_b4_cell = ip._replace_rlhist_multiline(u'l€ne3\nline4', - hlen_b4_cell) - - self.assertEqual(ip.readline.get_current_history_length(), - hlen_b4_cell) - hist = self.rl_hist_entries(ip.readline, 3) - expected = [u'line0', u'l€ne1\nline2', u'l€ne3\nline4'] - # perform encoding, in case of casting due to ASCII locale - enc = sys.stdin.encoding or "utf-8" - expected = [ py3compat.unicode_to_str(e, enc) for e in expected ] - self.assertEqual(hist, expected) - - - @skipif(not get_ipython().has_readline, 'no readline') - @skipif(not hasattr(get_ipython().readline, 'remove_history_item'), - 'no remove_history_item') - def test_replace_multiline_hist_replaces_empty_line(self): - """Test that multiline history skips empty line cells""" - ip = get_ipython() - ip.multiline_history = True - - ip.readline.add_history(u'line0') - #start cell - hlen_b4_cell = ip.readline.get_current_history_length() - ip.readline.add_history('l€ne1') - ip.readline.add_history('line2') - hlen_b4_cell = ip._replace_rlhist_multiline(u'l€ne1\nline2', - hlen_b4_cell) - ip.readline.add_history('') - hlen_b4_cell = ip._replace_rlhist_multiline(u'', hlen_b4_cell) - ip.readline.add_history('l€ne3') - hlen_b4_cell = ip._replace_rlhist_multiline(u'l€ne3', hlen_b4_cell) - ip.readline.add_history(' ') - hlen_b4_cell = ip._replace_rlhist_multiline(' ', hlen_b4_cell) - ip.readline.add_history('\t') - ip.readline.add_history('\t ') - hlen_b4_cell = ip._replace_rlhist_multiline('\t', hlen_b4_cell) - ip.readline.add_history('line4') - hlen_b4_cell = ip._replace_rlhist_multiline(u'line4', hlen_b4_cell) - - self.assertEqual(ip.readline.get_current_history_length(), - hlen_b4_cell) - hist = self.rl_hist_entries(ip.readline, 4) - # expect no empty cells in history - expected = [u'line0', u'l€ne1\nline2', u'l€ne3', u'line4'] - # perform encoding, in case of casting due to ASCII locale - enc = sys.stdin.encoding or "utf-8" - expected = [ py3compat.unicode_to_str(e, enc) for e in expected ] - self.assertEqual(hist, expected) @mock_input def test_inputtransformer_syntaxerror(self): @@ -264,23 +110,6 @@ class SyntaxErrorTransformer(InputTransformer): pass class TerminalMagicsTestCase(unittest.TestCase): - def test_paste_magics_message(self): - """Test that an IndentationError while using paste magics doesn't - trigger a message about paste magics and also the opposite.""" - - ip = get_ipython() - s = ('for a in range(5):\n' - 'print(a)') - - tm = ip.magics_manager.registry['TerminalMagics'] - with tt.AssertPrints("If you want to paste code into IPython, try the " - "%paste and %cpaste magic functions."): - ip.run_cell(s) - - with tt.AssertNotPrints("If you want to paste code into IPython, try the " - "%paste and %cpaste magic functions."): - tm.store_or_execute(s, name=None) - def test_paste_magics_blankline(self): """Test that code with a blank line doesn't get split (gh-3246).""" ip = get_ipython()