Skip to content

Commit e510de1

Browse files
authored
fix: regenerate_notebook resolves the source by relative path, not filename (#263)
fix: regenerate_notebook resolves the source by relative path, not filename
2 parents 7ad7a61 + bf27665 commit e510de1

2 files changed

Lines changed: 77 additions & 6 deletions

File tree

autohands/build_util.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ def is_clean_skip_exit(output: str) -> bool:
358358
return bool(tail) and _SKIP_EXIT_RE.match(tail[-1]) is not None
359359

360360

361-
def regenerate_notebook(nb_path, scripts_dir) -> Path:
361+
def regenerate_notebook(nb_path, scripts_dir, rel=None) -> Path:
362362
"""
363363
Regenerate one notebook from its source ``.py`` into a temp dir.
364364
@@ -376,8 +376,13 @@ def regenerate_notebook(nb_path, scripts_dir) -> Path:
376376
The notebook that failed, e.g. ``notebooks/imaging/model_fit.ipynb``.
377377
scripts_dir
378378
The directory holding the source scripts, e.g. ``<workspace>/scripts``.
379-
The source is looked up at the notebook's path relative to its own
380-
``notebooks/`` root, with a ``.py`` suffix.
379+
rel
380+
The notebook's path RELATIVE to its own ``notebooks/`` root, e.g.
381+
``imaging/model_fit.ipynb``. The source script is that same relative
382+
path under ``scripts_dir`` with a ``.py`` suffix. Omitting it falls back
383+
to the bare filename, which is only correct for a notebook sitting at
384+
the root — every workspace notebook lives in a subdirectory, so callers
385+
iterating a tree must pass this.
381386
382387
Returns
383388
-------
@@ -390,7 +395,8 @@ def regenerate_notebook(nb_path, scripts_dir) -> Path:
390395
"""
391396
nb_path = Path(nb_path)
392397
scripts_dir = Path(scripts_dir)
393-
script_path = scripts_dir / Path(nb_path.name).with_suffix(".py")
398+
rel = Path(rel) if rel is not None else Path(nb_path.name)
399+
script_path = scripts_dir / rel.with_suffix(".py")
394400
if not script_path.exists():
395401
raise FileNotFoundError(f"No source script at {script_path}")
396402

@@ -553,7 +559,7 @@ def _classify_notebook_run(run_target, recorded, report, env, timeout_secs):
553559

554560

555561
def execute_notebook(f, report=None, env=None, write_back=True,
556-
retry_from_scripts=None, report_as=None):
562+
retry_from_scripts=None, report_as=None, notebook_rel=None):
557563
"""
558564
Execute one notebook as a subprocess, with the kernel cwd at the repo root.
559565
@@ -594,7 +600,7 @@ def execute_notebook(f, report=None, env=None, write_back=True,
594600

595601
print(" notebook failed; regenerating from source script and retrying...")
596602
try:
597-
regenerated = regenerate_notebook(f, retry_from_scripts)
603+
regenerated = regenerate_notebook(f, retry_from_scripts, rel=notebook_rel)
598604
except Exception as exc:
599605
# No source script, or generation itself failed. The first attempt's
600606
# FAIL stands — the recovery was unavailable, not the notebook fixed.
@@ -690,12 +696,20 @@ def execute_notebooks_in_folder(
690696
else:
691697
from env_config import build_env_for_script
692698
env = build_env_for_script(file, env_config)
699+
# The path relative to the notebooks root is what maps a notebook
700+
# to its source script; the bare filename would collide across
701+
# subdirectories and miss the source entirely.
702+
try:
703+
notebook_rel = file.relative_to(Path.cwd() / directory)
704+
except ValueError: # pragma: no cover - file outside the root
705+
notebook_rel = Path(file.name)
693706
execute_notebook(
694707
file,
695708
report=report,
696709
env=env,
697710
write_back=write_back,
698711
retry_from_scripts=retry_from_scripts,
712+
notebook_rel=notebook_rel,
699713
)
700714

701715

tests/test_notebook_delegation.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,60 @@ def counting(*args, **kwargs):
217217
assert status == "passed"
218218
assert len(calls) == 1
219219
assert report.results[0].status == Status.PASSED
220+
221+
222+
class TestNestedNotebooks:
223+
"""
224+
A notebook's source script is found by its path relative to `notebooks/`.
225+
226+
Every workspace notebook lives in a subdirectory (`imaging/`, `modeling/`,
227+
...). Resolving the source by bare filename would look in the wrong place
228+
and, worse, could collide across subdirectories — two `model_fit.ipynb`
229+
under different topics map to two different scripts.
230+
"""
231+
232+
def test_nested_notebook_regenerates_from_its_own_source(self, workspace):
233+
_write_notebook(
234+
workspace / "notebooks" / "imaging" / "model_fit.ipynb",
235+
"raise RuntimeError('stale')",
236+
)
237+
(workspace / "scripts" / "imaging").mkdir(parents=True)
238+
(workspace / "scripts" / "imaging" / "model_fit.py").write_text("x = 1 + 1\n")
239+
# A decoy at the root: resolving by bare filename would pick this up.
240+
(workspace / "scripts" / "model_fit.py").write_text("raise RuntimeError('wrong source')\n")
241+
report = _report()
242+
243+
status = execute_notebook(
244+
workspace / "notebooks" / "imaging" / "model_fit.ipynb",
245+
report=report,
246+
write_back=False,
247+
retry_from_scripts=workspace / "scripts",
248+
notebook_rel=Path("imaging/model_fit.ipynb"),
249+
)
250+
251+
assert status == "passed", "must regenerate from scripts/imaging/, not the root decoy"
252+
assert len(report.results) == 1
253+
254+
def test_the_folder_runner_passes_the_relative_path(self, workspace):
255+
"""End-to-end through execute_notebooks_in_folder, which computes it."""
256+
from build_util import execute_notebooks_in_folder
257+
258+
_write_notebook(
259+
workspace / "notebooks" / "imaging" / "model_fit.ipynb",
260+
"raise RuntimeError('stale')",
261+
)
262+
(workspace / "scripts" / "imaging").mkdir(parents=True)
263+
(workspace / "scripts" / "imaging" / "model_fit.py").write_text("x = 1 + 1\n")
264+
(workspace / "scripts" / "model_fit.py").write_text("raise RuntimeError('wrong source')\n")
265+
report = _report()
266+
267+
execute_notebooks_in_folder(
268+
directory="notebooks",
269+
no_run_list=[],
270+
report=report,
271+
write_back=False,
272+
retry_from_scripts=workspace / "scripts",
273+
)
274+
275+
assert len(report.results) == 1
276+
assert report.results[0].status == Status.PASSED

0 commit comments

Comments
 (0)