Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Split off part of NotebookPlugin into NotebookTabWidget #283

Merged
merged 20 commits into from
Jun 1, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d4bdd1d
Tests: Remove code specific to Python 2
jitseniesen May 27, 2020
ea93c38
Plugin: Remove .move_tab()
jitseniesen May 25, 2020
c98c2b8
Widgets: Add new class, NotebookTabWidget
jitseniesen May 25, 2020
80afe47
Refactor: Move clients attribute out of NotebookPlugin
jitseniesen May 25, 2020
a87049d
Refactor: Move .add_tab() from plugin to tab widget
jitseniesen May 26, 2020
5529fd2
Widgets: Decouple NotebookClient and NotebookWidget from plugin
jitseniesen May 26, 2020
713c53a
Widgets: Add example app, showing how to use widgets outside Spyder
jitseniesen May 26, 2020
19e9f86
Refactor: Move creation of welcome tab from plugin to tab widget
jitseniesen May 28, 2020
5e915fb
Refactor: Move opening notebooks from plugin to tab widget
jitseniesen May 28, 2020
102604e
Widgets: Remove unused parameter give_focus
jitseniesen May 29, 2020
8d84a48
Widgets: Remove testing attribute from tab widget
jitseniesen May 29, 2020
c62342a
Refactor: Move saving and closing from plugin to tab widget
jitseniesen May 29, 2020
912286a
Widgets: Remove NotebookTabWidget.get_current_client()
jitseniesen May 30, 2020
6586b2b
Widgets: Remove argument from close_client() that is not used.
jitseniesen May 30, 2020
ce04a63
Widgets: Rename arguments in functions for saving and closing tabs
jitseniesen May 30, 2020
bcc18b2
Plugin: Remove functions that are not used (except for testing)
jitseniesen May 30, 2020
57896ff
Widgets: Don't reopen tabs with new notebooks after closing them
jitseniesen May 30, 2020
86844fa
Widget: Remove .clients attribute from NotebookTabWidget
jitseniesen May 30, 2020
d9b9203
Utils: Remove code for Spyder v3
jitseniesen May 30, 2020
8a4699d
CI: Exclude example_app.py from coverage report
jitseniesen May 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor: Move saving and closing from plugin to tab widget
  • Loading branch information
jitseniesen committed May 29, 2020
commit c62342a516c24991571203ae89b24b783259a9fe
133 changes: 12 additions & 121 deletions spyder_notebook/notebookplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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 = _("<b>{0}</b> has been modified.<br>"
"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 {}<p>{}")
.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 {}<p>{}")
.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
Expand All @@ -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:
Expand Down
28 changes: 14 additions & 14 deletions spyder_notebook/tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
18 changes: 17 additions & 1 deletion spyder_notebook/widgets/example_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading