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: Pass connection file when opening console #422

Merged
merged 2 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions spyder_notebook/notebookplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""Notebook plugin."""

# Standard library imports
import logging
import os.path as osp

# Spyder imports
Expand All @@ -20,6 +21,8 @@
from spyder_notebook.widgets.main_widget import NotebookMainWidget
from spyder_notebook.utils.localization import _

logger = logging.getLogger(__name__)


class NotebookPlugin(SpyderDockablePlugin):
"""Spyder Notebook plugin."""
Expand Down Expand Up @@ -94,10 +97,11 @@ def open_notebook(self, filenames=None):
self.get_widget().open_notebook(filenames)

# ------ Private API ------------------------------------------------------
def _open_console(self, kernel_id, tab_name):
def _open_console(self, connection_file, tab_name):
"""Open an IPython console as requested."""
logger.info(f'Opening console with {connection_file=}')
ipyconsole = self.get_plugin(Plugins.IPythonConsole)
ipyconsole.create_client_for_kernel(kernel_id)
ipyconsole.create_client_for_kernel(connection_file)
ipyclient = ipyconsole.get_current_client()
ipyclient.allow_rename = False
ipyconsole.rename_client_tab(ipyclient, tab_name)
Expand Down
192 changes: 2 additions & 190 deletions spyder_notebook/tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@
import json
import os
import os.path as osp
import shutil
import sys
from unittest.mock import Mock

# Third-party library imports
from flaky import flaky
import pytest
import requests
from qtpy.QtCore import Qt, QTimer
from qtpy.QtWebEngineWidgets import WEBENGINE
from qtpy.QtWidgets import QFileDialog, QApplication, QLineEdit, QMainWindow
from qtpy.QtWidgets import QMainWindow
import requests

# Spyder imports
from spyder.api.plugins import Plugins
Expand All @@ -35,7 +32,6 @@
# =============================================================================
NOTEBOOK_UP = 40000
CALLBACK_TIMEOUT = 10000
INTERACTION_CLICK = 100
LOCATION = osp.realpath(osp.join(os.getcwd(), osp.dirname(__file__)))


Expand All @@ -57,24 +53,6 @@ def text_present(nbwidget, qtbot, text="Test"):
return text in nbwidget.dom.toHtml()


def manage_save_dialog(qtbot, fname, directory=LOCATION):
"""
Manage the QFileDialog when saving.

You can use this with QTimer to manage the QFileDialog.
Before calling anything that may show a QFileDialog for save call:
QTimer.singleShot(1000, lambda: manage_save_dialog(qtbot))
"""
top_level_widgets = QApplication.topLevelWidgets()
for w in top_level_widgets:
if isinstance(w, QFileDialog):
if directory is not None:
w.setDirectory(directory)
input_field = w.findChildren(QLineEdit)[0]
input_field.setText(fname)
qtbot.keyClick(w, Qt.Key_Enter)


def is_kernel_up(kernel_id, sessions_url):
"""Determine if the kernel with the id is up."""
sessions_req = requests.get(sessions_url).content.decode()
Expand Down Expand Up @@ -141,172 +119,6 @@ def fake_get_server(filename, interpreter, start):
# =============================================================================
# Tests
# =============================================================================
@flaky(max_runs=5)
def test_shutdown_notebook_kernel(notebook, qtbot):
"""Test that kernel is shutdown from server when closing a notebook."""
# Wait for prompt
nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)

# Get kernel id for the client
client = notebook.get_widget().tabwidget.currentWidget()
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.get_widget().tabwidget.close_client()

# Assert that the kernel is down for the closed client
assert not is_kernel_up(kernel_id, sessions_url)


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_widget().tabwidget.currentWidget().notebookwidget
qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)

# Get file name
client = notebook.get_widget().tabwidget.currentWidget()
filename = client.get_filename()

# Close the current client
notebook.get_widget().tabwidget.close_client()

# Assert file is deleted
assert not osp.exists(filename)


@flaky(max_runs=3)
def test_close_nonexisting_notebook(notebook, qtbot):
"""Test that we can close a tab if the notebook file does not exist.
Regression test for spyder-ide/spyder-notebook#187."""
# Set up tab with non-existingg notebook
filename = osp.join(LOCATION, 'does-not-exist.ipynb')
notebook.open_notebook(filenames=[filename])
nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)
client = notebook.get_widget().tabwidget.currentWidget()

# Close tab
notebook.get_widget().tabwidget.close_client()

# Assert tab is closed (without raising an exception)
for client_index in range(notebook.get_widget().tabwidget.count()):
assert client != notebook.get_widget().tabwidget.widget(client_index)


