diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index 523e99d4076..d2f9d775a44 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -1331,7 +1331,9 @@ def post_visible_setup(self): # Show history file if no console is visible if not self.ipyconsole.isvisible: - self.historylog.add_history(get_conf_path('history.py')) + self.historylog.add_history( + self.ipyconsole.history_title, + self.ipyconsole.get_current_shellwidget()._history) if self.open_project: self.projects.open_project(self.open_project) diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index b89346427dc..565102d6b1c 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -1792,6 +1792,38 @@ def test_render_issue(): assert test_description in test_issue_2 assert test_traceback in test_issue_2 +@flaky(max_runs=3) +@pytest.mark.slow +def test_history(main_window, qtbot): + """Test History""" + # ---- Setup ---- + # Create 3 clients and wait until the window is fully up + for i in range(3): + main_window.ipyconsole.create_new_client() + + clients = main_window.ipyconsole.get_clients() + for client in clients: + shell = client.shellwidget + qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) + + #Execute some code in each widget + commands = [] + for i, client in enumerate(clients): + shell = client.shellwidget + commands.append('i = {i}\n'.format(i=i)) + shell.execute(commands[-1]) + qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) + print(main_window.historylog.editors[0].toPlainText()[-10:]) + + commands_text = ''.join(commands) + + index = main_window.historylog.filenames.index(main_window.ipyconsole.history_title) + assert index == 0 + text = main_window.historylog.editors[index].toPlainText()[-len(commands_text):] + history = main_window.historylog.histories[index][-len(clients):] + + assert history == commands + assert text == commands_text @pytest.mark.slow @flaky(max_runs=3) diff --git a/spyder/config/base.py b/spyder/config/base.py index c4f8b59b83d..778cd011af7 100644 --- a/spyder/config/base.py +++ b/spyder/config/base.py @@ -503,7 +503,7 @@ def running_in_mac_app(): #============================================================================== SAVED_CONFIG_FILES = ('help', 'onlinehelp', 'path', 'pylint.results', 'spyder.ini', 'temp.py', 'temp.spydata', 'template.py', - 'history.py', 'history_internal.py', 'workingdir', + 'history_internal.py', 'workingdir', '.projects', '.spyproject', '.ropeproject', 'monitor.log', 'monitor_debug.log', 'rope.log', 'langconfig', 'spyder.lock') diff --git a/spyder/plugins/console/plugin.py b/spyder/plugins/console/plugin.py index c22fcd481e3..2fb57221fe4 100644 --- a/spyder/plugins/console/plugin.py +++ b/spyder/plugins/console/plugin.py @@ -103,12 +103,6 @@ def __init__(self, parent=None, namespace=None, commands=[], message=None, self.dismiss_error = False #------ Private API -------------------------------------------------------- - def set_historylog(self, historylog): - """Bind historylog instance to this console - Not used anymore since v2.0""" - historylog.add_history(self.shell.history_filename) - self.shell.append_to_history.connect(historylog.append_to_history) - def set_help(self, help_plugin): """Bind help instance to this console""" self.shell.help = help_plugin diff --git a/spyder/plugins/history/plugin.py b/spyder/plugins/history/plugin.py index e1d54c66ce8..d125f0bb958 100644 --- a/spyder/plugins/history/plugin.py +++ b/spyder/plugins/history/plugin.py @@ -50,6 +50,7 @@ def __init__(self, parent): self.editors = [] self.filenames = [] + self.histories = [] # Initialize plugin actions, toolbutton and general signals self.initialize_plugin() @@ -89,14 +90,14 @@ def get_plugin_title(self): def get_plugin_icon(self): """Return widget icon.""" return ima.icon('history') - + def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.tabwidget.currentWidget() - + def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True @@ -129,12 +130,11 @@ def get_plugin_actions(self): def on_first_registration(self): """Action to be performed on first plugin registration""" self.main.tabify_plugins(self.main.ipyconsole, self) - + def register_plugin(self): """Register plugin in Spyder's main window""" self.focus_changed.connect(self.main.plugin_focus_changed) self.main.add_dockwidget(self) -# self.main.console.set_historylog(self) self.main.console.shell.refresh.connect(self.refresh_plugin) def update_font(self): @@ -178,21 +178,18 @@ def move_tab(self, index_from, index_to): self.editors.insert(index_to, editor) #------ Public API --------------------------------------------------------- - def add_history(self, filename): + def add_history(self, filename, history_list): """ Add new history tab Slot for add_history signal emitted by shell instance """ - filename = encoding.to_unicode_from_fs(filename) if filename in self.filenames: return + #Limit history depth + history_list = history_list[-self.get_option('max_entries'):] editor = codeeditor.CodeEditor(self) - if osp.splitext(filename)[1] == '.py': - language = 'py' - else: - language = 'bat' editor.setup_editor(linenumbers=self.get_option('line_numbers'), - language=language, + language='py', scrollflagarea=False) editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) @@ -200,26 +197,10 @@ def add_history(self, filename): editor.set_font( self.get_plugin_font(), color_scheme ) editor.toggle_wrap_mode( self.get_option('wrap') ) - # Avoid a possible error when reading the history file - try: - text, _ = encoding.read(filename) - except (IOError, OSError): - text = "# Previous history could not be read from disk, sorry\n\n" - text = normalize_eols(text) - linebreaks = [m.start() for m in re.finditer('\n', text)] - maxNline = self.get_option('max_entries') - if len(linebreaks) > maxNline: - text = text[linebreaks[-maxNline - 1] + 1:] - # Avoid an error when trying to write the trimmed text to - # disk. - # See issue 9093 - try: - encoding.write(text, filename) - except (IOError, OSError): - pass - editor.set_text(text) + editor.set_text('\n'.join(history_list) + '\n') editor.set_cursor_position('eof') + self.histories.append(history_list) self.editors.append(editor) self.filenames.append(filename) index = self.tabwidget.addTab(editor, osp.basename(filename)) @@ -237,11 +218,28 @@ def append_to_history(self, filename, command): filename = to_text_string(filename.toUtf8(), 'utf-8') command = to_text_string(command) index = self.filenames.index(filename) + self.histories[index].append(command) self.editors[index].append(command) if self.get_option('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) - + + def set_history(self, filename, history_list): + """ + Update the history for history filename + Slot for set_history signal emitted by shell instance + """ + #Limit history depth + history_list = history_list[-self.get_option('max_entries'):] + if not is_text_string(filename): # filename is a QString + filename = to_text_string(filename.toUtf8(), 'utf-8') + index = self.filenames.index(filename) + self.histories[index] = history_list + self.editors[index].set_text('\n'.join(history_list) + '\n') + if self.get_option('go_to_eof'): + self.editors[index].set_cursor_position('eof') + self.tabwidget.setCurrentIndex(index) + @Slot() def change_history_depth(self): "Change history max entries""" diff --git a/spyder/plugins/history/tests/test_plugin.py b/spyder/plugins/history/tests/test_plugin.py index 22dd420c8d2..2d63d494319 100644 --- a/spyder/plugins/history/tests/test_plugin.py +++ b/spyder/plugins/history/tests/test_plugin.py @@ -64,12 +64,13 @@ def historylog_with_tab(historylog, mocker, monkeypatch): monkeypatch.setattr(history.HistoryLog, 'get_option', get_option) monkeypatch.setattr(history.HistoryLog, 'set_option', set_option) + commands = [] # Create tab for page. hl.set_option('wrap', False) hl.set_option('line_numbers', False) hl.set_option('max_entries', 100) hl.set_option('go_to_eof', True) - hl.add_history('test_history.py') + hl.add_history('test_history.py', commands) return hl #============================================================================== @@ -105,6 +106,7 @@ def test_init(historylog): hl = historylog assert hl.editors == [] assert hl.filenames == [] + assert hl.histories == [] assert len(hl.plugin_actions) == 5 assert len(hl.tabwidget.menu.actions()) == 5 assert len(hl.tabwidget.cornerWidget().menu().actions()) == 5 @@ -130,11 +132,11 @@ def test_add_history(historylog, mocker, monkeypatch): # Add one file. tab1 = 'test_history.py' - text1 = 'a = 5\nb= 10\na + b\n' + commands = ["This", "is", "a", "list", "of", "commands"] + hl.set_option('line_numbers', False) hl.set_option('wrap', False) - history.encoding.read.return_value = (text1, '') - hl.add_history(tab1) + hl.add_history(tab1, commands) # Check tab and editor were created correctly. assert len(hle) == 1 assert hl.filenames == [tab1] @@ -143,20 +145,19 @@ def test_add_history(historylog, mocker, monkeypatch): assert hle[0].wordWrapMode() == QTextOption.NoWrap assert hl.tabwidget.tabText(0) == tab1 assert hl.tabwidget.tabToolTip(0) == tab1 + assert hl.histories[0] == commands hl.set_option('line_numbers', True) hl.set_option('wrap', True) # Try to add same file -- does not process filename again, so # linenumbers and wrap doesn't change. - hl.add_history(tab1) + hl.add_history(tab1, commands) assert hl.tabwidget.currentIndex() == 0 assert not hl.editors[0].linenumberarea.isVisible() # Add another file. tab2 = 'history2.js' - text2 = 'random text\nspam line\n\n\n\n' - history.encoding.read.return_value = (text2, '') - hl.add_history(tab2) + hl.add_history(tab2, commands) # Check second tab and editor were created correctly. assert len(hle) == 2 assert hl.filenames == [tab1, tab2] @@ -165,6 +166,7 @@ def test_add_history(historylog, mocker, monkeypatch): assert hle[1].wordWrapMode() == QTextOption.WrapAtWordBoundaryOrAnywhere assert hl.tabwidget.tabText(1) == tab2 assert hl.tabwidget.tabToolTip(1) == tab2 + assert hl.histories[1] == commands assert hl.filenames == [tab1, tab2] @@ -173,13 +175,14 @@ def test_add_history(historylog, mocker, monkeypatch): assert hle[0].is_python() assert hle[0].isReadOnly() assert not hle[0].isVisible() - assert hle[0].toPlainText() == text1 + assert hle[0].toPlainText() == '\n'.join(commands) + '\n' - assert not hle[1].supported_language - assert not hle[1].is_python() + # The history is loaded from iPython so it has to be python + # assert not hle[1].supported_language + # assert not hle[1].is_python() assert hle[1].isReadOnly() assert hle[1].isVisible() - assert hle[1].toPlainText() == text2 + assert hle[1].toPlainText() == '\n'.join(commands) + '\n' def test_append_to_history(historylog_with_tab, mocker): @@ -195,7 +198,7 @@ def test_append_to_history(historylog_with_tab, mocker): # Force cursor to the beginning of the file. hl.editors[0].set_cursor_position('sof') hl.append_to_history('test_history.py', 'import re\n') - assert hl.editors[0].toPlainText() == 'import re\n' + assert hl.editors[0].toPlainText() == '\nimport re\n' assert hl.tabwidget.currentIndex() == 0 # Cursor moved to end. assert hl.editors[0].is_cursor_at_end() @@ -206,7 +209,7 @@ def test_append_to_history(historylog_with_tab, mocker): # Force cursor to the beginning of the file. hl.editors[0].set_cursor_position('sof') hl.append_to_history('test_history.py', 'a = r"[a-z]"\n') - assert hl.editors[0].toPlainText() == 'import re\na = r"[a-z]"\n' + assert hl.editors[0].toPlainText() == '\nimport re\na = r"[a-z]"\n' # Cursor not at end. assert not hl.editors[0].is_cursor_at_end() diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py index e0e32526c4e..4133677259b 100644 --- a/spyder/plugins/ipythonconsole/plugin.py +++ b/spyder/plugins/ipythonconsole/plugin.py @@ -110,6 +110,7 @@ def __init__(self, parent, testing=False, test_dir=None, self.tabwidget = None self.menu_actions = None + self.history_title = "History" self.master_clients = 0 self.clients = [] self.filenames = [] @@ -642,7 +643,6 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False, reset_warning = self.get_option('show_reset_namespace_warning') ask_before_restart = self.get_option('ask_before_restart') client = ClientWidget(self, id_=client_id, - history_filename=get_conf_path('history.py'), config_options=self.config_options(), additional_options=self.additional_options( is_pylab=is_pylab, @@ -917,9 +917,18 @@ def register_client(self, client, give_focus=True): # Connect client to our history log if self.main.historylog is not None: - self.main.historylog.add_history(client.history_filename) - client.append_to_history.connect( - self.main.historylog.append_to_history) + self.main.historylog.add_history( + self.history_title, + shellwidget.history_tail( + self.main.historylog.get_option('max_entries'))) + # To save history + shellwidget.sig_new_history.connect( + lambda history: self.main.historylog.set_history( + self.history_title, history)) + shellwidget.executing.connect( + lambda command: self.main.historylog.append_to_history( + self.history_title, command)) + # Set font for client client.set_font( self.get_plugin_font() ) @@ -1442,7 +1451,6 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey, client = ClientWidget(self, id_=client_id, given_name=given_name, - history_filename=get_conf_path('history.py'), config_options=self.config_options(), additional_options=self.additional_options(), interpreter_versions=self.interpreter_versions(), diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py index 53470ed7a1a..25777e0f461 100644 --- a/spyder/plugins/ipythonconsole/widgets/client.py +++ b/spyder/plugins/ipythonconsole/widgets/client.py @@ -44,7 +44,6 @@ MENU_SEPARATOR) from spyder.py3compat import to_text_string from spyder.plugins.ipythonconsole.widgets import ShellWidget -from spyder.widgets.mixins import SaveHistoryMixin from spyder.plugins.variableexplorer.widgets.collectionseditor import ( CollectionsEditor) @@ -86,7 +85,7 @@ def background(f): #----------------------------------------------------------------------------- # Client widget #----------------------------------------------------------------------------- -class ClientWidget(QWidget, SaveHistoryMixin): +class ClientWidget(QWidget): """ Client widget for the IPython Console @@ -98,10 +97,7 @@ class ClientWidget(QWidget, SaveHistoryMixin): INITHISTORY = ['# -*- coding: utf-8 -*-', '# *** Spyder Python Console History Log ***',] - append_to_history = Signal(str, str) - - def __init__(self, plugin, id_, - history_filename, config_options, + def __init__(self, plugin, id_, config_options, additional_options, interpreter_versions, connection_file=None, hostname=None, menu_actions=None, slave=False, @@ -112,7 +108,6 @@ def __init__(self, plugin, id_, ask_before_restart=True, css_path=None): super(ClientWidget, self).__init__(plugin) - SaveHistoryMixin.__init__(self, history_filename) # --- Init attrs self.id_ = id_ @@ -131,7 +126,6 @@ def __init__(self, plugin, id_, self.stop_button = None self.reset_button = None self.stop_icon = ima.icon('stop') - self.history = [] self.allow_rename = True self.stderr_dir = None self.is_error_shown = False @@ -246,16 +240,10 @@ def configure_shellwidget(self, give_focus=True): # Set exit callback self.shellwidget.set_exit_callback() - # To save history - self.shellwidget.executing.connect(self.add_to_history) - # For Mayavi to run correctly self.shellwidget.executing.connect( self.shellwidget.set_backend_for_mayavi) - # To update history after execution - self.shellwidget.executed.connect(self.update_history) - # To update the Variable Explorer after execution self.shellwidget.executed.connect( self.shellwidget.refresh_namespacebrowser) @@ -599,9 +587,6 @@ def reset_namespace(self): self.shellwidget.reset_namespace(warning=self.reset_warning, message=True) - def update_history(self): - self.history = self.shellwidget._history - @Slot(object) def show_syspath(self, syspath): """Show sys.path contents.""" diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py index 5066a28969d..c47b714b3ac 100644 --- a/spyder/plugins/ipythonconsole/widgets/shell.py +++ b/spyder/plugins/ipythonconsole/widgets/shell.py @@ -58,6 +58,7 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget, sig_is_spykernel = Signal(object) sig_kernel_restarted = Signal(str) sig_prompt_ready = Signal() + sig_new_history = Signal(list) # For global working directory sig_change_cwd = Signal(str) @@ -510,6 +511,11 @@ def _prompt_started_hook(self): self._highlighter.highlighting_on = True self.sig_prompt_ready.emit() + def _set_history(self, history): + """ Send a signal when history is changed.""" + super(ShellWidget, self)._set_history(history) + self.sig_new_history.emit(history) + #---- Qt methods ---------------------------------------------------------- def focusInEvent(self, event): """Reimplement Qt method to send focus change notification"""