From 40eb8e654f75953c35fb99bf5d06db18807cd256 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 25 Apr 2023 11:41:08 +0100 Subject: [PATCH 1/6] Move imports for Spyder 6 compatibility --- spyder_notebook/widgets/main_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_notebook/widgets/main_widget.py b/spyder_notebook/widgets/main_widget.py index 0699e3d2..162f7f86 100644 --- a/spyder_notebook/widgets/main_widget.py +++ b/spyder_notebook/widgets/main_widget.py @@ -14,7 +14,7 @@ # Spyder imports from spyder.api.widgets.main_widget import PluginMainWidget from spyder.config.gui import is_dark_interface -from spyder.utils.switcher import shorten_paths +from spyder.plugins.switcher.utils import shorten_paths # Local imports from spyder_notebook.utils.localization import _ From 95af6252dc008a657c20cdda4f5c51112a1e12b7 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 25 Apr 2023 11:42:26 +0100 Subject: [PATCH 2/6] Remove focus_changed signal for Spyder 6 compatibility This signal was removed in Spyder 6 and AFAICT it did not have any use. --- spyder_notebook/notebookplugin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index de707768..1fd5a1e5 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -33,10 +33,6 @@ class NotebookPlugin(SpyderDockablePlugin): WIDGET_CLASS = NotebookMainWidget CONF_WIDGET_CLASS = NotebookConfigPage - # ---- Signals - # ------------------------------------------------------------------------ - focus_changed = Signal() - # ---- SpyderDockablePlugin API # ------------------------------------------------------------------------ @staticmethod @@ -54,8 +50,8 @@ def get_icon(self): return self.create_icon('notebook') def on_initialize(self): - """Register plugin in Spyder's main window.""" - self.focus_changed.connect(self.main.plugin_focus_changed) + """Set up the plugin; does nothing.""" + pass @on_plugin_available(plugin=Plugins.Preferences) def on_preferences_available(self): From b54b1991c98f0433fb0ada5b77e2bb259b0ea9d4 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 25 Apr 2023 21:20:25 +0100 Subject: [PATCH 3/6] Reset Spyder configuration before every test --- conftest.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..131e1454 --- /dev/null +++ b/conftest.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# + +"""Configuration file for Pytest.""" + +# Standard library imports +import os + +# To activate/deactivate certain things in Spyder when running tests. +# NOTE: Please leave this before any other import here!! +os.environ['SPYDER_PYTEST'] = 'True' + +# Third-party imports +import pytest + + +@pytest.fixture(autouse=True) +def reset_conf_before_test(): + from spyder.config.manager import CONF + CONF.reset_to_defaults(notification=False) From 30348647bea39619238fbcf8e19bcac64c85f9f0 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 25 Apr 2023 21:21:20 +0100 Subject: [PATCH 4/6] Install Spyder from Git in GitHub CI tests --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 682230d3..4e483d48 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -15,7 +15,7 @@ jobs: matrix: OS: ['ubuntu', 'macos', 'windows'] PYTHON_VERSION: ['3.8', '3.9', '3.10'] - SPYDER_SOURCE: ['conda'] + SPYDER_SOURCE: ['git'] name: ${{ matrix.OS }} py${{ matrix.PYTHON_VERSION }} spyder-from-${{ matrix.SPYDER_SOURCE }} runs-on: ${{ matrix.OS }}-latest env: From fb6c9334f8444934a2cc0e13607a2467a727b98c Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 26 Apr 2023 12:35:37 +0100 Subject: [PATCH 5/6] Move interaction with switcher from widget to plugin --- spyder_notebook/notebookplugin.py | 72 ++++++++++++++++++++++++-- spyder_notebook/tests/test_plugin.py | 1 - spyder_notebook/widgets/main_widget.py | 61 ---------------------- 3 files changed, 69 insertions(+), 65 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 1fd5a1e5..6c5db88c 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -5,13 +5,14 @@ """Notebook plugin.""" -# Qt imports -from qtpy.QtCore import Signal +# Standard library imports +import os.path as osp # Spyder imports from spyder.api.plugins import Plugins, SpyderDockablePlugin from spyder.api.plugin_registration.decorators import ( on_plugin_available, on_plugin_teardown) +from spyder.plugins.switcher.utils import shorten_paths # Local imports from spyder_notebook.config import CONF_DEFAULTS, CONF_VERSION @@ -25,7 +26,7 @@ class NotebookPlugin(SpyderDockablePlugin): NAME = 'notebook' REQUIRES = [Plugins.Preferences] - OPTIONAL = [Plugins.IPythonConsole] + OPTIONAL = [Plugins.IPythonConsole, Plugins.Switcher] TABIFY = [Plugins.Editor] CONF_SECTION = NAME CONF_DEFAULTS = CONF_DEFAULTS @@ -63,6 +64,12 @@ def on_ipyconsole_available(self): self.get_widget().sig_open_console_requested.connect( self._open_console) + @on_plugin_available(plugin=Plugins.Switcher) + def on_switcher_available(self): + switcher = self.get_plugin(Plugins.Switcher) + switcher.sig_mode_selected.connect(self._handle_switcher_modes) + switcher.sig_item_selected.connect(self._handle_switcher_selection) + @on_plugin_teardown(plugin=Plugins.Preferences) def on_preferences_teardown(self): preferences = self.get_plugin(Plugins.Preferences) @@ -73,6 +80,12 @@ def on_ipyconsole_teardown(self): self.get_widget().sig_open_console_requested.disconnect( self._open_console) + @on_plugin_teardown(plugin=Plugins.Switcher) + def on_switcher_teardown(self): + switcher = self.get_plugin(Plugins.Switcher) + switcher.sig_mode_selected.disconnect(self._handle_switcher_modes) + switcher.sig_item_selected.disconnect(self._handle_switcher_selection) + def on_mainwindow_visible(self): self.get_widget().open_previous_session() @@ -88,3 +101,56 @@ def _open_console(self, kernel_id, tab_name): ipyclient = ipyconsole.get_current_client() ipyclient.allow_rename = False ipyconsole.rename_client_tab(ipyclient, tab_name) + + def _handle_switcher_modes(self, mode): + """ + Populate switcher with opened notebooks. + + List the file names of the opened notebooks with their directories in + the switcher. Only handle file mode, where `mode` is empty string. + """ + if mode != '': + return + + tabwidget = self.get_widget().tabwidget + clients = [tabwidget.widget(i) for i in range(tabwidget.count())] + paths = [client.get_filename() for client in clients] + is_unsaved = [False for client in clients] + short_paths = shorten_paths(paths, is_unsaved) + icon = self.create_icon('notebook') + section = self.get_name() + switcher = self.get_plugin(Plugins.Switcher) + + for path, short_path, client in zip(paths, short_paths, clients): + title = osp.basename(path) + description = osp.dirname(path) + if len(path) > 75: + description = short_path + is_last_item = (client == clients[-1]) + + switcher.add_item( + title=title, + description=description, + icon=icon, + section=section, + data=client, + last_item=is_last_item + ) + + def _handle_switcher_selection(self, item, mode, search_text): + """ + Handle user selecting item in switcher. + + If the selected item is not in the section of the switcher that + corresponds to this plugin, then ignore it. Otherwise, switch to + selected item in notebook plugin and hide the switcher. + """ + if item.get_section() != self.get_title(): + return + + client = item.get_data() + tabwidget = self.get_widget().tabwidget + tabwidget.setCurrentIndex(tabwidget.indexOf(client)) + self.switch_to_plugin() + switcher = self.get_plugin(Plugins.Switcher) + switcher.hide() diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index aec5386b..e50988f1 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -95,7 +95,6 @@ def is_kernel_up(kernel_id, sessions_url): class MainMock(QMainWindow): def __init__(self): super().__init__() - self.switcher = Mock() self.main = self self.resize(640, 480) diff --git a/spyder_notebook/widgets/main_widget.py b/spyder_notebook/widgets/main_widget.py index 162f7f86..e403d364 100644 --- a/spyder_notebook/widgets/main_widget.py +++ b/spyder_notebook/widgets/main_widget.py @@ -4,9 +4,6 @@ # Licensed under the terms of the MIT License # (see LICENSE.txt for details) -# Standard library imports -import os.path as osp - # Third-party imports from qtpy.QtCore import Signal from qtpy.QtWidgets import QMessageBox, QVBoxLayout @@ -14,7 +11,6 @@ # Spyder imports from spyder.api.widgets.main_widget import PluginMainWidget from spyder.config.gui import is_dark_interface -from spyder.plugins.switcher.utils import shorten_paths # Local imports from spyder_notebook.utils.localization import _ @@ -85,12 +81,6 @@ def __init__(self, name, plugin, parent): layout.addWidget(self.tabwidget) self.setLayout(layout) - # Connect to switcher - self.switcher = plugin.main.switcher - self.switcher.sig_mode_selected.connect(self.handle_switcher_modes) - self.switcher.sig_item_selected.connect( - self.handle_switcher_selection) - # ---- PluginMainWidget API # ------------------------------------------------------------------------ def get_focus_widget(self): @@ -341,54 +331,3 @@ def clear_recent_notebooks(self): """Clear the list of recent notebooks.""" self.recent_notebooks = [] self.update_recent_notebooks_menu() - - def handle_switcher_modes(self, mode): - """ - Populate switcher with opened notebooks. - - List the file names of the opened notebooks with their directories in - the switcher. Only handle file mode, where `mode` is empty string. - """ - if mode != '': - return - - clients = [self.tabwidget.widget(i) - for i in range(self.tabwidget.count())] - paths = [client.get_filename() for client in clients] - is_unsaved = [False for client in clients] - short_paths = shorten_paths(paths, is_unsaved) - icon = self.create_icon('notebook') - section = self.get_title() - - for path, short_path, client in zip(paths, short_paths, clients): - title = osp.basename(path) - description = osp.dirname(path) - if len(path) > 75: - description = short_path - is_last_item = (client == clients[-1]) - - self.switcher.add_item( - title=title, - description=description, - icon=icon, - section=section, - data=client, - last_item=is_last_item - ) - - def handle_switcher_selection(self, item, mode, search_text): - """ - Handle user selecting item in switcher. - - If the selected item is not in the section of the switcher that - corresponds to this plugin, then ignore it. Otherwise, switch to - selected item in notebook plugin and hide the switcher. - """ - if item.get_section() != self.get_title(): - return - - client = item.get_data() - index = self.tabwidget.indexOf(client) - self.tabwidget.setCurrentIndex(index) - self._plugin.switch_to_plugin() - self.switcher.hide() From 59843678374a2f253771ad3473d1e5c2b6650471 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 29 Apr 2023 22:09:48 +0100 Subject: [PATCH 6/6] Be more forceful when shutting down servers. In particular, disconnect signals and kill the server if it is still starting up. This may avoid errors in the CI testing. --- spyder_notebook/utils/servermanager.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/spyder_notebook/utils/servermanager.py b/spyder_notebook/utils/servermanager.py index 34a19989..726b1e7b 100644 --- a/spyder_notebook/utils/servermanager.py +++ b/spyder_notebook/utils/servermanager.py @@ -287,13 +287,22 @@ def _check_server_started(self, server_process): self.sig_server_started.emit(server_process) def shutdown_all_servers(self): - """Shutdown all running servers.""" + """ + Shutdown all servers. + + Disconnect all signals of the server process and try to shutdown the + server nicely. However, if the server is still starting up, or if + shutting down nicely does not work, then kill the server process. + """ for server in self.servers: + process = server.process + process.readyReadStandardOutput.disconnect() + process.errorOccurred.disconnect() + process.finished.disconnect() + if server.state == ServerState.RUNNING: logger.debug('Shutting down notebook server for %s', server.notebook_dir) - server.process.errorOccurred.disconnect() - server.process.finished.disconnect() try: serverapp.shutdown_server(server.server_info, log=logger) @@ -308,6 +317,15 @@ def shutdown_all_servers(self): logger.warning(f'Ignoring {err}') server.state = ServerState.FINISHED + if server.state == ServerState.STARTING: + process.kill() + server.state = ServerState.FINISHED + + if process.state() != QProcess.NotRunning: + # Should not be necessary, but make sure that process is killed + process.kill() + + def read_server_output(self, server_process): """ Read the output of the notebook server process.