Skip to content

Commit

Permalink
Handle non-existing kernels (#309)
Browse files Browse the repository at this point in the history
Use a default kernel name for missing specs or non-existing kernels based on language
  • Loading branch information
jtpio authored and maartenbreddels committed Jul 29, 2019
1 parent 2b657a6 commit 34718a2
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 9 deletions.
18 changes: 18 additions & 0 deletions tests/app/no_kernelspec_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest


@pytest.fixture
def non_existing_kernel_notebook(base_url):
return base_url + "/voila/render/no_kernelspec.ipynb"


@pytest.fixture
def voila_args(notebook_directory, voila_args_extra):
return ['--VoilaTest.root_dir=%r' % notebook_directory] + voila_args_extra


@pytest.mark.gen_test
def test_non_existing_kernel(http_client, non_existing_kernel_notebook):
response = yield http_client.fetch(non_existing_kernel_notebook)
assert response.code == 200
assert 'Executing without a kernelspec' in response.body.decode('utf-8')
18 changes: 18 additions & 0 deletions tests/app/non_existing_kernel_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest


@pytest.fixture
def non_existing_kernel_notebook(base_url):
return base_url + "/voila/render/non_existing_kernel.ipynb"


@pytest.fixture
def voila_args(notebook_directory, voila_args_extra):
return ['--VoilaTest.root_dir=%r' % notebook_directory] + voila_args_extra


@pytest.mark.gen_test
def test_non_existing_kernel(http_client, non_existing_kernel_notebook):
response = yield http_client.fetch(non_existing_kernel_notebook)
assert response.code == 200
assert 'non-existing kernel' in response.body.decode('utf-8')
29 changes: 29 additions & 0 deletions tests/notebooks/no_kernelspec.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print('Executing without a kernelspec')"
]
}
],
"metadata": {
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.7"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
34 changes: 34 additions & 0 deletions tests/notebooks/non_existing_kernel.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print('Executing with non-existing kernel')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Xeus Python 2",
"language": "python",
"name": "xpython2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.14"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
8 changes: 8 additions & 0 deletions voila/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
from jupyter_server.services.config import ConfigManager
from jupyter_server.base.handlers import FileFindHandler

from jupyter_client.kernelspec import KernelSpecManager

from jupyter_core.paths import jupyter_config_path, jupyter_path

from ipython_genutils.py3compat import getcwd
Expand Down Expand Up @@ -359,9 +361,14 @@ def start(self):
self.log.info('Storing connection files in %s.' % self.connection_dir)
self.log.info('Serving static files from %s.' % self.static_root)

self.kernel_spec_manager = KernelSpecManager(
parent=self
)

self.kernel_manager = MappingKernelManager(
parent=self,
connection_dir=self.connection_dir,
kernel_spec_manager=self.kernel_spec_manager,
allowed_message_types=[
'comm_msg',
'comm_info_request',
Expand All @@ -388,6 +395,7 @@ def start(self):
base_url=self.base_url,
server_url=self.server_url or self.base_url,
kernel_manager=self.kernel_manager,
kernel_spec_manager=self.kernel_spec_manager,
allow_remote_access=True,
autoreload=self.autoreload,
voila_jinja2_env=env,
Expand Down
68 changes: 59 additions & 9 deletions voila/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.config_manager import recursive_update
import nbformat

from .execute import executenb
from .exporter import VoilaExporter
Expand Down Expand Up @@ -45,18 +46,11 @@ def get(self, path=None):
else:
nbextensions = []

model = self.contents_manager.get(path=notebook_path)
if 'content' in model:
notebook = model['content']
else:
raise tornado.web.HTTPError(404, 'file not found')

# Fetch kernel name from the notebook metadata
kernel_name = notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)
notebook = yield self.load_notebook(notebook_path)

# Launch kernel and execute notebook
cwd = os.path.dirname(notebook_path)
kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name, path=cwd))
kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=notebook.metadata.kernelspec.name, path=cwd))
km = self.kernel_manager.get_kernel(kernel_id)
result = executenb(notebook, km=km, cwd=cwd, config=self.traitlet_config)

Expand Down Expand Up @@ -98,3 +92,59 @@ def filter_empty_code_cells(cell):
# Compose reply
self.set_header('Content-Type', 'text/html')
self.write(html)

@tornado.gen.coroutine
def load_notebook(self, path):
model = self.contents_manager.get(path=path)
if 'content' not in model:
raise tornado.web.HTTPError(404, 'file not found')
if model.get('type') == 'notebook':
notebook = model['content']
notebook = yield self.fix_notebook(notebook)
raise tornado.gen.Return(notebook) # TODO py2: replace by return
else:
raise tornado.web.HTTPError(500, 'file not supported')

@tornado.gen.coroutine
def fix_notebook(self, notebook):
"""Returns a notebook object with a valid kernelspec.
In case the kernel is not found, we search for a matching kernel based on the language.
"""

# Fetch kernel name from the notebook metadata
if 'kernelspec' not in notebook.metadata:
notebook.metadata.kernelspec = nbformat.NotebookNode()
kernelspec = notebook.metadata.kernelspec
kernel_name = kernelspec.get('name', self.kernel_manager.default_kernel_name)
# We use `maybe_future` to support RemoteKernelSpecManager
all_kernel_specs = yield tornado.gen.maybe_future(self.kernel_spec_manager.get_all_specs())
# Find a spec matching the language if the kernel name does not exist in the kernelspecs
if kernel_name not in all_kernel_specs:
missing_kernel_name = kernel_name
kernel_name = yield self.find_kernel_name_for_language(kernelspec.language.lower(), kernel_specs=all_kernel_specs)
self.log.warning('Could not find a kernel named %r, will use %r', missing_kernel_name, kernel_name)
# We make sure the notebook's kernelspec is correct
notebook.metadata.kernelspec.name = kernel_name
notebook.metadata.kernelspec.display_name = all_kernel_specs[kernel_name]['spec']['display_name']
notebook.metadata.kernelspec.language = all_kernel_specs[kernel_name]['spec']['language']
raise tornado.gen.Return(notebook) # TODO py2: replace by return

@tornado.gen.coroutine
def find_kernel_name_for_language(self, kernel_language, kernel_specs=None):
"""Finds a best matching kernel name given a kernel language.
If multiple kernels matches are found, we try to return the same kernel name each time.
"""
if kernel_specs is None:
kernel_specs = yield tornado.gen.maybe_future(self.kernel_spec_manager.get_all_specs())
matches = [
name for name, kernel in kernel_specs.items()
if kernel["spec"]["language"].lower() == kernel_language
]
if matches:
# Sort by display name to get the same kernel each time.
matches.sort(key=lambda name: kernel_specs[name]["spec"]["display_name"])
raise tornado.gen.Return(matches[0]) # TODO py2: replace by return
else:
raise tornado.web.HTTPError(500, 'No Jupyter kernel for language %r found' % kernel_language)

0 comments on commit 34718a2

Please sign in to comment.