Skip to content
Draft
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
41 changes: 38 additions & 3 deletions src/xdoctest/utils/util_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,28 @@ def import_notebook_from_path(
return module


def _has_jupyter_kernel(kernel_name: str | None = None) -> bool:
"""Return whether a usable Jupyter kernelspec is registered.

Importing :mod:`jupyter_client` or :mod:`ipykernel` is not sufficient:
isolated environments may have both packages installed without exposing a
kernelspec on Jupyter's search path.
"""
try:
import jupyter_client.kernelspec
except ImportError:
return False

if kernel_name is None:
kernel_name = jupyter_client.kernelspec.NATIVE_KERNEL_NAME
try:
jupyter_client.kernelspec.get_kernel_spec(kernel_name)
except jupyter_client.kernelspec.NoSuchKernel:
return False
else:
return True


def execute_notebook(
ipynb_fpath: str | os.PathLike,
timeout: typing.Any = None,
Expand All @@ -258,8 +280,11 @@ def execute_notebook(

Example:
>>> # xdoctest: +REQUIRES(PY3, module:IPython, module:nbconvert, CPYTHON)
>>> import xdoctest
>>> from xdoctest import utils
>>> from os.path import join
>>> if not _has_jupyter_kernel():
>>> raise xdoctest.ExitTestException('no Jupyter kernel is registered')
>>> self = utils.TempDir()
>>> dpath = self.ensure()
>>> ipynb_fpath = join(dpath, 'hello_world.ipydb')
Expand Down Expand Up @@ -322,12 +347,22 @@ def _make_test_notebook_fpath(

# TODO: is there an API to generate kernelspec json correctly?
kernel_name = jupyter_client.kernelspec.NATIVE_KERNEL_NAME
spec = jupyter_client.kernelspec.get_kernel_spec(kernel_name)
try:
spec = jupyter_client.kernelspec.get_kernel_spec(kernel_name)
except jupyter_client.kernelspec.NoSuchKernel:
# Constructing a notebook file does not require an executable kernel.
# Use conventional Python metadata when the environment has the
# Jupyter libraries installed but no registered native kernelspec.
display_name = 'Python 3'
language = 'python'
else:
display_name = spec.display_name
language = spec.language
metadata = {
'kernelspec': {
'name': kernel_name,
'display_name': spec.display_name,
'language': spec.language,
'display_name': display_name,
'language': language,
}
}
# Use nbformat API to create notebook structure and cell json
Expand Down
47 changes: 47 additions & 0 deletions tests/test_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ def skip_notebook_tests_if_unsupported() -> None:
import IPython # NOQA
import nbconvert # NOQA
import nbformat # NOQA
import jupyter_client.kernelspec

kernel_name = jupyter_client.kernelspec.NATIVE_KERNEL_NAME
try:
jupyter_client.kernelspec.get_kernel_spec(kernel_name)
except jupyter_client.kernelspec.NoSuchKernel:
pytest.skip('No Jupyter kernel named {!r}'.format(kernel_name))

import platform

Expand Down Expand Up @@ -110,3 +117,43 @@ def test_xdoctest_outside_notebook() -> None:
text = info['out']
assert isinstance(text, str)
assert '3 / 3 passed' in text


def test_missing_kernel_is_treated_as_unsupported(monkeypatch) -> None:
jupyter_client = pytest.importorskip('jupyter_client')
pytest.importorskip('IPython')
pytest.importorskip('nbconvert')
pytest.importorskip('nbformat')

def _raise_no_such_kernel(kernel_name):
raise jupyter_client.kernelspec.NoSuchKernel(kernel_name)

monkeypatch.setattr(
jupyter_client.kernelspec,
'get_kernel_spec',
_raise_no_such_kernel,
)
with pytest.raises(pytest.skip.Exception, match='No Jupyter kernel'):
skip_notebook_tests_if_unsupported()


def test_make_notebook_without_registered_kernel(monkeypatch, tmp_path) -> None:
jupyter_client = pytest.importorskip('jupyter_client')
nbformat = pytest.importorskip('nbformat')
from xdoctest.utils import util_notebook

def _raise_no_such_kernel(kernel_name):
raise jupyter_client.kernelspec.NoSuchKernel(kernel_name)

monkeypatch.setattr(
jupyter_client.kernelspec,
'get_kernel_spec',
_raise_no_such_kernel,
)
fpath = tmp_path / 'demo.ipynb'
util_notebook._make_test_notebook_fpath(fpath, ['x = 1'])
with fpath.open('r') as file:
notebook = nbformat.read(file, as_version=nbformat.NO_CONVERT)
assert notebook.metadata.kernelspec.name == 'python3'
assert notebook.metadata.kernelspec.display_name == 'Python 3'
assert notebook.metadata.kernelspec.language == 'python'
Loading