# TODO Find out what goes wrong on Mac
@flaky(max_runs=3)
@pytest.mark.skipif(sys.platform == 'darwin', reason='Prompt never comes up')
def test_open_notebook_in_non_ascii_dir(notebook, qtbot, tmpdir):
"""Test that a notebook can be opened from a non-ascii directory."""
# Move the test file to non-ascii directory
test_notebook = osp.join(LOCATION, 'test.ipynb')
test_notebook_non_ascii = osp.join(str(tmpdir), u'äöüß', 'test.ipynb')
os.mkdir(os.path.join(str(tmpdir), u'äöüß'))
shutil.copyfile(test_notebook, test_notebook_non_ascii)

# Wait for prompt
notebook.open_notebook(filenames=[test_notebook_non_ascii])
nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)

# Assert that the In prompt has "Test" in it
# and the client has the correct name
qtbot.waitUntil(lambda: text_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)
assert text_present(nbwidget, qtbot)
assert notebook.get_widget().tabwidget.currentWidget().get_short_name() ==\
"test"


@flaky(max_runs=3)
@pytest.mark.skipif(not sys.platform.startswith('linux'),
reason='Test hangs on CI on Windows and MacOS')
def test_save_notebook(notebook, qtbot, tmpdir):
"""Test that a notebook can be saved."""
# Wait for prompt
nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)

# Writes: a = "test"
qtbot.keyClick(nbwidget, Qt.Key_A, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_Space, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_Equal, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_Space, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_QuoteDbl, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_T, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_E, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_S, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_T, delay=INTERACTION_CLICK)
qtbot.keyClick(nbwidget, Qt.Key_QuoteDbl, delay=INTERACTION_CLICK)

# Save the notebook
name = osp.join(str(tmpdir), 'save.ipynb')
QTimer.singleShot(1000, lambda: manage_save_dialog(qtbot, fname=name))
notebook.get_widget().save_as()

# Wait for prompt
nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)

# Assert that the In prompt has "test" in it
# and the client has the correct name
qtbot.waitUntil(lambda: text_present(nbwidget, qtbot, text="test"),
timeout=NOTEBOOK_UP)
assert text_present(nbwidget, qtbot, text="test")
assert notebook.get_widget().tabwidget.currentWidget().get_short_name() ==\
"save"


@flaky(max_runs=3)
@pytest.mark.skipif(os.name == 'nt',
reason='Test hangs often on CI on Windows')
def test_save_notebook_as_with_error(mocker, notebook, qtbot, tmpdir):
"""Test that errors are handled in save_as()."""
# Wait for prompt
nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)

# Set up mocks
name = osp.join(str(tmpdir), 'save.ipynb')
mocker.patch('spyder_notebook.widgets.notebooktabwidget.getsavefilename',
return_value=(name, 'ignored'))
mocker.patch('spyder_notebook.widgets.notebooktabwidget.nbformat.write',
side_effect=PermissionError)
mock_critical = mocker.patch('spyder_notebook.widgets.notebooktabwidget'
'.QMessageBox.critical')

# Save the notebook
notebook.get_widget().save_as()

# Assert that message box is displayed (reporting error raised by write)
assert mock_critical.called


@flaky(max_runs=3)
def test_new_notebook(notebook, qtbot):
"""Test that a new client is really a notebook."""
# Wait for prompt
nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
timeout=NOTEBOOK_UP)

# Assert that we have one notebook and the welcome page
assert notebook.get_widget().tabwidget.count() == 2


# Teardown sometimes fails on Mac with Python 3.8 due to NoProcessException
# in shutdown_server() in notebookapp.py in external notebook library
@flaky
Expand Down
12 changes: 9 additions & 3 deletions spyder_notebook/widgets/main_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)

# Standard library imports
import os.path as osp

# Third-party imports
from jupyter_core.paths import jupyter_runtime_dir
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QMessageBox, QVBoxLayout

Expand Down Expand Up @@ -55,8 +59,8 @@ class NotebookMainWidget(PluginMainWidget):

Parameters
-----------
kernel_id: str
Id of the kernel to open a console for.
connection_file: str
Name of the connection file for the kernel to open a console for.
tab_name: str
Tab name to set for the created console.
"""
Expand Down Expand Up @@ -317,8 +321,10 @@ def open_console(self, client=None):
)
return

connection_file = f'kernel-{kernel_id}.json'
connection_file = osp.join(jupyter_runtime_dir(), connection_file)
self.sig_open_console_requested.emit(
kernel_id,
connection_file,
client.get_short_name()
)

Expand Down
Loading