Source code for genno.testing.jupyter

"""Testing Juypter notebooks.

Copied 2023-04-27 from the corresponding module in ixmp.
"""

import os
import sys
from warnings import warn

import pytest

nbformat = pytest.importorskip("nbformat")


[docs] def run_notebook(nb_path, tmp_path, env=None, **kwargs): """Execute a Jupyter notebook via :mod:`nbclient` and collect output. Parameters ---------- nb_path : os.PathLike The notebook file to execute. tmp_path : os.PathLike A directory in which to create temporary output. env : mapping, optional Execution environment for :mod:`nbclient`. Default: :obj:`os.environ`. kwargs : Keyword arguments for :class:`nbclient.NotebookClient`. Defaults are set for: "allow_errors" Default :data:`False`. If :obj:`True`, the execution always succeeds, and cell output contains exception information rather than code outputs. "kernel_version" Jupyter kernel to use. Default: either "python2" or "python3", matching the current Python major version. .. warning:: Any existing configuration for this kernel on the local system— such as an IPython start-up file—will be executed when the kernel starts. Code that enables GUI features can interfere with :func:`run_notebook`. "timeout" in seconds; default 10. Returns ------- nb : :class:`nbformat.NotebookNode` Parsed and executed notebook. errors : list Any execution errors. """ import nbformat from nbclient import NotebookClient # Read the notebook nb = nbformat.read(nb_path, as_version=4) # Set default keywords kwargs.setdefault("allow_errors", False) kernel = kwargs.pop("kernel", None) if kernel: # pragma: no cover warn( '"kernel" keyword argument to run_notebook(); use "kernel_name"', DeprecationWarning, 2, ) kwargs.setdefault("kernel_name", kernel or f"python{sys.version_info[0]}") kwargs.setdefault("timeout", 10) # Set up environment env = env or os.environ.copy() env.setdefault("PYDEVD_DISABLE_FILE_VALIDATION", "1") # Create a client and use it to execute the notebook client = NotebookClient(nb, **kwargs, resources=dict(metadata=dict(path=tmp_path))) # Execute the notebook. # `env` is passed from nbclient to jupyter_client.launcher.launch_kernel() client.execute(env=env) # Retrieve error information from cells errors = [ output for cell in nb.cells if "outputs" in cell for output in cell["outputs"] if output.output_type == "error" ] return nb, errors
[docs] def get_cell_output(nb, name_or_index, kind="data"): """Retrieve a cell from `nb` according to its metadata `name_or_index`: The Jupyter notebook format allows specifying a document-wide unique 'name' metadata attribute for each cell: https://nbformat.readthedocs.io/en/latest/format_description.html #cell-metadata Return the cell matching `name_or_index` if :class:`str`; or the cell at the :class:`int` index; or raise :class:`ValueError`. Parameters ---------- kind : str, optional Kind of cell output to retrieve. For 'data', the data in format 'text/plain' is run through :func:`eval`. To retrieve an exception message, use 'evalue'. """ if isinstance(name_or_index, int): cell = nb.cells[name_or_index] else: # pragma: no cover for i, _cell in enumerate(nb.cells): try: if _cell.metadata.jupyter.name == name_or_index: cell = _cell break except AttributeError: continue try: result = cell["outputs"][0][kind] except NameError: # pragma: no cover raise ValueError(f"no cell named {name_or_index}") else: return eval(result["text/plain"]) if kind == "data" else result