Skip to content

Commit 4d10496

Browse files
committed
Keep stored outputs of non-executable notebooks and copy all mplstyle files
1 parent 1b0743b commit 4d10496

3 files changed

Lines changed: 98 additions & 30 deletions

File tree

‎scripts/build.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
copy_figures,
4848
generate_version_manifest,
4949
)
50-
from lib.executor import execute_notebooks
50+
from lib.executor import execute_notebooks, save_stored_outputs
5151
from lib.roadmap import build_roadmap
5252

5353
# Optional: embeddings for semantic search
@@ -431,8 +431,17 @@ def build_version(
431431
else:
432432
print(f" No notebooks found")
433433

434-
# 3. Execute notebooks
434+
# 3. Execute notebooks, non-executable ones keep the outputs they ship with
435435
notebook_results: dict = {}
436+
stored = [nb for nb in notebooks if not nb.get("executable", True)]
437+
if stored:
438+
print(f" Saving stored outputs...")
439+
n_stored = sum(
440+
1 for nb in stored
441+
if save_stored_outputs(notebooks_source / nb["file"], output_dir)
442+
)
443+
print(f" {n_stored} of {len(stored)} notebooks with stored outputs")
444+
436445
if execute and notebooks:
437446
print(f" Executing notebooks...")
438447
notebook_results = execute_notebooks(output_dir, notebooks, parallel=True)

‎scripts/lib/executor.py‎

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,90 @@ def _execute_single_notebook(
174174
except Exception as e:
175175
return {"success": False, "error": f"Failed to read executed notebook: {e}", "cells": {}}
176176

177-
# Extract outputs from each cell
178-
# Skip the first code cell (injected setup) when mapping indices
177+
# Extract outputs from each cell, skipping the injected setup cell
178+
cells_output = _extract_cell_outputs(executed_nb, name, figures_dir, skip_setup_cell=True)
179+
180+
# Clean up temporary executed notebook
181+
if result.get("output_path"):
182+
try:
183+
result["output_path"].unlink()
184+
except Exception:
185+
pass
186+
187+
duration = int((time.time() - start_time) * 1000)
188+
189+
output_data = {
190+
"cells": cells_output,
191+
"executedAt": datetime.now(timezone.utc).isoformat(),
192+
"duration": duration,
193+
"success": True,
194+
}
195+
196+
_save_output(outputs_dir / f"{name}.json", output_data)
197+
198+
return output_data
199+
200+
201+
def save_stored_outputs(notebook_path: Path, version_dir: Path) -> dict[str, Any] | None:
202+
"""
203+
Save the outputs stored in a notebook that is not executed by this build.
204+
205+
Non-executable notebooks ship with their outputs, but the copied notebooks
206+
are stripped. The stored outputs are written in the same format as the
207+
outputs of executed notebooks, so the frontend shows them the same way.
208+
209+
Returns the output data, or None if the notebook has no stored outputs.
210+
"""
211+
name = notebook_path.stem
212+
213+
try:
214+
with open(notebook_path, "r", encoding="utf-8") as f:
215+
notebook = json.load(f)
216+
except Exception as e:
217+
return {"success": False, "error": f"Failed to read notebook: {e}", "cells": {}}
218+
219+
outputs_dir = version_dir / "outputs"
220+
figures_dir = version_dir / "figures"
221+
figures_dir.mkdir(parents=True, exist_ok=True)
222+
223+
cells_output = _extract_cell_outputs(notebook, name, figures_dir, skip_setup_cell=False)
224+
if not cells_output:
225+
return None
226+
227+
output_data = {
228+
"cells": cells_output,
229+
"executedAt": datetime.now(timezone.utc).isoformat(),
230+
"duration": 0,
231+
"success": True,
232+
"stored": True,
233+
}
234+
235+
outputs_dir.mkdir(parents=True, exist_ok=True)
236+
_save_output(outputs_dir / f"{name}.json", output_data)
237+
238+
return output_data
239+
240+
241+
def _extract_cell_outputs(
242+
notebook: dict,
243+
name: str,
244+
figures_dir: Path,
245+
skip_setup_cell: bool,
246+
) -> dict[str, dict]:
247+
"""
248+
Extract stdout, stderr and figures of each code cell.
249+
250+
Cell indices refer to the original notebook. With skip_setup_cell the first
251+
code cell is the injected setup cell, it is skipped and the indices are
252+
shifted back by one.
253+
254+
Returns dict mapping cell index to its outputs, cells without output are omitted.
255+
"""
179256
cells_output = {}
180257
figure_count = 0
181-
setup_cell_skipped = False
258+
setup_cell_skipped = not skip_setup_cell
182259

183-
for i, cell in enumerate(executed_nb.get("cells", [])):
260+
for i, cell in enumerate(notebook.get("cells", [])):
184261
if cell.get("cell_type") != "code":
185262
continue
186263

@@ -190,7 +267,7 @@ def _execute_single_notebook(
190267
continue
191268

192269
# Adjust index to match original notebook (subtract 1 for skipped setup cell)
193-
original_index = i - 1
270+
original_index = i - 1 if skip_setup_cell else i
194271

195272
cell_output = {
196273
"stdout": None,
@@ -238,25 +315,7 @@ def _execute_single_notebook(
238315
if cell_output["stdout"] or cell_output["stderr"] or cell_output["figures"]:
239316
cells_output[str(original_index)] = cell_output
240317

241-
# Clean up temporary executed notebook
242-
if result.get("output_path"):
243-
try:
244-
result["output_path"].unlink()
245-
except Exception:
246-
pass
247-
248-
duration = int((time.time() - start_time) * 1000)
249-
250-
output_data = {
251-
"cells": cells_output,
252-
"executedAt": datetime.now(timezone.utc).isoformat(),
253-
"duration": duration,
254-
"success": True,
255-
}
256-
257-
_save_output(outputs_dir / f"{name}.json", output_data)
258-
259-
return output_data
318+
return cells_output
260319

261320

262321
def _execute_with_nbconvert(notebook_path: Path) -> dict[str, Any]:

‎scripts/lib/notebooks.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ def copy_notebooks(source_dir: Path, target_dir: Path, docs_root: Path | None =
4343
except Exception as e:
4444
print(f" Warning: Failed to process {nb_path.name}: {e}")
4545

46-
# Copy mplstyle file if it exists (notebooks reference ../pathsim_docs.mplstyle)
47-
mplstyle_source = source_dir.parent / "pathsim_docs.mplstyle"
48-
if mplstyle_source.exists():
49-
shutil.copy2(mplstyle_source, target_dir / "pathsim_docs.mplstyle")
46+
# Copy mplstyle files, notebooks reference them one level up
47+
# (e.g. ../pathsim_docs.mplstyle, ../fastsim_docs.mplstyle)
48+
for mplstyle_source in source_dir.parent.glob("*.mplstyle"):
49+
shutil.copy2(mplstyle_source, target_dir / mplstyle_source.name)
5050

5151
# Copy referenced figures by searching docs directory. Raster figures are
5252
# re-encoded to WebP, which renames foo.png -> foo.webp; the returned mapping

0 commit comments

Comments
 (0)