From d4bdd1dbb54890a19f6de35b000630d5f886b87d Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 27 May 2020 18:55:54 +0100 Subject: [PATCH 01/20] Tests: Remove code specific to Python 2 This code was left behind when support for Python 2 was dropped. --- spyder_notebook/tests/test_plugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index cbf6446f..f4255612 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -26,10 +26,6 @@ # Local imports from spyder_notebook.notebookplugin import NotebookPlugin -# Python 2 compatibility -if sys.version_info[0] == 2: - PermissionError = OSError - # ============================================================================= # Constants # ============================================================================= From ea93c3830e50f084f00111ff1ad8e377b25c445d Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Mon, 25 May 2020 21:24:14 +0100 Subject: [PATCH 02/20] Plugin: Remove .move_tab() The code suggests that this function is called when the user moves a tab in order to keep the list in `self.clients` in the same order as the tabs. However, as far as I can tell, this function is never actually called, the order in `self.clients` is not synchronized, and the order is not actually used. --- spyder_notebook/notebookplugin.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index dc55a7a6..7504ad65 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -93,7 +93,6 @@ def __init__(self, parent, testing=False): # Fixes Issue 561 self.tabwidget.setDocumentMode(True) self.tabwidget.currentChanged.connect(self.refresh_plugin) - self.tabwidget.move_data.connect(self.move_tab) self.tabwidget.set_close_function(self.close_client) @@ -488,11 +487,6 @@ def add_tab(self, widget): self.switch_to_plugin() self.activateWindow() - def move_tab(self, index_from, index_to): - """Move tab.""" - client = self.clients.pop(index_from) - self.clients.insert(index_to, client) - # ------ Public API (for FileSwitcher) ------------------------------------ def handle_switcher_modes(self, mode): """ From c98c2b8100e1fa3a83375c0438d5f76206391ccb Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Mon, 25 May 2020 16:34:47 +0100 Subject: [PATCH 03/20] Widgets: Add new class, NotebookTabWidget This is going to be the main widget of the plugin. The idea is that you can also use this widget independent of Spyder. --- spyder_notebook/notebookplugin.py | 21 ++++------ spyder_notebook/widgets/notebooktabwidget.py | 41 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 14 deletions(-) create mode 100755 spyder_notebook/widgets/notebooktabwidget.py diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 7504ad65..6c70c558 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -9,7 +9,6 @@ import os import os.path as osp import subprocess -import sys # Qt imports from qtpy import PYQT4, PYSIDE @@ -29,12 +28,12 @@ from spyder.utils.qthelpers import (create_action, create_toolbutton, add_actions, MENU_SEPARATOR) from spyder.utils.switcher import shorten_paths -from spyder.widgets.tabs import Tabs # Local imports -from .utils.nbopen import nbopen, NBServerError -from .widgets.client import NotebookClient +from spyder_notebook.utils.nbopen import nbopen, NBServerError +from spyder_notebook.widgets.client import NotebookClient +from spyder_notebook.widgets.notebooktabwidget import NotebookTabWidget NOTEBOOK_TMPDIR = osp.join(get_temp_dir(), 'notebooks') @@ -82,16 +81,10 @@ def __init__(self, parent, testing=False): menu_btn.setMenu(self._options_menu) menu_btn.setPopupMode(menu_btn.InstantPopup) corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]} - self.tabwidget = Tabs(self, menu=self._options_menu, - actions=self.menu_actions, - corner_widgets=corner_widgets) - - if hasattr(self.tabwidget, 'setDocumentMode') \ - and not sys.platform == 'darwin': - # Don't set document mode to true on OSX because it generates - # a crash when the console is detached from the main window - # Fixes Issue 561 - self.tabwidget.setDocumentMode(True) + self.tabwidget = NotebookTabWidget( + self, menu=self._options_menu, actions=self.menu_actions, + corner_widgets=corner_widgets) + self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.set_close_function(self.close_client) diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py new file mode 100755 index 00000000..8381993a --- /dev/null +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) Spyder Project Contributors +# Licensed under the terms of the MIT License + +"""File implementing NotebookTabWidget.""" + +# Standard library imports +import sys + +# Spyder imports +from spyder.widgets.tabs import Tabs + + +class NotebookTabWidget(Tabs): + """Tabbed widget whose tabs display notebooks.""" + + def __init__(self, parent, actions, menu, corner_widgets): + """ + Constructor. + + Parameters + ---------- + parent : QWidget + Parent of the tabbed widget. + actions : list of (QAction or QMenu or None) or None + Items to be added to the context menu. + menu : QMenu or None + Context menu of the tabbed widget. + corner_widgets : dict of (Qt.Corner, list of QWidget or int) or None + Widgets to be placed in the top left and right corner of the + tabbed widget. A button for browsing the tabs is always added to + the top left corner. + """ + super().__init__(parent, actions, menu, corner_widgets) + + if not sys.platform == 'darwin': + # Don't set document mode to true on OSX because it generates + # a crash when the console is detached from the main window + # Fixes spyder-ide/spyder#561 + self.setDocumentMode(True) From 80afe47b0fd8c22a05dcc9c6bb855e6c8bf08e67 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Mon, 25 May 2020 17:08:34 +0100 Subject: [PATCH 04/20] Refactor: Move clients attribute out of NotebookPlugin --- spyder_notebook/notebookplugin.py | 21 ++++++++++---------- spyder_notebook/tests/test_plugin.py | 4 ++-- spyder_notebook/widgets/notebooktabwidget.py | 12 ++++++++++- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 6c70c558..6af5d7d3 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -64,7 +64,6 @@ def __init__(self, parent, testing=False): self.main = parent - self.clients = [] self.untitled_num = 0 self.recent_notebooks = self.get_option('recent_notebooks', default=[]) self.recent_notebook_menu = QMenu(_("Open recent"), self) @@ -121,7 +120,7 @@ def get_focus_widget(self): def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed.""" - for cl in self.clients: + for cl in self.tabwidget.clients: cl.close() self.set_option('recent_notebooks', self.recent_notebooks) return True @@ -251,7 +250,8 @@ def clear_recent_notebooks(self): def get_clients(self): """Return notebooks list.""" - return [cl for cl in self.clients if isinstance(cl, NotebookClient)] + return [cl for cl in self.tabwidget.clients + if isinstance(cl, NotebookClient)] def get_focus_client(self): """Return current notebook with focus, if any.""" @@ -365,7 +365,7 @@ def close_client(self, index=None, client=None, save=False): # Note: notebook index may have changed after closing related widgets self.tabwidget.removeTab(self.tabwidget.indexOf(client)) - self.clients.remove(client) + self.tabwidget.clients.remove(client) self.create_welcome_client() @@ -472,7 +472,7 @@ def open_console(self, client=None): # ------ Public API (for tabs) -------------------------------------------- def add_tab(self, widget): """Add tab.""" - self.clients.append(widget) + self.tabwidget.clients.append(widget) index = self.tabwidget.addTab(widget, widget.get_short_name()) self.tabwidget.setCurrentIndex(index) self.tabwidget.setTabToolTip(index, widget.get_filename()) @@ -491,18 +491,19 @@ def handle_switcher_modes(self, mode): if mode != '': return - paths = [client.get_filename() for client in self.clients] - is_unsaved = [False for client in self.clients] + paths = [client.get_filename() for client in self.tabwidget.clients] + is_unsaved = [False for client in self.tabwidget.clients] short_paths = shorten_paths(paths, is_unsaved) icon = QIcon(os.path.join(PACKAGE_PATH, 'images', 'icon.svg')) section = self.get_plugin_title() - for path, short_path, client in zip(paths, short_paths, self.clients): + for path, short_path, client in zip( + paths, short_paths, self.tabwidget.clients): title = osp.basename(path) description = osp.dirname(path) if len(path) > 75: description = short_path - is_last_item = (client == self.clients[-1]) + is_last_item = (client == self.tabwidget.clients[-1]) self.switcher.add_item( title=title, description=description, icon=icon, section=section, data=client, last_item=is_last_item) @@ -519,7 +520,7 @@ def handle_switcher_selection(self, item, mode, search_text): return client = item.get_data() - index = self.clients.index(client) + index = self.tabwidget.clients.index(client) self.tabwidget.setCurrentIndex(index) self.switch_to_plugin() self.switcher.hide() diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index f4255612..8fa1963f 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -157,7 +157,7 @@ def test_close_nonexisting_notebook(notebook, qtbot): notebook.close_client() # Assert tab is closed (without raising an exception) - assert client not in notebook.clients + assert client not in notebook.tabwidget.clients @flaky(max_runs=3) @@ -249,7 +249,7 @@ def test_new_notebook(notebook, qtbot): qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Assert that we have one notebook and the welcome page - assert len(notebook.clients) == 2 + assert len(notebook.tabwidget.clients) == 2 def test_open_console_when_no_kernel(notebook, qtbot, mocker): diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index 8381993a..3d714cc9 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -13,7 +13,15 @@ class NotebookTabWidget(Tabs): - """Tabbed widget whose tabs display notebooks.""" + """ + Tabbed widget whose tabs display notebooks. + + This is the main widget of the notebook plugin. + + Attributes + ---------- + clients : list of NotebookClient + """ def __init__(self, parent, actions, menu, corner_widgets): """ @@ -34,6 +42,8 @@ def __init__(self, parent, actions, menu, corner_widgets): """ super().__init__(parent, actions, menu, corner_widgets) + self.clients = [] + if not sys.platform == 'darwin': # Don't set document mode to true on OSX because it generates # a crash when the console is detached from the main window From a87049d3485135b15bc7e3b864210c4c995df7d2 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 May 2020 09:48:26 +0100 Subject: [PATCH 05/20] Refactor: Move .add_tab() from plugin to tab widget I also removed switching to the plugin whenever a tab is added because I don't see why it is useful: a tab is only added when the user requests it, in which case the plugin already has focus, or at startup, in which case the plugin need not get focus. --- spyder_notebook/notebookplugin.py | 15 ++------------- spyder_notebook/widgets/notebooktabwidget.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 6af5d7d3..5deea275 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -324,7 +324,7 @@ def create_new_client(self, filename=None, give_focus=True): welcome_client = self.create_welcome_client() client = NotebookClient(self, filename) - self.add_tab(client) + self.tabwidget.add_tab(client) if NOTEBOOK_TMPDIR not in filename: self.add_to_recent(filename) self.setup_menu_actions() @@ -374,7 +374,7 @@ def create_welcome_client(self): if self.tabwidget.count() == 0: welcome = open(WELCOME).read() client = NotebookClient(self, WELCOME, ini_message=welcome) - self.add_tab(client) + self.tabwidget.add_tab(client) return client def save_notebook(self, client): @@ -469,17 +469,6 @@ def open_console(self, client=None): self.ipyconsole.rename_client_tab(ipyclient, client.get_short_name()) - # ------ Public API (for tabs) -------------------------------------------- - def add_tab(self, widget): - """Add tab.""" - self.tabwidget.clients.append(widget) - index = self.tabwidget.addTab(widget, widget.get_short_name()) - self.tabwidget.setCurrentIndex(index) - self.tabwidget.setTabToolTip(index, widget.get_filename()) - if self.dockwidget: - self.switch_to_plugin() - self.activateWindow() - # ------ Public API (for FileSwitcher) ------------------------------------ def handle_switcher_modes(self, mode): """ diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index 3d714cc9..d9869886 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -49,3 +49,17 @@ def __init__(self, parent, actions, menu, corner_widgets): # a crash when the console is detached from the main window # Fixes spyder-ide/spyder#561 self.setDocumentMode(True) + + def add_tab(self, widget): + """ + Add tab containing some notebook widget to the tabbed widget. + + Parameters + ---------- + widget : NotebookClient + Notebook widget to display in new tab. + """ + self.clients.append(widget) + index = self.addTab(widget, widget.get_short_name()) + self.setCurrentIndex(index) + self.setTabToolTip(index, widget.get_filename()) From 5529fd2d9c7947e7b48aa6b0d4edfbddb8f3e759 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 May 2020 19:00:28 +0100 Subject: [PATCH 06/20] Widgets: Decouple NotebookClient and NotebookWidget from plugin --- spyder_notebook/notebookplugin.py | 9 ++-- spyder_notebook/widgets/client.py | 82 +++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 5deea275..adbb5199 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -59,9 +59,6 @@ def __init__(self, parent, testing=False): self.testing = testing self.fileswitcher_dlg = None - self.tabwidget = None - self.menu_actions = None - self.main = parent self.untitled_num = 0 @@ -77,6 +74,7 @@ def __init__(self, parent, testing=False): menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), tip=_('Options')) + self.menu_actions = self.get_plugin_actions() menu_btn.setMenu(self._options_menu) menu_btn.setPopupMode(menu_btn.InstantPopup) corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]} @@ -323,7 +321,7 @@ def create_new_client(self, filename=None, give_focus=True): return welcome_client = self.create_welcome_client() - client = NotebookClient(self, filename) + client = NotebookClient(self, filename, self.menu_actions) self.tabwidget.add_tab(client) if NOTEBOOK_TMPDIR not in filename: self.add_to_recent(filename) @@ -373,7 +371,8 @@ def create_welcome_client(self): """Create a welcome client with some instructions.""" if self.tabwidget.count() == 0: welcome = open(WELCOME).read() - client = NotebookClient(self, WELCOME, ini_message=welcome) + client = NotebookClient( + self, WELCOME, self.menu_actions, ini_message=welcome) self.tabwidget.add_tab(client) return client diff --git a/spyder_notebook/widgets/client.py b/spyder_notebook/widgets/client.py index e5a151df..66c422ba 100644 --- a/spyder_notebook/widgets/client.py +++ b/spyder_notebook/widgets/client.py @@ -31,7 +31,7 @@ from spyder.widgets.findreplace import FindReplace # Local imports -from ..widgets.dom import DOMWidget +from spyder_notebook.widgets.dom import DOMWidget # ----------------------------------------------------------------------------- # Templates @@ -56,19 +56,54 @@ class NotebookWidget(DOMWidget): """WebView widget for notebooks.""" + def __init__(self, parent, actions=None): + """ + Constructor. + + Parameters + ---------- + parent : QWidget + Parent of the widget under construction. + actions : list of (QAction or QMenu or None) or None, optional + Actions to be added to the context menu of the widget under + construction. The default is None, meaning that no actions + will be added. + """ + super().__init__(parent) + self.actions = actions + def contextMenuEvent(self, event): - """Don't show some actions which have no meaning for the notebook.""" - menu = QMenu(self) - plugin_actions = self.parent().plugin_actions - actions = plugin_actions + [None, - self.pageAction(QWebEnginePage.SelectAll), - self.pageAction(QWebEnginePage.Copy), None, - self.zoom_in_action, self.zoom_out_action] + """ + Handle context menu events. + + This overrides WebView.contextMenuEvent() in order to add the + actions in `self.actions` and remove the Back and Forward actions + which have no meaning for the notebook widget. + + Parameters + ---------- + event : QContextMenuEvent + The context menu event that needs to be handled. + """ + if self.actions is None: + actions = [] + else: + actions = self.actions + [None] + + actions += [ + self.pageAction(QWebEnginePage.SelectAll), + self.pageAction(QWebEnginePage.Copy), + None, + self.zoom_in_action, + self.zoom_out_action] + if not WEBENGINE: settings = self.page().settings() settings.setAttribute(QWebEngineSettings.DeveloperExtrasEnabled, True) actions += [None, self.pageAction(QWebEnginePage.InspectElement)] + + menu = QMenu(self) add_actions(menu, actions) menu.popup(event.globalPos()) event.accept() @@ -119,9 +154,25 @@ class NotebookClient(QWidget): render notebooks. """ - def __init__(self, plugin, filename, ini_message=None): - """Constructor.""" - super().__init__(plugin) + def __init__(self, parent, filename, actions=None, ini_message=None): + """ + Constructor. + + Parameters + ---------- + parent : QWidget + Parent of the widget under construction. + filename : str + File name of the notebook. + actions : list of (QAction or QMenu or None) or None, optional + Actions to be added to the context menu of the widget under + construction. The default is None, meaning that no actions + will be added. + ini_message : str or None, optional + HTML to be initially displayed in the widget. The default is + None, meaning that an empty page is displayed initially. + """ + super().__init__(parent) if os.name == 'nt': filename = filename.replace('/', '\\') @@ -131,8 +182,7 @@ def __init__(self, plugin, filename, ini_message=None): self.server_url = None self.path = None - self.plugin_actions = plugin.get_plugin_actions() - self.notebookwidget = NotebookWidget(self) + self.notebookwidget = NotebookWidget(self, actions) if ini_message: self.notebookwidget.show_message(ini_message) else: @@ -278,12 +328,12 @@ def shutdown_kernel(self): # Tests # ----------------------------------------------------------------------------- def main(): - """Simple test.""" + """Execute a simple test.""" from spyder.utils.qthelpers import qapplication app = qapplication() - widget = NotebookClient(plugin=None, name='') + widget = NotebookClient(parent=None, filename='') widget.show() - widget.set_url('http://google.com') + widget.go_to('http://google.com') sys.exit(app.exec_()) From 713c53a0c44e283f817632eede26333f72626a45 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 May 2020 20:30:47 +0100 Subject: [PATCH 07/20] Widgets: Add example app, showing how to use widgets outside Spyder --- spyder_notebook/widgets/example_app.py | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100755 spyder_notebook/widgets/example_app.py diff --git a/spyder_notebook/widgets/example_app.py b/spyder_notebook/widgets/example_app.py new file mode 100755 index 00000000..504c936c --- /dev/null +++ b/spyder_notebook/widgets/example_app.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) Spyder Project Contributors +# Licensed under the terms of the MIT License + +""" +Simple application for working with notebooks. + +This is a stand-alone application showing how spyder_notebook can be used +outside Spyder. It is mainly meant for development and testing purposes, +but it can also serve as an example. +""" + +# Standard library imports +import sys + +# Qt import +from qtpy.QtCore import QCoreApplication, Qt +from qtpy.QtQuick import QQuickWindow, QSGRendererInterface +from qtpy.QtWidgets import QApplication, QMainWindow + +# Plugin imports +from spyder_notebook.widgets.client import NotebookClient +from spyder_notebook.widgets.notebooktabwidget import NotebookTabWidget + + +def use_software_rendering(): + """ + Instruct Qt to use software rendering. + + This is necessary for some buggy graphics drivers (e.g. nvidia). + This function should be run before the QApplication is created. + """ + QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) + QQuickWindow.setSceneGraphBackend(QSGRendererInterface.Software) + + +class NotebookAppMainWindow(QMainWindow): + """Main window for stand-alone notebook application.""" + + def __init__(self): + super().__init__() + widget = NotebookTabWidget(self, None, None, None) + client = NotebookClient(parent=self, filename='Google') + client.go_to('http://google.com') + widget.add_tab(client) + self.setCentralWidget(widget) + + +if __name__ == '__main__': + use_software_rendering() + app = QApplication([]) + window = NotebookAppMainWindow() + window.show() + sys.exit(app.exec_()) From 19e9f86477016351bffaa60562854d871ca5506f Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Thu, 28 May 2020 09:52:14 +0100 Subject: [PATCH 08/20] Refactor: Move creation of welcome tab from plugin to tab widget Also rename the function from create_welcome_tab to maybe_create_welcome_tab to indicate that the welcome tab is not always created, and change the example app to create a welcome tab. --- spyder_notebook/notebookplugin.py | 15 +++-------- spyder_notebook/widgets/example_app.py | 5 +--- spyder_notebook/widgets/notebooktabwidget.py | 27 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index adbb5199..4f2d8a80 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -317,10 +317,10 @@ def create_new_client(self, filename=None, give_focus=True): # Create a welcome widget # See issue 93 self.untitled_num -= 1 - self.create_welcome_client() + self.maybe_create_welcome_client() return - welcome_client = self.create_welcome_client() + welcome_client = self.tabwidget.maybe_create_welcome_client() client = NotebookClient(self, filename, self.menu_actions) self.tabwidget.add_tab(client) if NOTEBOOK_TMPDIR not in filename: @@ -365,16 +365,7 @@ def close_client(self, index=None, client=None, save=False): self.tabwidget.removeTab(self.tabwidget.indexOf(client)) self.tabwidget.clients.remove(client) - self.create_welcome_client() - - def create_welcome_client(self): - """Create a welcome client with some instructions.""" - if self.tabwidget.count() == 0: - welcome = open(WELCOME).read() - client = NotebookClient( - self, WELCOME, self.menu_actions, ini_message=welcome) - self.tabwidget.add_tab(client) - return client + self.tabwidget.maybe_create_welcome_client() def save_notebook(self, client): """ diff --git a/spyder_notebook/widgets/example_app.py b/spyder_notebook/widgets/example_app.py index 504c936c..4b28ab52 100755 --- a/spyder_notebook/widgets/example_app.py +++ b/spyder_notebook/widgets/example_app.py @@ -20,7 +20,6 @@ from qtpy.QtWidgets import QApplication, QMainWindow # Plugin imports -from spyder_notebook.widgets.client import NotebookClient from spyder_notebook.widgets.notebooktabwidget import NotebookTabWidget @@ -41,9 +40,7 @@ class NotebookAppMainWindow(QMainWindow): def __init__(self): super().__init__() widget = NotebookTabWidget(self, None, None, None) - client = NotebookClient(parent=self, filename='Google') - client.go_to('http://google.com') - widget.add_tab(client) + widget.maybe_create_welcome_client() self.setCentralWidget(widget) diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index d9869886..517107c2 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -6,11 +6,20 @@ """File implementing NotebookTabWidget.""" # Standard library imports +import os.path as osp import sys # Spyder imports from spyder.widgets.tabs import Tabs +# Local imports +from spyder_notebook.widgets.client import NotebookClient + + +# Path to HTML file with welcome message +PACKAGE_PATH = osp.join(osp.dirname(__file__), '..') +WELCOME = osp.join(PACKAGE_PATH, 'utils', 'templates', 'welcome.html') + class NotebookTabWidget(Tabs): """ @@ -20,6 +29,7 @@ class NotebookTabWidget(Tabs): Attributes ---------- + actions : list of (QAction or QMenu or None) or None clients : list of NotebookClient """ @@ -42,6 +52,7 @@ def __init__(self, parent, actions, menu, corner_widgets): """ super().__init__(parent, actions, menu, corner_widgets) + self.actions = actions self.clients = [] if not sys.platform == 'darwin': @@ -50,6 +61,22 @@ def __init__(self, parent, actions, menu, corner_widgets): # Fixes spyder-ide/spyder#561 self.setDocumentMode(True) + def maybe_create_welcome_client(self): + """ + Create a welcome tab if there are no tabs. + + Returns + ------- + client : NotebookClient or None + The client in the created tab, or None if no tab is created. + """ + if self.count() == 0: + welcome = open(WELCOME).read() + client = NotebookClient( + self, WELCOME, self.actions, ini_message=welcome) + self.add_tab(client) + return client + def add_tab(self, widget): """ Add tab containing some notebook widget to the tabbed widget. From 5e915fbe71aa897abb6e3a8358f6a8ebb606d85b Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Thu, 28 May 2020 11:45:42 +0100 Subject: [PATCH 09/20] Refactor: Move opening notebooks from plugin to tab widget Update example app to allow opening new and existing notebooks. --- spyder_notebook/notebookplugin.py | 58 ++---------- spyder_notebook/widgets/example_app.py | 20 +++- spyder_notebook/widgets/notebooktabwidget.py | 99 +++++++++++++++++++- 3 files changed, 124 insertions(+), 53 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 4f2d8a80..83483357 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -8,11 +8,10 @@ # Stdlib imports import os import os.path as osp -import subprocess # Qt imports from qtpy import PYQT4, PYSIDE -from qtpy.compat import getsavefilename, getopenfilenames +from qtpy.compat import getsavefilename from qtpy.QtCore import Qt, QEventLoop, QTimer, Signal from qtpy.QtGui import QIcon from qtpy.QtWidgets import QApplication, QMessageBox, QVBoxLayout, QMenu @@ -31,7 +30,6 @@ # Local imports -from spyder_notebook.utils.nbopen import nbopen, NBServerError from spyder_notebook.widgets.client import NotebookClient from spyder_notebook.widgets.notebooktabwidget import NotebookTabWidget @@ -61,7 +59,6 @@ def __init__(self, parent, testing=False): self.fileswitcher_dlg = None self.main = parent - self.untitled_num = 0 self.recent_notebooks = self.get_option('recent_notebooks', default=[]) self.recent_notebook_menu = QMenu(_("Open recent"), self) @@ -80,7 +77,7 @@ def __init__(self, parent, testing=False): corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]} self.tabwidget = NotebookTabWidget( self, menu=self._options_menu, actions=self.menu_actions, - corner_widgets=corner_widgets) + corner_widgets=corner_widgets, testing=testing) self.tabwidget.currentChanged.connect(self.refresh_plugin) @@ -284,52 +281,16 @@ def get_current_client_name(self, short=False): def create_new_client(self, filename=None, give_focus=True): """Create a new notebook or load a pre-existing one.""" - # Generate the notebook name (in case of a new one) - if not filename: - if not osp.isdir(NOTEBOOK_TMPDIR): - os.makedirs(NOTEBOOK_TMPDIR) - nb_name = 'untitled' + str(self.untitled_num) + '.ipynb' - filename = osp.join(NOTEBOOK_TMPDIR, nb_name) - kernelspec = dict(display_name='Python 3 (Spyder)', - name='python3') - metadata = dict(kernelspec=kernelspec) - nb_contents = nbformat.v4.new_notebook(metadata=metadata) - nbformat.write(nb_contents, filename) - self.untitled_num += 1 - # Save spyder_pythonpath before creating a client # because it's needed by our kernel spec. if not self.testing: self.set_option('main/spyder_pythonpath', self.main.get_spyder_pythonpath()) - # Open the notebook with nbopen and get the url we need to render - try: - server_info = nbopen(filename) - except (subprocess.CalledProcessError, NBServerError): - QMessageBox.critical( - self, - _("Server error"), - _("The Jupyter Notebook server failed to start or it is " - "taking too much time to do it. Please start it in a " - "system terminal with the command 'jupyter notebook' to " - "check for errors.")) - # Create a welcome widget - # See issue 93 - self.untitled_num -= 1 - self.maybe_create_welcome_client() - return - - welcome_client = self.tabwidget.maybe_create_welcome_client() - client = NotebookClient(self, filename, self.menu_actions) - self.tabwidget.add_tab(client) + filename = self.tabwidget.create_new_client(filename, give_focus) if NOTEBOOK_TMPDIR not in filename: self.add_to_recent(filename) self.setup_menu_actions() - client.register(server_info) - client.load_notebook() - if welcome_client and not self.testing: - self.tabwidget.setCurrentIndex(0) def close_client(self, index=None, client=None, save=False): """ @@ -434,12 +395,13 @@ def save_as(self, name=None, close=False): def open_notebook(self, filenames=None): """Open a notebook from file.""" - if not filenames: - filenames, _selfilter = getopenfilenames(self, _("Open notebook"), - '', FILES_FILTER) - if filenames: - for filename in filenames: - self.create_new_client(filename=filename) + # Save spyder_pythonpath before creating a client + # because it's needed by our kernel spec. + if not self.testing: + self.set_option('main/spyder_pythonpath', + self.main.get_spyder_pythonpath()) + + self.tabwidget.open_notebook(filenames) def open_console(self, client=None): """Open an IPython console for the given client or the current one.""" diff --git a/spyder_notebook/widgets/example_app.py b/spyder_notebook/widgets/example_app.py index 4b28ab52..26256a45 100755 --- a/spyder_notebook/widgets/example_app.py +++ b/spyder_notebook/widgets/example_app.py @@ -17,7 +17,7 @@ # Qt import from qtpy.QtCore import QCoreApplication, Qt from qtpy.QtQuick import QQuickWindow, QSGRendererInterface -from qtpy.QtWidgets import QApplication, QMainWindow +from qtpy.QtWidgets import QAction, QApplication, QMainWindow # Plugin imports from spyder_notebook.widgets.notebooktabwidget import NotebookTabWidget @@ -39,9 +39,21 @@ class NotebookAppMainWindow(QMainWindow): def __init__(self): super().__init__() - widget = NotebookTabWidget(self, None, None, None) - widget.maybe_create_welcome_client() - self.setCentralWidget(widget) + self.tabwidget = NotebookTabWidget(self, None, None, None) + self.tabwidget.maybe_create_welcome_client() + self.setCentralWidget(self.tabwidget) + self._setup_menu() + + def _setup_menu(self): + file_menu = self.menuBar().addMenu('File') + + new_action = QAction('New Notebook', self) + new_action.triggered.connect(self.tabwidget.create_new_client) + file_menu.addAction(new_action) + + open_action = QAction('Open Notebook', self) + open_action.triggered.connect(self.tabwidget.open_notebook) + file_menu.addAction(open_action) if __name__ == '__main__': diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index 517107c2..d7c24774 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -6,20 +6,38 @@ """File implementing NotebookTabWidget.""" # Standard library imports +import os import os.path as osp +import subprocess import sys +# Qt imports +from qtpy.compat import getopenfilenames +from qtpy.QtWidgets import QMessageBox + +# Third-party imports +import nbformat + # Spyder imports +from spyder.config.base import _ +from spyder.utils.programs import get_temp_dir from spyder.widgets.tabs import Tabs # Local imports +from spyder_notebook.utils.nbopen import nbopen, NBServerError from spyder_notebook.widgets.client import NotebookClient +# Directory in which new notebooks are created +NOTEBOOK_TMPDIR = osp.join(get_temp_dir(), 'notebooks') + # Path to HTML file with welcome message PACKAGE_PATH = osp.join(osp.dirname(__file__), '..') WELCOME = osp.join(PACKAGE_PATH, 'utils', 'templates', 'welcome.html') +# Filter to use in file dialogs +FILES_FILTER = '{} (*.ipynb)'.format(_('Jupyter notebooks')) + class NotebookTabWidget(Tabs): """ @@ -30,10 +48,14 @@ class NotebookTabWidget(Tabs): Attributes ---------- actions : list of (QAction or QMenu or None) or None + Items to be added to the context menu. clients : list of NotebookClient + List of notebook clients displayed in tabs in this widget. + untitled_num : int + Number used in file name of newly created notebooks. """ - def __init__(self, parent, actions, menu, corner_widgets): + def __init__(self, parent, actions, menu, corner_widgets, testing=False): """ Constructor. @@ -54,6 +76,8 @@ def __init__(self, parent, actions, menu, corner_widgets): self.actions = actions self.clients = [] + self.testing = testing + self.untitled_num = 0 if not sys.platform == 'darwin': # Don't set document mode to true on OSX because it generates @@ -61,6 +85,79 @@ def __init__(self, parent, actions, menu, corner_widgets): # Fixes spyder-ide/spyder#561 self.setDocumentMode(True) + def open_notebook(self, filenames=None): + """ + Open a notebook from file. + + Parameters + ---------- + filenames : list of str or None, optional + List of file names of notebooks to open. The default is None, + meaning that the user should be asked. + """ + if not filenames: + filenames, _selfilter = getopenfilenames( + self, _('Open notebook'), '', FILES_FILTER) + if filenames: + for filename in filenames: + self.create_new_client(filename=filename) + + def create_new_client(self, filename=None, give_focus=True): + """ + Create a new notebook or load a pre-existing one. + + Parameters + ---------- + filename : str, optional + File name of the notebook to load in the new client. The default + is None, meaning that a new notebook should be created. + give_focus : bool, optional + Not used. The default is True. + + Returns + ------- + filename : str or None + File name of notebook that is opened, or None if unsuccessful. + """ + # Generate the notebook name (in case of a new one) + if not filename: + if not osp.isdir(NOTEBOOK_TMPDIR): + os.makedirs(NOTEBOOK_TMPDIR) + nb_name = 'untitled' + str(self.untitled_num) + '.ipynb' + filename = osp.join(NOTEBOOK_TMPDIR, nb_name) + kernelspec = dict(display_name='Python 3 (Spyder)', + name='python3') + metadata = dict(kernelspec=kernelspec) + nb_contents = nbformat.v4.new_notebook(metadata=metadata) + nbformat.write(nb_contents, filename) + self.untitled_num += 1 + + # Open the notebook with nbopen and get the url we need to render + try: + server_info = nbopen(filename) + except (subprocess.CalledProcessError, NBServerError): + QMessageBox.critical( + self, + _("Server error"), + _("The Jupyter Notebook server failed to start or it is " + "taking too much time to do it. Please start it in a " + "system terminal with the command 'jupyter notebook' to " + "check for errors.")) + # Create a welcome widget + # See issue 93 + self.untitled_num -= 1 + self.maybe_create_welcome_client() + return + + welcome_client = self.maybe_create_welcome_client() + client = NotebookClient(self, filename, self.actions) + self.add_tab(client) + client.register(server_info) + client.load_notebook() + if welcome_client and not self.testing: + self.setCurrentIndex(0) + return filename + def maybe_create_welcome_client(self): """ Create a welcome tab if there are no tabs. From 102604eab6d661adee7a64ee523c245ae86e0054 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Fri, 29 May 2020 10:27:06 +0100 Subject: [PATCH 10/20] Widgets: Remove unused parameter give_focus --- spyder_notebook/notebookplugin.py | 6 +++--- spyder_notebook/widgets/notebooktabwidget.py | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 83483357..8be77783 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -167,7 +167,7 @@ def register_plugin(self): super().register_plugin() self.focus_changed.connect(self.main.plugin_focus_changed) self.ipyconsole = self.main.ipyconsole - self.create_new_client(give_focus=False) + self.create_new_client() # Connect to switcher self.switcher = self.main.switcher @@ -279,7 +279,7 @@ def get_current_client_name(self, short=False): else: return client.get_filename() - def create_new_client(self, filename=None, give_focus=True): + def create_new_client(self, filename=None): """Create a new notebook or load a pre-existing one.""" # Save spyder_pythonpath before creating a client # because it's needed by our kernel spec. @@ -287,7 +287,7 @@ def create_new_client(self, filename=None, give_focus=True): self.set_option('main/spyder_pythonpath', self.main.get_spyder_pythonpath()) - filename = self.tabwidget.create_new_client(filename, give_focus) + filename = self.tabwidget.create_new_client(filename) if NOTEBOOK_TMPDIR not in filename: self.add_to_recent(filename) self.setup_menu_actions() diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index d7c24774..5b641856 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -102,7 +102,7 @@ def open_notebook(self, filenames=None): for filename in filenames: self.create_new_client(filename=filename) - def create_new_client(self, filename=None, give_focus=True): + def create_new_client(self, filename=None): """ Create a new notebook or load a pre-existing one. @@ -111,8 +111,6 @@ def create_new_client(self, filename=None, give_focus=True): filename : str, optional File name of the notebook to load in the new client. The default is None, meaning that a new notebook should be created. - give_focus : bool, optional - Not used. The default is True. Returns ------- From 8d84a48d7c5327832ba8ddaafbc4f0c4a1eb426b Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Fri, 29 May 2020 10:44:23 +0100 Subject: [PATCH 11/20] Widgets: Remove testing attribute from tab widget Instead, change the test so that it expects the widget to behave as normal. --- spyder_notebook/notebookplugin.py | 2 +- spyder_notebook/tests/test_plugin.py | 4 +++- spyder_notebook/widgets/notebooktabwidget.py | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 8be77783..04c568c5 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -77,7 +77,7 @@ def __init__(self, parent, testing=False): corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]} self.tabwidget = NotebookTabWidget( self, menu=self._options_menu, actions=self.menu_actions, - corner_widgets=corner_widgets, testing=testing) + corner_widgets=corner_widgets) self.tabwidget.currentChanged.connect(self.refresh_plugin) diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index 8fa1963f..40f8967e 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -94,10 +94,12 @@ def is_kernel_up(kernel_id, sessions_url): # ============================================================================= @pytest.fixture def notebook(qtbot): - """Set up the Notebook plugin.""" + """Set up the Notebook plugin with a welcome tab and a tab with a new + notebook. The latter tab is selected.""" notebook_plugin = NotebookPlugin(None, testing=True) qtbot.addWidget(notebook_plugin) notebook_plugin.create_new_client() + notebook_plugin.tabwidget.setCurrentIndex(1) return notebook_plugin diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index 5b641856..128fcc6c 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -55,7 +55,7 @@ class NotebookTabWidget(Tabs): Number used in file name of newly created notebooks. """ - def __init__(self, parent, actions, menu, corner_widgets, testing=False): + def __init__(self, parent, actions, menu, corner_widgets): """ Constructor. @@ -76,7 +76,6 @@ def __init__(self, parent, actions, menu, corner_widgets, testing=False): self.actions = actions self.clients = [] - self.testing = testing self.untitled_num = 0 if not sys.platform == 'darwin': @@ -106,6 +105,9 @@ def create_new_client(self, filename=None): """ Create a new notebook or load a pre-existing one. + This function also creates and selects a welcome tab, if no tabs are + present. + Parameters ---------- filename : str, optional @@ -152,7 +154,7 @@ def create_new_client(self, filename=None): self.add_tab(client) client.register(server_info) client.load_notebook() - if welcome_client and not self.testing: + if welcome_client: self.setCurrentIndex(0) return filename From c62342a516c24991571203ae89b24b783259a9fe Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Fri, 29 May 2020 22:52:41 +0100 Subject: [PATCH 12/20] Refactor: Move saving and closing from plugin to tab widget --- spyder_notebook/notebookplugin.py | 133 ++-------------- spyder_notebook/tests/test_plugin.py | 28 ++-- spyder_notebook/widgets/example_app.py | 18 ++- spyder_notebook/widgets/notebooktabwidget.py | 157 ++++++++++++++++++- 4 files changed, 199 insertions(+), 137 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 04c568c5..0b9514ed 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -11,14 +11,10 @@ # Qt imports from qtpy import PYQT4, PYSIDE -from qtpy.compat import getsavefilename -from qtpy.QtCore import Qt, QEventLoop, QTimer, Signal +from qtpy.QtCore import Qt, Signal from qtpy.QtGui import QIcon from qtpy.QtWidgets import QApplication, QMessageBox, QVBoxLayout, QMenu -# Third-party imports -import nbformat - # Spyder imports from spyder.api.plugins import SpyderPluginWidget from spyder.config.base import _ @@ -81,8 +77,6 @@ def __init__(self, parent, testing=False): self.tabwidget.currentChanged.connect(self.refresh_plugin) - self.tabwidget.set_close_function(self.close_client) - layout.addWidget(self.tabwidget) self.setLayout(layout) @@ -219,7 +213,10 @@ def update_notebook_actions(self): self.clear_recent_notebooks_action.setEnabled(True) else: self.clear_recent_notebooks_action.setEnabled(False) - client = self.get_current_client() + try: + client = self.tabwidget.get_current_client() + except AttributeError: # tabwidget is not yet constructed + client = None if client: if client.get_filename() != WELCOME: self.save_as_action.setEnabled(True) @@ -255,24 +252,15 @@ def get_focus_client(self): if widget is client or widget is client.notebookwidget: return client - def get_current_client(self): - """Return the currently selected notebook.""" - try: - client = self.tabwidget.currentWidget() - except AttributeError: - client = None - if client is not None: - return client - def get_current_nbwidget(self): """Return the notebookwidget of the current client.""" - client = self.get_current_client() + client = self.tabwidget.get_current_client() if client is not None: return client.notebookwidget def get_current_client_name(self, short=False): """Get the current client name.""" - client = self.get_current_client() + client = self.tabwidget.get_current_client() if client: if short: return client.get_short_name() @@ -292,107 +280,6 @@ def create_new_client(self, filename=None): self.add_to_recent(filename) self.setup_menu_actions() - def close_client(self, index=None, client=None, save=False): - """ - Close client tab from index or widget (or close current tab). - - The notebook is saved if `save` is `False`. - """ - if not self.tabwidget.count(): - return - if client is not None: - index = self.tabwidget.indexOf(client) - if index is None and client is None: - index = self.tabwidget.currentIndex() - if index is not None: - client = self.tabwidget.widget(index) - - is_welcome = client.get_filename() == WELCOME - if not save and not is_welcome: - self.save_notebook(client) - if not is_welcome: - client.shutdown_kernel() - client.close() - - # Delete notebook file if it is in temporary directory - filename = client.get_filename() - if filename.startswith(get_temp_dir()): - try: - os.remove(filename) - except EnvironmentError: - pass - - # Note: notebook index may have changed after closing related widgets - self.tabwidget.removeTab(self.tabwidget.indexOf(client)) - self.tabwidget.clients.remove(client) - - self.tabwidget.maybe_create_welcome_client() - - def save_notebook(self, client): - """ - Save notebook corresponding to given client. - - If the notebook is newly created and not empty, then ask the user for - a new filename and save under that name. - - This function is called when the user closes a tab. - """ - client.save() - - # Check filename to find out whether notebook is newly created - path = client.get_filename() - dirname, basename = osp.split(path) - if dirname != NOTEBOOK_TMPDIR or not basename.startswith('untitled'): - return - - # Read file to see whether notebook is empty - wait_save = QEventLoop() - QTimer.singleShot(1000, wait_save.quit) - wait_save.exec_() - nb_contents = nbformat.read(path, as_version=4) - if (len(nb_contents['cells']) == 0 - or len(nb_contents['cells'][0]['source']) == 0): - return - - # Ask user to save notebook with new filename - buttons = QMessageBox.Yes | QMessageBox.No - text = _("{0} has been modified.
" - "Do you want to save changes?").format(basename) - answer = QMessageBox.question( - self, self.get_plugin_title(), text, buttons) - if answer == QMessageBox.Yes: - self.save_as(close=True) - - def save_as(self, name=None, close=False): - """Save notebook as.""" - current_client = self.get_current_client() - current_client.save() - original_path = current_client.get_filename() - if not name: - original_name = osp.basename(original_path) - else: - original_name = name - filename, _selfilter = getsavefilename(self, _("Save notebook"), - original_name, FILES_FILTER) - if filename: - try: - nb_contents = nbformat.read(original_path, as_version=4) - except EnvironmentError as error: - txt = (_("Error while reading {}

{}") - .format(original_path, str(error))) - QMessageBox.critical(self, _("File Error"), txt) - return - try: - nbformat.write(nb_contents, filename) - except EnvironmentError as error: - txt = (_("Error while writing {}

{}") - .format(filename, str(error))) - QMessageBox.critical(self, _("File Error"), txt) - return - if not close: - self.close_client(save=True) - self.create_new_client(filename=filename) - def open_notebook(self, filenames=None): """Open a notebook from file.""" # Save spyder_pythonpath before creating a client @@ -403,10 +290,14 @@ def open_notebook(self, filenames=None): self.tabwidget.open_notebook(filenames) + def save_as(self): + """Save current notebook to different file.""" + self.tabwidget.save_as() + def open_console(self, client=None): """Open an IPython console for the given client or the current one.""" if not client: - client = self.get_current_client() + client = self.tabwidget.get_current_client() if self.ipyconsole is not None: kernel_id = client.get_kernel_id() if not kernel_id: diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index 40f8967e..3433a26a 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -114,14 +114,14 @@ def test_shutdown_notebook_kernel(notebook, qtbot): qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Get kernel id for the client - client = notebook.get_current_client() + client = notebook.tabwidget.get_current_client() qtbot.waitUntil(lambda: client.get_kernel_id() is not None, timeout=NOTEBOOK_UP) kernel_id = client.get_kernel_id() sessions_url = client.get_session_url() # Close the current client - notebook.close_client() + notebook.tabwidget.close_client() # Assert that the kernel is down for the closed client assert not is_kernel_up(kernel_id, sessions_url) @@ -135,11 +135,11 @@ def test_file_in_temp_dir_deleted_after_notebook_closed(notebook, qtbot): qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Get file name - client = notebook.get_current_client() + client = notebook.tabwidget.get_current_client() filename = client.get_filename() # Close the current client - notebook.close_client() + notebook.tabwidget.close_client() # Assert file is deleted assert not osp.exists(filename) @@ -153,10 +153,10 @@ def test_close_nonexisting_notebook(notebook, qtbot): notebook.open_notebook(filenames=[filename]) nbwidget = notebook.get_current_nbwidget() qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) - client = notebook.get_current_client() + client = notebook.tabwidget.get_current_client() # Close tab - notebook.close_client() + notebook.tabwidget.close_client() # Assert tab is closed (without raising an exception) assert client not in notebook.tabwidget.clients @@ -180,7 +180,7 @@ def test_open_notebook(notebook, qtbot, tmpdir): # and the client has the correct name qtbot.waitUntil(lambda: text_present(nbwidget), timeout=NOTEBOOK_UP) assert text_present(nbwidget) - assert notebook.get_current_client().get_short_name() == "test" + assert notebook.tabwidget.get_current_client().get_short_name() == "test" @flaky(max_runs=3) @@ -207,7 +207,7 @@ def test_save_notebook(notebook, qtbot, tmpdir): # Save the notebook name = osp.join(str(tmpdir), 'save.ipynb') QTimer.singleShot(1000, lambda: manage_save_dialog(qtbot, fname=name)) - notebook.save_as(name=name) + notebook.save_as() # Wait for prompt nbwidget = notebook.get_current_nbwidget() @@ -218,19 +218,19 @@ def test_save_notebook(notebook, qtbot, tmpdir): qtbot.waitUntil(lambda: text_present(nbwidget, text="test"), timeout=NOTEBOOK_UP) assert text_present(nbwidget, text="test") - assert notebook.get_current_client().get_short_name() == "save" + assert notebook.tabwidget.get_current_client().get_short_name() == "save" def test_save_notebook_as_with_error(mocker, notebook, qtbot, tmpdir): """Test that errors are handled in save_as().""" # Set up mocks name = osp.join(str(tmpdir), 'save.ipynb') - mocker.patch('spyder_notebook.notebookplugin.getsavefilename', + mocker.patch('spyder_notebook.widgets.notebooktabwidget.getsavefilename', return_value=(name, 'ignored')) - mocker.patch('spyder_notebook.notebookplugin.nbformat.write', + mocker.patch('spyder_notebook.widgets.notebooktabwidget.nbformat.write', side_effect=PermissionError) - mock_critical = mocker.patch('spyder_notebook.notebookplugin.QMessageBox' - '.critical') + mock_critical = mocker.patch('spyder_notebook.widgets.notebooktabwidget' + '.QMessageBox.critical') # Wait for prompt nbwidget = notebook.get_current_nbwidget() @@ -265,7 +265,7 @@ def test_open_console_when_no_kernel(notebook, qtbot, mocker): qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Shut the kernel down and check that this is successful - client = notebook.get_current_client() + client = notebook.tabwidget.get_current_client() kernel_id = client.get_kernel_id() sessions_url = client.get_session_url() client.shutdown_kernel() diff --git a/spyder_notebook/widgets/example_app.py b/spyder_notebook/widgets/example_app.py index 26256a45..bb5f9746 100755 --- a/spyder_notebook/widgets/example_app.py +++ b/spyder_notebook/widgets/example_app.py @@ -51,10 +51,26 @@ def _setup_menu(self): new_action.triggered.connect(self.tabwidget.create_new_client) file_menu.addAction(new_action) - open_action = QAction('Open Notebook', self) + open_action = QAction('Open Notebook...', self) open_action.triggered.connect(self.tabwidget.open_notebook) file_menu.addAction(open_action) + save_action = QAction('Save Notebook', self) + save_action.triggered.connect( + lambda checked: self.tabwidget.save_notebook( + self.tabwidget.currentWidget())) + file_menu.addAction(save_action) + + saveas_action = QAction('Save As...', self) + saveas_action.triggered.connect(self.tabwidget.save_as) + file_menu.addAction(saveas_action) + + close_action = QAction('Close Notebook', self) + close_action.triggered.connect( + lambda checked: self.tabwidget.close_client( + self.tabwidget.currentIndex())) + file_menu.addAction(close_action) + if __name__ == '__main__': use_software_rendering() diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index 128fcc6c..a439a38b 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -12,7 +12,8 @@ import sys # Qt imports -from qtpy.compat import getopenfilenames +from qtpy.compat import getopenfilenames, getsavefilename +from qtpy.QtCore import QEventLoop, QTimer from qtpy.QtWidgets import QMessageBox # Third-party imports @@ -84,6 +85,8 @@ def __init__(self, parent, actions, menu, corner_widgets): # Fixes spyder-ide/spyder#561 self.setDocumentMode(True) + self.set_close_function(self.close_client) + def open_notebook(self, filenames=None): """ Open a notebook from file. @@ -174,6 +177,143 @@ def maybe_create_welcome_client(self): self.add_tab(client) return client + def close_client(self, index=None, client=None, save=False): + """ + Close client tab from index or widget (or close current tab). + + First save the note book (unless this is the welcome client or `save` + is True). Then delete the note book if it is in `get_temp_dir()`. + Then shutdown the kernel of the notebook and close the tab. Finally, + create a welcome tab if there are no tabs. + + Parameters + ---------- + index : int or None, optional + Index of tab to be closed. The default is None, meaning that the + value of `client` determines the tab to be closed. + client : NotebookClient or None, optional + Client of tab to be closed. The default is None, meaning that + the current tab is closed (assuming that `index` is also None). + save : bool, optional + The default is False, meaning that the notebook is saved before + the tab is closed. + """ + if not self.count(): + return + if client is not None: + index = self.indexOf(client) + if index is None and client is None: + index = self.currentIndex() + if index is not None: + client = self.widget(index) + + is_welcome = client.get_filename() == WELCOME + if not save and not is_welcome: + self.save_notebook(client) + if not is_welcome: + client.shutdown_kernel() + client.close() + + # Delete notebook file if it is in temporary directory + filename = client.get_filename() + if filename.startswith(get_temp_dir()): + try: + os.remove(filename) + except EnvironmentError: + pass + + # Note: notebook index may have changed after closing related widgets + self.removeTab(self.indexOf(client)) + self.clients.remove(client) + + self.maybe_create_welcome_client() + + def save_notebook(self, client): + """ + Save notebook corresponding to given client. + + If the notebook is newly created and not empty, then ask the user + whether to save it under a new name. + + Parameters + ---------- + client : NotebookClient + Client of notebook to be saved. + """ + client.save() + + # Check filename to find out whether notebook is newly created + path = client.get_filename() + dirname, basename = osp.split(path) + if dirname != NOTEBOOK_TMPDIR or not basename.startswith('untitled'): + return + + # Read file to see whether notebook is empty + wait_save = QEventLoop() + QTimer.singleShot(1000, wait_save.quit) + wait_save.exec_() + nb_contents = nbformat.read(path, as_version=4) + if (len(nb_contents['cells']) == 0 + or len(nb_contents['cells'][0]['source']) == 0): + return + + # Ask user to save notebook with new filename + buttons = QMessageBox.Yes | QMessageBox.No + text = _("{0} has been modified.
" + "Do you want to save changes?").format(basename) + answer = QMessageBox.question( + self, _('Save changes'), text, buttons) + if answer == QMessageBox.Yes: + self.save_as(close=True) + + def save_as(self, name=None, close=False): + """ + Save current notebook under a different file name. + + First, save the note book under the original file name. Then ask user + for a new file name (if `name` is not set), and return if no new name + is given. Then, read the contents of the note book that was just saved + and write them under the new file name. Finally. close the original + tab (unless `close` is True) and open a new tab with the note book + loaded from the new file name. + + Parameters + ---------- + name : str or None, optional + File name under which the notebook is to be saved. The default is + None, meaning that the user should be asked for the file name. + close : bool + The default is False, meaning that the tab should be closed + after saving the notebook. + """ + current_client = self.get_current_client() + current_client.save() + original_path = current_client.get_filename() + if not name: + original_name = osp.basename(original_path) + else: + original_name = name + filename, _selfilter = getsavefilename(self, _("Save notebook"), + original_name, FILES_FILTER) + if filename: + try: + nb_contents = nbformat.read(original_path, as_version=4) + except EnvironmentError as error: + txt = (_("Error while reading {}

{}") + .format(original_path, str(error))) + QMessageBox.critical(self, _("File Error"), txt) + return + try: + nbformat.write(nb_contents, filename) + except EnvironmentError as error: + txt = (_("Error while writing {}

{}") + .format(filename, str(error))) + QMessageBox.critical(self, _("File Error"), txt) + return + if not close: + self.close_client(save=True) + self.create_new_client(filename=filename) + def add_tab(self, widget): """ Add tab containing some notebook widget to the tabbed widget. @@ -187,3 +327,18 @@ def add_tab(self, widget): index = self.addTab(widget, widget.get_short_name()) self.setCurrentIndex(index) self.setTabToolTip(index, widget.get_filename()) + + def get_current_client(self): + """ + Return the currently selected notebook. + + Returns + ------- + client : NotebookClient + """ + try: + client = self.currentWidget() + except AttributeError: + client = None + if client is not None: + return client From 912286aace7cf6d0e2795f281d4ba5c386677a75 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 30 May 2020 15:43:28 +0100 Subject: [PATCH 13/20] Widgets: Remove NotebookTabWidget.get_current_client() The function .currentIndex() works just as well. --- spyder_notebook/notebookplugin.py | 8 ++++---- spyder_notebook/tests/test_plugin.py | 12 ++++++------ spyder_notebook/widgets/notebooktabwidget.py | 17 +---------------- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 0b9514ed..8e47ea50 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -214,7 +214,7 @@ def update_notebook_actions(self): else: self.clear_recent_notebooks_action.setEnabled(False) try: - client = self.tabwidget.get_current_client() + client = self.tabwidget.currentWidget() except AttributeError: # tabwidget is not yet constructed client = None if client: @@ -254,13 +254,13 @@ def get_focus_client(self): def get_current_nbwidget(self): """Return the notebookwidget of the current client.""" - client = self.tabwidget.get_current_client() + client = self.tabwidget.currentWidget() if client is not None: return client.notebookwidget def get_current_client_name(self, short=False): """Get the current client name.""" - client = self.tabwidget.get_current_client() + client = self.tabwidget.currentWidget() if client: if short: return client.get_short_name() @@ -297,7 +297,7 @@ def save_as(self): def open_console(self, client=None): """Open an IPython console for the given client or the current one.""" if not client: - client = self.tabwidget.get_current_client() + client = self.tabwidget.currentWidget() if self.ipyconsole is not None: kernel_id = client.get_kernel_id() if not kernel_id: diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index 3433a26a..ec12df0b 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -114,7 +114,7 @@ def test_shutdown_notebook_kernel(notebook, qtbot): qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Get kernel id for the client - client = notebook.tabwidget.get_current_client() + client = notebook.tabwidget.currentWidget() qtbot.waitUntil(lambda: client.get_kernel_id() is not None, timeout=NOTEBOOK_UP) kernel_id = client.get_kernel_id() @@ -135,7 +135,7 @@ def test_file_in_temp_dir_deleted_after_notebook_closed(notebook, qtbot): qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Get file name - client = notebook.tabwidget.get_current_client() + client = notebook.tabwidget.currentWidget() filename = client.get_filename() # Close the current client @@ -153,7 +153,7 @@ def test_close_nonexisting_notebook(notebook, qtbot): notebook.open_notebook(filenames=[filename]) nbwidget = notebook.get_current_nbwidget() qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) - client = notebook.tabwidget.get_current_client() + client = notebook.tabwidget.currentWidget() # Close tab notebook.tabwidget.close_client() @@ -180,7 +180,7 @@ def test_open_notebook(notebook, qtbot, tmpdir): # and the client has the correct name qtbot.waitUntil(lambda: text_present(nbwidget), timeout=NOTEBOOK_UP) assert text_present(nbwidget) - assert notebook.tabwidget.get_current_client().get_short_name() == "test" + assert notebook.tabwidget.currentWidget().get_short_name() == "test" @flaky(max_runs=3) @@ -218,7 +218,7 @@ def test_save_notebook(notebook, qtbot, tmpdir): qtbot.waitUntil(lambda: text_present(nbwidget, text="test"), timeout=NOTEBOOK_UP) assert text_present(nbwidget, text="test") - assert notebook.tabwidget.get_current_client().get_short_name() == "save" + assert notebook.tabwidget.currentWidget().get_short_name() == "save" def test_save_notebook_as_with_error(mocker, notebook, qtbot, tmpdir): @@ -265,7 +265,7 @@ def test_open_console_when_no_kernel(notebook, qtbot, mocker): qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Shut the kernel down and check that this is successful - client = notebook.tabwidget.get_current_client() + client = notebook.tabwidget.currentWidget() kernel_id = client.get_kernel_id() sessions_url = client.get_session_url() client.shutdown_kernel() diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index a439a38b..e60834c8 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -286,7 +286,7 @@ def save_as(self, name=None, close=False): The default is False, meaning that the tab should be closed after saving the notebook. """ - current_client = self.get_current_client() + current_client = self.currentWidget() current_client.save() original_path = current_client.get_filename() if not name: @@ -327,18 +327,3 @@ def add_tab(self, widget): index = self.addTab(widget, widget.get_short_name()) self.setCurrentIndex(index) self.setTabToolTip(index, widget.get_filename()) - - def get_current_client(self): - """ - Return the currently selected notebook. - - Returns - ------- - client : NotebookClient - """ - try: - client = self.currentWidget() - except AttributeError: - client = None - if client is not None: - return client From 6586b2beec9f945259018ca3ccdb93ea9f295f56 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 30 May 2020 15:52:05 +0100 Subject: [PATCH 14/20] Widgets: Remove argument from close_client() that is not used. --- spyder_notebook/widgets/notebooktabwidget.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index e60834c8..753942d8 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -177,9 +177,9 @@ def maybe_create_welcome_client(self): self.add_tab(client) return client - def close_client(self, index=None, client=None, save=False): + def close_client(self, index=None, save=False): """ - Close client tab from index or widget (or close current tab). + Close client tab with given index (or close current tab). First save the note book (unless this is the welcome client or `save` is True). Then delete the note book if it is in `get_temp_dir()`. @@ -190,22 +190,16 @@ def close_client(self, index=None, client=None, save=False): ---------- index : int or None, optional Index of tab to be closed. The default is None, meaning that the - value of `client` determines the tab to be closed. - client : NotebookClient or None, optional - Client of tab to be closed. The default is None, meaning that - the current tab is closed (assuming that `index` is also None). + current tab is closed. save : bool, optional The default is False, meaning that the notebook is saved before the tab is closed. """ if not self.count(): return - if client is not None: - index = self.indexOf(client) - if index is None and client is None: + if index is None: index = self.currentIndex() - if index is not None: - client = self.widget(index) + client = self.widget(index) is_welcome = client.get_filename() == WELCOME if not save and not is_welcome: From ce04a63981b506ad4aaf85e2070bed0354f1e014 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 30 May 2020 16:57:14 +0100 Subject: [PATCH 15/20] Widgets: Rename arguments in functions for saving and closing tabs The new names better reflects their meaning. --- spyder_notebook/widgets/notebooktabwidget.py | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index 753942d8..67dfad9b 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -177,23 +177,23 @@ def maybe_create_welcome_client(self): self.add_tab(client) return client - def close_client(self, index=None, save=False): + def close_client(self, index=None, save_before_close=True): """ Close client tab with given index (or close current tab). - First save the note book (unless this is the welcome client or `save` - is True). Then delete the note book if it is in `get_temp_dir()`. - Then shutdown the kernel of the notebook and close the tab. Finally, - create a welcome tab if there are no tabs. + First save the notebook (unless this is the welcome client or + `save_before_close` is False). Then delete the notebook if it is in + `get_temp_dir()`. Then shutdown the kernel of the notebook and close + the tab. Finally, create a welcome tab if there are no tabs. Parameters ---------- index : int or None, optional Index of tab to be closed. The default is None, meaning that the current tab is closed. - save : bool, optional - The default is False, meaning that the notebook is saved before - the tab is closed. + save_before_close : bool, optional + Whether to save the notebook before closing the tab. The default + is True. """ if not self.count(): return @@ -202,7 +202,7 @@ def close_client(self, index=None, save=False): client = self.widget(index) is_welcome = client.get_filename() == WELCOME - if not save and not is_welcome: + if save_before_close and not is_welcome: self.save_notebook(client) if not is_welcome: client.shutdown_kernel() @@ -258,27 +258,27 @@ def save_notebook(self, client): answer = QMessageBox.question( self, _('Save changes'), text, buttons) if answer == QMessageBox.Yes: - self.save_as(close=True) + self.save_as(close_after_save=False) - def save_as(self, name=None, close=False): + def save_as(self, name=None, close_after_save=True): """ Save current notebook under a different file name. - First, save the note book under the original file name. Then ask user + First, save the notebook under the original file name. Then ask user for a new file name (if `name` is not set), and return if no new name - is given. Then, read the contents of the note book that was just saved + is given. Then, read the contents of the notebook that was just saved and write them under the new file name. Finally. close the original - tab (unless `close` is True) and open a new tab with the note book - loaded from the new file name. + tab (unless `close_after_save` is False) and open a new tab with the + notebook loaded from the new file name. Parameters ---------- name : str or None, optional File name under which the notebook is to be saved. The default is None, meaning that the user should be asked for the file name. - close : bool - The default is False, meaning that the tab should be closed - after saving the notebook. + close_after_save : bool, optional + Whether to close the original tab after saving the notebook and + before opening it under the new name. The default is True. """ current_client = self.currentWidget() current_client.save() @@ -304,8 +304,8 @@ def save_as(self, name=None, close=False): .format(filename, str(error))) QMessageBox.critical(self, _("File Error"), txt) return - if not close: - self.close_client(save=True) + if close_after_save: + self.close_client(save_before_close=False) self.create_new_client(filename=filename) def add_tab(self, widget): From bcc18b20a3c314408f09e935f7845d4a3bc430b7 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 30 May 2020 16:59:00 +0100 Subject: [PATCH 16/20] Plugin: Remove functions that are not used (except for testing) --- spyder_notebook/notebookplugin.py | 30 +--------------------------- spyder_notebook/tests/test_plugin.py | 18 ++++++++--------- 2 files changed, 10 insertions(+), 38 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 8e47ea50..d285e5b0 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -13,7 +13,7 @@ from qtpy import PYQT4, PYSIDE from qtpy.QtCore import Qt, Signal from qtpy.QtGui import QIcon -from qtpy.QtWidgets import QApplication, QMessageBox, QVBoxLayout, QMenu +from qtpy.QtWidgets import QMessageBox, QVBoxLayout, QMenu # Spyder imports from spyder.api.plugins import SpyderPluginWidget @@ -26,7 +26,6 @@ # Local imports -from spyder_notebook.widgets.client import NotebookClient from spyder_notebook.widgets.notebooktabwidget import NotebookTabWidget @@ -240,33 +239,6 @@ def clear_recent_notebooks(self): self.recent_notebooks = [] self.setup_menu_actions() - def get_clients(self): - """Return notebooks list.""" - return [cl for cl in self.tabwidget.clients - if isinstance(cl, NotebookClient)] - - def get_focus_client(self): - """Return current notebook with focus, if any.""" - widget = QApplication.focusWidget() - for client in self.get_clients(): - if widget is client or widget is client.notebookwidget: - return client - - def get_current_nbwidget(self): - """Return the notebookwidget of the current client.""" - client = self.tabwidget.currentWidget() - if client is not None: - return client.notebookwidget - - def get_current_client_name(self, short=False): - """Get the current client name.""" - client = self.tabwidget.currentWidget() - if client: - if short: - return client.get_short_name() - else: - return client.get_filename() - def create_new_client(self, filename=None): """Create a new notebook or load a pre-existing one.""" # Save spyder_pythonpath before creating a client diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index ec12df0b..e0c0cca2 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -110,7 +110,7 @@ def notebook(qtbot): def test_shutdown_notebook_kernel(notebook, qtbot): """Test that kernel is shutdown from server when closing a notebook.""" # Wait for prompt - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Get kernel id for the client @@ -131,7 +131,7 @@ def test_file_in_temp_dir_deleted_after_notebook_closed(notebook, qtbot): """Test that notebook file in temporary directory is deleted after the notebook is closed.""" # Wait for prompt - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Get file name @@ -151,7 +151,7 @@ def test_close_nonexisting_notebook(notebook, qtbot): # Set up tab with non-existingg notebook filename = osp.join(LOCATION, 'does-not-exist.ipynb') notebook.open_notebook(filenames=[filename]) - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) client = notebook.tabwidget.currentWidget() @@ -173,7 +173,7 @@ def test_open_notebook(notebook, qtbot, tmpdir): # Wait for prompt notebook.open_notebook(filenames=[test_notebook_non_ascii]) - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Assert that the In prompt has "Test" in it @@ -189,7 +189,7 @@ def test_open_notebook(notebook, qtbot, tmpdir): def test_save_notebook(notebook, qtbot, tmpdir): """Test that a notebook can be saved.""" # Wait for prompt - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Writes: a = "test" @@ -210,7 +210,7 @@ def test_save_notebook(notebook, qtbot, tmpdir): notebook.save_as() # Wait for prompt - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Assert that the In prompt has "test" in it @@ -233,7 +233,7 @@ def test_save_notebook_as_with_error(mocker, notebook, qtbot, tmpdir): '.QMessageBox.critical') # Wait for prompt - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Save the notebook @@ -247,7 +247,7 @@ def test_save_notebook_as_with_error(mocker, notebook, qtbot, tmpdir): def test_new_notebook(notebook, qtbot): """Test that a new client is really a notebook.""" # Wait for prompt - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Assert that we have one notebook and the welcome page @@ -261,7 +261,7 @@ def test_open_console_when_no_kernel(notebook, qtbot, mocker): MockMessageBox = mocker.patch('spyder_notebook.notebookplugin.QMessageBox') # Wait for prompt - nbwidget = notebook.get_current_nbwidget() + nbwidget = notebook.tabwidget.currentWidget().notebookwidget qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Shut the kernel down and check that this is successful From 57896ff4145321febdedfb1e4443ffbd82327d65 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 30 May 2020 17:27:59 +0100 Subject: [PATCH 17/20] Widgets: Don't reopen tabs with new notebooks after closing them Before, if the user closes a tab with a newly created notebook and chooses to save it under a different name when prompted, a new tab would be opened with the new file name. This commit prevents the opening of the new tab. --- spyder_notebook/widgets/notebooktabwidget.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index 67dfad9b..d8ac8625 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -258,17 +258,17 @@ def save_notebook(self, client): answer = QMessageBox.question( self, _('Save changes'), text, buttons) if answer == QMessageBox.Yes: - self.save_as(close_after_save=False) + self.save_as(reopen_after_save=False) - def save_as(self, name=None, close_after_save=True): + def save_as(self, name=None, reopen_after_save=True): """ Save current notebook under a different file name. First, save the notebook under the original file name. Then ask user for a new file name (if `name` is not set), and return if no new name is given. Then, read the contents of the notebook that was just saved - and write them under the new file name. Finally. close the original - tab (unless `close_after_save` is False) and open a new tab with the + and write them under the new file name. If `reopen_after_save` is + True, then close the original tab and open a new tab with the notebook loaded from the new file name. Parameters @@ -276,9 +276,9 @@ def save_as(self, name=None, close_after_save=True): name : str or None, optional File name under which the notebook is to be saved. The default is None, meaning that the user should be asked for the file name. - close_after_save : bool, optional - Whether to close the original tab after saving the notebook and - before opening it under the new name. The default is True. + reopen_after_save : bool, optional + Whether to close the original tab and re-open it under the new + file name after saving the notebook. The default is True. """ current_client = self.currentWidget() current_client.save() @@ -304,9 +304,9 @@ def save_as(self, name=None, close_after_save=True): .format(filename, str(error))) QMessageBox.critical(self, _("File Error"), txt) return - if close_after_save: + if reopen_after_save: self.close_client(save_before_close=False) - self.create_new_client(filename=filename) + self.create_new_client(filename=filename) def add_tab(self, widget): """ From 86844fac9f26d9667decc5704a39201eaddabf35 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 30 May 2020 17:48:02 +0100 Subject: [PATCH 18/20] Widget: Remove .clients attribute from NotebookTabWidget QTabbedWidget keeks track of all the tabs so there is no need to duplicate that in NotebookTabWidget. --- spyder_notebook/notebookplugin.py | 17 +++++++++-------- spyder_notebook/tests/test_plugin.py | 5 +++-- spyder_notebook/widgets/notebooktabwidget.py | 6 ------ 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index d285e5b0..ac955520 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -108,8 +108,8 @@ def get_focus_widget(self): def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed.""" - for cl in self.tabwidget.clients: - cl.close() + for client_index in range(self.tabwidget.count()): + self.tabwidget.widget(client_index).close() self.set_option('recent_notebooks', self.recent_notebooks) return True @@ -295,19 +295,20 @@ def handle_switcher_modes(self, mode): if mode != '': return - paths = [client.get_filename() for client in self.tabwidget.clients] - is_unsaved = [False for client in self.tabwidget.clients] + 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 = QIcon(os.path.join(PACKAGE_PATH, 'images', 'icon.svg')) section = self.get_plugin_title() - for path, short_path, client in zip( - paths, short_paths, self.tabwidget.clients): + 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 == self.tabwidget.clients[-1]) + 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) @@ -324,7 +325,7 @@ def handle_switcher_selection(self, item, mode, search_text): return client = item.get_data() - index = self.tabwidget.clients.index(client) + index = self.tabwidget.indexOf(client) self.tabwidget.setCurrentIndex(index) self.switch_to_plugin() self.switcher.hide() diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index e0c0cca2..7bb87f02 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -159,7 +159,8 @@ def test_close_nonexisting_notebook(notebook, qtbot): notebook.tabwidget.close_client() # Assert tab is closed (without raising an exception) - assert client not in notebook.tabwidget.clients + for client_index in range(notebook.tabwidget.count()): + assert client != notebook.tabwidget.widget(client_index) @flaky(max_runs=3) @@ -251,7 +252,7 @@ def test_new_notebook(notebook, qtbot): qtbot.waitUntil(lambda: prompt_present(nbwidget), timeout=NOTEBOOK_UP) # Assert that we have one notebook and the welcome page - assert len(notebook.tabwidget.clients) == 2 + assert notebook.tabwidget.count() == 2 def test_open_console_when_no_kernel(notebook, qtbot, mocker): diff --git a/spyder_notebook/widgets/notebooktabwidget.py b/spyder_notebook/widgets/notebooktabwidget.py index d8ac8625..f3446561 100755 --- a/spyder_notebook/widgets/notebooktabwidget.py +++ b/spyder_notebook/widgets/notebooktabwidget.py @@ -50,8 +50,6 @@ class NotebookTabWidget(Tabs): ---------- actions : list of (QAction or QMenu or None) or None Items to be added to the context menu. - clients : list of NotebookClient - List of notebook clients displayed in tabs in this widget. untitled_num : int Number used in file name of newly created notebooks. """ @@ -76,7 +74,6 @@ def __init__(self, parent, actions, menu, corner_widgets): super().__init__(parent, actions, menu, corner_widgets) self.actions = actions - self.clients = [] self.untitled_num = 0 if not sys.platform == 'darwin': @@ -218,8 +215,6 @@ def close_client(self, index=None, save_before_close=True): # Note: notebook index may have changed after closing related widgets self.removeTab(self.indexOf(client)) - self.clients.remove(client) - self.maybe_create_welcome_client() def save_notebook(self, client): @@ -317,7 +312,6 @@ def add_tab(self, widget): widget : NotebookClient Notebook widget to display in new tab. """ - self.clients.append(widget) index = self.addTab(widget, widget.get_short_name()) self.setCurrentIndex(index) self.setTabToolTip(index, widget.get_filename()) From d9b920350c6242033650999783a847d336924800 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 30 May 2020 17:52:00 +0100 Subject: [PATCH 19/20] Utils: Remove code for Spyder v3 --- spyder_notebook/utils/nbopen.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/spyder_notebook/utils/nbopen.py b/spyder_notebook/utils/nbopen.py index ce8de70f..08ff3eb2 100644 --- a/spyder_notebook/utils/nbopen.py +++ b/spyder_notebook/utils/nbopen.py @@ -21,14 +21,8 @@ from spyder.config.base import DEV, get_home_dir, get_module_path -try: - # Spyder 4 - from spyder.plugins.ipythonconsole.utils.kernelspec import SpyderKernelSpec - KERNELSPEC = ('spyder.plugins.ipythonconsole.utils' - '.kernelspec.SpyderKernelSpec') -except ImportError: - # Spyder 3 - KERNELSPEC = 'spyder.utils.ipython.kernelspec.SpyderKernelSpec' +# Kernel specification to use in notebook server +KERNELSPEC = 'spyder.plugins.ipythonconsole.utils.kernelspec.SpyderKernelSpec' class NBServerError(Exception): From 8a4699da2db9d031d4d9e56461371e10b5941ed0 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 30 May 2020 22:49:38 +0100 Subject: [PATCH 20/20] CI: Exclude example_app.py from coverage report --- .coveragerc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index e26f2eb5..208c6de3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,7 @@ [run] -omit = */tests/* +omit = + */tests/* + spyder_notebook/widgets/example_app.py [report] fail_under=0