diff --git a/docs/computation/execute.md b/docs/computation/execute.md index 0b280224..a2cde2a6 100644 --- a/docs/computation/execute.md +++ b/docs/computation/execute.md @@ -23,7 +23,8 @@ To trigger the execution of notebook pages, use the global `nb_execution_mode` c | -------- | -------------------------------------------------------------------- | | `off` | Do not execute the notebook | | `force` | Always execute the notebook (before parsing) | -| `auto` | Execute notebooks with missing outputs (before parsing) | +| `auto` | Execute notebooks with any missing outputs (before parsing) | +| `lazy` | Execute notebooks without any existing outputs (before parsing) | | `cache` | Execute notebook and store/retrieve outputs from a cache | | `inline` | Execute the notebook during parsing (allows for variable evaluation) | @@ -36,6 +37,12 @@ nb_execution_mode = "auto" This will only execute notebooks that are missing at least one output. If a notebook has *all* of its outputs populated, then it will not be executed. +To only execute notebooks that do not have *any* of its outputs populated, change the above configuration value to: + +```python +nb_execution_mode = "lazy" +``` + To force the execution of all notebooks, regardless of their outputs, change the above configuration value to: ```python diff --git a/docs/conf.py b/docs/conf.py index 0f954923..6fa1b3d2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,6 +95,7 @@ "off", "force", "auto", + "lazy", "cache", "commonmark", "gfm", diff --git a/myst_nb/core/config.py b/myst_nb/core/config.py index 023cccac..6c2ff456 100644 --- a/myst_nb/core/config.py +++ b/myst_nb/core/config.py @@ -197,13 +197,16 @@ def __post_init__(self): "sections": (Section.global_lvl, Section.file_lvl, Section.execute), }, ) - execution_mode: Literal["off", "force", "auto", "cache", "inline"] = dc.field( + execution_mode: Literal[ + "off", "force", "auto", "lazy", "cache", "inline" + ] = dc.field( default="auto", metadata={ "validator": in_( [ "off", "auto", + "lazy", "force", "cache", "inline", diff --git a/myst_nb/core/execute/__init__.py b/myst_nb/core/execute/__init__.py index 5945988a..dc559982 100644 --- a/myst_nb/core/execute/__init__.py +++ b/myst_nb/core/execute/__init__.py @@ -50,7 +50,8 @@ def create_client( logger.info(f"Excluded from execution by pattern: {pattern!r}") return NotebookClientBase(notebook, path, nb_config, logger) - # 'auto' mode only executes the notebook if it is missing at least one output + # 'auto' mode only executes the notebook if it is missing at least one output, + # 'lazy' mode only executes if all outputs are missing. missing_outputs = ( len(cell.outputs) == 0 for cell in notebook.cells if cell["cell_type"] == "code" ) @@ -58,7 +59,11 @@ def create_client( logger.info("Skipped execution in 'auto' mode (all outputs present)") return NotebookClientBase(notebook, path, nb_config, logger) - if nb_config.execution_mode in ("auto", "force"): + if nb_config.execution_mode == "lazy" and not all(missing_outputs): + logger.info("Skipped execution in 'lazy' mode (at least one output present)") + return NotebookClientBase(notebook, path, nb_config, logger) + + if nb_config.execution_mode in ("auto", "lazy", "force"): return NotebookClientDirect(notebook, path, nb_config, logger) if nb_config.execution_mode == "cache": diff --git a/tests/test_execute.py b/tests/test_execute.py index 9597182a..f763135a 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -52,6 +52,21 @@ def test_basic_unrun_auto(sphinx_run, file_regression, check_nbs): assert data["succeeded"] is True +@pytest.mark.sphinx_params("basic_unrun.ipynb", conf={"nb_execution_mode": "lazy"}) +def test_basic_unrun_lazy(sphinx_run, file_regression, check_nbs): + sphinx_run.build() + # print(sphinx_run.status()) + assert sphinx_run.warnings() == "" + assert "test_name" in sphinx_run.app.env.metadata["basic_unrun"] + regress_nb_doc(file_regression, sphinx_run, check_nbs) + + assert NbMetadataCollector.new_exec_data(sphinx_run.env) + data = NbMetadataCollector.get_exec_data(sphinx_run.env, "basic_unrun") + assert data + assert data["method"] == "lazy" + assert data["succeeded"] is True + + @pytest.mark.sphinx_params("basic_unrun.ipynb", conf={"nb_execution_mode": "cache"}) def test_basic_unrun_cache(sphinx_run, file_regression, check_nbs): """The outputs should be populated.""" @@ -149,6 +164,21 @@ def test_basic_failing_auto(sphinx_run, file_regression, check_nbs): sphinx_run.get_report_file() +@pytest.mark.skipif(ipy_version[0] < 8, reason="Error message changes for ipython v8") +@pytest.mark.sphinx_params("basic_failing.ipynb", conf={"nb_execution_mode": "lazy"}) +def test_basic_failing_lazy(sphinx_run, file_regression, check_nbs): + sphinx_run.build() + assert "Executing notebook failed" in sphinx_run.warnings() + regress_nb_doc(file_regression, sphinx_run, check_nbs) + + data = NbMetadataCollector.get_exec_data(sphinx_run.env, "basic_failing") + assert data + assert data["method"] == "lazy" + assert data["succeeded"] is False + assert data["traceback"] + sphinx_run.get_report_file() + + @pytest.mark.skipif(ipy_version[0] < 8, reason="Error message changes for ipython v8") @pytest.mark.sphinx_params("basic_failing.ipynb", conf={"nb_execution_mode": "inline"}) def test_basic_failing_inline(sphinx_run, file_regression, check_nbs):