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

BUG: Get eval results from metadata #597

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ dmypy.json
.DS_Store

.vscode/
/hatch.toml

todos.md
_archive/
13 changes: 9 additions & 4 deletions myst_nb/core/execute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING

from .base import ExecutionError, ExecutionResult, NotebookClientBase # noqa: F401
from .base import (
ExecutionError, # noqa: F401
ExecutionResult, # noqa: F401
NotebookClientBase,
NotebookClientReadOnly,
)
from .cache import NotebookClientCache
from .direct import NotebookClientDirect
from .inline import NotebookClientInline
Expand Down Expand Up @@ -48,15 +53,15 @@ def create_client(
for pattern in nb_config.execution_excludepatterns:
if posix_path.match(pattern):
logger.info(f"Excluded from execution by pattern: {pattern!r}")
return NotebookClientBase(notebook, path, nb_config, logger)
return NotebookClientReadOnly(notebook, path, nb_config, logger)

# 'auto' mode only executes the notebook if it is missing at least one output
missing_outputs = (
len(cell.outputs) == 0 for cell in notebook.cells if cell["cell_type"] == "code"
)
if nb_config.execution_mode == "auto" and not any(missing_outputs):
logger.info("Skipped execution in 'auto' mode (all outputs present)")
return NotebookClientBase(notebook, path, nb_config, logger)
return NotebookClientReadOnly(notebook, path, nb_config, logger)

if nb_config.execution_mode in ("auto", "force"):
return NotebookClientDirect(notebook, path, nb_config, logger)
Expand All @@ -67,4 +72,4 @@ def create_client(
if nb_config.execution_mode == "inline":
return NotebookClientInline(notebook, path, nb_config, logger)

return NotebookClientBase(notebook, path, nb_config, logger)
return NotebookClientReadOnly(notebook, path, nb_config, logger)
39 changes: 35 additions & 4 deletions myst_nb/core/execute/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
from __future__ import annotations

from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any

from nbformat import NotebookNode
from nbformat import NotebookNode, from_dict
from typing_extensions import TypedDict, final

from myst_nb.core.config import NbParserConfig
from myst_nb.core.loggers import LoggerType
from myst_nb.core.nb_to_tokens import nb_node_to_dict
from myst_nb.ext.glue import extract_glue_data

if TYPE_CHECKING:
from markdown_it.tree import SyntaxTreeNode


class ExecutionResult(TypedDict):
"""Result of executing a notebook."""
Expand Down Expand Up @@ -39,7 +42,7 @@ class EvalNameError(Exception):


class NotebookClientBase:
"""A base client for interacting with Jupyter notebooks.
"""A client base class for interacting with Jupyter notebooks.

This class is intended to be used as a context manager,
and should only be entered once.
Expand Down Expand Up @@ -166,7 +169,9 @@ def code_cell_outputs(
cell = cells[cell_index]
return cell.get("execution_count", None), cell.get("outputs", [])

def eval_variable(self, name: str) -> list[NotebookNode]:
def eval_variable(
self, name: str, current_cell: SyntaxTreeNode
) -> list[NotebookNode]:
"""Retrieve the value of a variable from the kernel.

:param name: the name of the variable,
Expand All @@ -177,3 +182,29 @@ def eval_variable(self, name: str) -> list[NotebookNode]:
:raises EvalNameError: if the variable name is invalid
"""
raise NotImplementedError


class NotebookClientReadOnly(NotebookClientBase):
"""A notebook client that does not execute the notebook."""

def eval_variable(
self, name: str, current_cell: SyntaxTreeNode
) -> list[NotebookNode]:
"""Retrieve the value of a variable from the current cell metadata if possible.

:param name: the name of the variable
:returns: code cell outputs
:raises RetrievalError: if the cell metadata does not contain the eval result
:raises EvalNameError: if the variable name is invalid
"""
for ux in current_cell.meta.get("metadata", {}).get("user_expressions", []):
if ux["expression"] == name:
return from_dict([ux["result"]])

from myst_nb.core.variables import RetrievalError

msg = (
f"Cell {current_cell.meta.get('index', '')} does "
f"not contain execution result for variable {name!r}"
)
raise RetrievalError(msg)
8 changes: 7 additions & 1 deletion myst_nb/core/execute/inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from tempfile import mkdtemp
import time
import traceback
from typing import TYPE_CHECKING

from nbclient.client import (
CellControlSignal,
Expand All @@ -25,6 +26,9 @@

from .base import EvalNameError, ExecutionError, NotebookClientBase

if TYPE_CHECKING:
from markdown_it.tree import SyntaxTreeNode


class NotebookClientInline(NotebookClientBase):
"""A notebook client that executes the notebook inline,
Expand Down Expand Up @@ -148,7 +152,9 @@ def code_cell_outputs(
cell = cells[cell_index]
return cell.get("execution_count", None), cell.get("outputs", [])

def eval_variable(self, name: str) -> list[NotebookNode]:
def eval_variable(
self, name: str, current_cell: SyntaxTreeNode
) -> list[NotebookNode]:
if not re.match(self.nb_config.eval_name_regex, name):
raise EvalNameError(name)
return self._client.eval_expression(name)
Expand Down
2 changes: 2 additions & 0 deletions myst_nb/core/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class MditRenderMixin:
current_node: Any
current_node_context: Any
create_highlighted_code_block: Any
current_cell: SyntaxTreeNode

@property
def nb_config(self: SelfType) -> NbParserConfig:
Expand Down Expand Up @@ -114,6 +115,7 @@ def render_nb_cell_markdown(self: SelfType, token: SyntaxTreeNode) -> None:
# it would be nice to "wrap" this in a container that included the metadata,
# but unfortunately this would break the heading structure of docutils/sphinx.
# perhaps we add an "invisible" (non-rendered) marker node to the document tree,
self.current_cell = token # type: ignore[union-attr]
self.render_children(token)

def render_nb_cell_raw(self: SelfType, token: SyntaxTreeNode) -> None:
Expand Down
6 changes: 5 additions & 1 deletion myst_nb/ext/eval/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ def retrieve_eval_data(document: nodes.document, key: str) -> list[VariableOutpu

# evaluate the variable
try:
outputs = element.renderer.nb_client.eval_variable(key)
outputs = element.renderer.nb_client.eval_variable(
key, element.renderer.current_cell
)
except RetrievalError:
raise
except NotImplementedError:
raise RetrievalError("This document does not have a running kernel")
except EvalNameError:
Expand Down
2 changes: 0 additions & 2 deletions tests/notebooks/with_eval.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
file_format: mystnb
kernelspec:
name: python3
mystnb:
execution_mode: 'inline'
Comment on lines -5 to -6
Copy link
Contributor Author

@flying-sheep flying-sheep Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is specified also in the tests. If also specified here, the version here overrides the one specified in the tests. In order to add another test, I removed it here. All tests pass.

---

# Inline evaluation
Expand Down
55 changes: 55 additions & 0 deletions tests/notebooks/with_eval_executed.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"x = 2"
]
},
{
"cell_type": "markdown",
"metadata": {
"user_expressions": [
{
"expression": "x",
"result": {
"data": {
"text/plain": "2'"
},
"metadata": {},
"status": "ok"
}
}
]
},
"source": [
"{eval}`x`"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"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.12.2"
},
"test_name": "notebook1"
},
"nbformat": 4,
"nbformat_minor": 2
}
25 changes: 25 additions & 0 deletions tests/test_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,28 @@ def test_sphinx(sphinx_run, clean_doctree, file_regression):
doctree.pformat(),
encoding="utf-8",
)


@pytest.mark.sphinx_params(
"with_eval_executed.ipynb", conf={"nb_execution_mode": "off"}
)
def test_no_build(sphinx_run, clean_doctree, file_regression):
"""Test using eval from cell metadata."""
sphinx_run.build()
assert sphinx_run.warnings() == ""
doctree = clean_doctree(sphinx_run.get_resolved_doctree("with_eval_executed"))
file_regression.check(doctree.pformat(), encoding="utf-8")


@pytest.mark.sphinx_params("with_eval.md", conf={"nb_execution_mode": "off"})
def test_no_build_error(sphinx_run):
"""Test that lack of execution results errors."""
sphinx_run.build()
assert (
"Cell 2 does not contain execution result for variable 'a'"
in sphinx_run.warnings()
)
assert (
"Cell 4 does not contain execution result for variable 'img'"
in sphinx_run.warnings()
)
8 changes: 8 additions & 0 deletions tests/test_eval/test_no_build.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<document source="with_eval_executed">
<container cell_index="0" cell_metadata="{}" classes="cell" exec_count="1" nb_element="cell_code">
<container classes="cell_input" nb_element="cell_code_source">
<literal_block language="ipython3" linenos="False" xml:space="preserve">
x = 2
<paragraph>
<inline classes="output text_plain">
2’
Loading