diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 535eed4..d6a6557 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -308,7 +308,7 @@ class InteractiveShell(SingletonConfigurable, Magic): """ ) multiline_history = CBool(sys.platform != 'win32', config=True, - help="Store multiple line spanning cells as a single entry in history." + help="Save multi-line entries as one entry in readline history" ) prompt_in1 = Unicode('In [\\#]: ', config=True) diff --git a/IPython/frontend/terminal/interactiveshell.py b/IPython/frontend/terminal/interactiveshell.py index 6767dbb..a57ecca 100644 --- a/IPython/frontend/terminal/interactiveshell.py +++ b/IPython/frontend/terminal/interactiveshell.py @@ -231,13 +231,30 @@ class TerminalInteractiveShell(InteractiveShell): def _replace_rlhist_multiline(self, source_raw, hlen_before_cell): """Store multiple lines as a single entry in history""" - if self.multiline_history and self.has_readline: - hlen = self.readline.get_current_history_length() - for i in range(hlen - hlen_before_cell): - self.readline.remove_history_item(hlen - i - 1) - stdin_encoding = sys.stdin.encoding or "utf-8" - self.readline.add_history(py3compat.unicode_to_str(source_raw.rstrip(), - stdin_encoding)) + + # 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 = sys.stdin.encoding or "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.""" @@ -255,13 +272,15 @@ class TerminalInteractiveShell(InteractiveShell): self.show_banner() more = False - hlen_before_cell = self.readline.get_current_history_length() # Mark activity in the builtins __builtin__.__dict__['__IPYTHON__active'] += 1 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. @@ -293,8 +312,8 @@ class TerminalInteractiveShell(InteractiveShell): try: self.write('\nKeyboardInterrupt\n') source_raw = self.input_splitter.source_raw_reset()[1] - self._replace_rlhist_multiline(source_raw, hlen_before_cell) - hlen_before_cell = self.readline.get_current_history_length() + hlen_b4_cell = \ + self._replace_rlhist_multiline(source_raw, hlen_b4_cell) more = False except KeyboardInterrupt: pass @@ -322,9 +341,9 @@ class TerminalInteractiveShell(InteractiveShell): self.edit_syntax_error() if not more: source_raw = self.input_splitter.source_raw_reset()[1] - self._replace_rlhist_multiline(source_raw, hlen_before_cell) - hlen_before_cell = self.readline.get_current_history_length() self.run_cell(source_raw, store_history=True) + hlen_b4_cell = \ + self._replace_rlhist_multiline(source_raw, hlen_b4_cell) # We are off again... __builtin__.__dict__['__IPYTHON__active'] -= 1 diff --git a/IPython/frontend/terminal/tests/test_interactivshell.py b/IPython/frontend/terminal/tests/test_interactivshell.py new file mode 100644 index 0000000..925f531 --- /dev/null +++ b/IPython/frontend/terminal/tests/test_interactivshell.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +"""Tests for the key interactiveshell module. + +Authors +------- +* Julian Taylor +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2011 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- +# stdlib +import unittest + +from IPython.testing.decorators import skipif + +class InteractiveShellTestCase(unittest.TestCase): + def rl_hist_entries(self, rl, n): + """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.assertEquals(ip.readline.get_current_history_length(), + hlen_b4_cell) + hist = self.rl_hist_entries(ip.readline, 2) + self.assertEquals(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.assertEquals(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.assertEquals(ip.readline.get_current_history_length(), + hlen_b4_cell) + hist = self.rl_hist_entries(ip.readline, 2) + self.assertEquals(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.assertEquals(ip.readline.get_current_history_length(), + hlen_b4_cell) + hist = self.rl_hist_entries(ip.readline, 3) + self.assertEquals(hist, ['line0', 'l€ne1\nline2', 'l€ne3\nline4']) + + + @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.assertEquals(ip.readline.get_current_history_length(), + hlen_b4_cell) + hist = self.rl_hist_entries(ip.readline, 4) + # expect no empty cells in history + self.assertEquals(hist, ['line0', 'l€ne1\nline2', 'l€ne3', 'line4'])