Files
stefan-jansen--machine-lear…/tests/add_missing_parameters_cells.py
2026-07-13 13:26:28 +08:00

119 lines
3.4 KiB
Python

"""Add missing Papermill parameters cells to notebooks.
Notebooks that already have a `# %% tags=["parameters"]` cell are skipped.
For notebooks without one, this inserts an empty parameters cell after the
first code cell (or after imports if detectable).
Usage:
uv run python tests/add_missing_parameters_cells.py [--dry-run]
"""
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).parent.parent
DRY_RUN = "--dry-run" in sys.argv
# Parameters cell template
PARAMS_CELL = """
# %% tags=["parameters"]
# Production defaults — Papermill injects overrides for CI
"""
def find_notebooks() -> list[Path]:
"""Find all Jupytext notebooks missing a parameters cell."""
notebooks = []
for d in sorted(REPO_ROOT.glob("[0-9]*_*")):
notebooks.extend(sorted(d.glob("**/*.py")))
for d in sorted((REPO_ROOT / "case_studies").glob("*")):
if d.is_dir() and d.name not in ("utils", "__pycache__"):
notebooks.extend(sorted(d.glob("**/*.py")))
missing = []
for nb in notebooks:
content = nb.read_text()
if "# %%" not in content:
continue
if 'tags=["parameters"]' in content or "tags=['parameters']" in content:
continue
missing.append(nb)
return missing
def add_parameters_cell(nb_path: Path) -> bool:
"""Insert a parameters cell after the first code cell.
Strategy: find the first `# %%` code cell (not markdown, not the header)
and insert the parameters cell after it. If the first code cell has imports,
the parameters cell goes after the import block.
"""
content = nb_path.read_text()
lines = content.split("\n")
# Find insertion point: after the first code cell
# Skip: header (---/jupyter metadata), markdown cells
in_header = False
found_first_code = False
insert_after = None
for i, line in enumerate(lines):
stripped = line.strip()
# Track Jupytext header
if i == 0 and stripped == "# ---":
in_header = True
continue
if in_header:
if stripped == "# ---":
in_header = False
continue
# Found a code cell
if stripped == "# %%" and not found_first_code:
found_first_code = True
# Look ahead to find end of this cell (next # %% or end)
for j in range(i + 1, len(lines)):
if lines[j].strip().startswith("# %%"):
insert_after = j
break
else:
insert_after = len(lines)
break
if insert_after is None:
return False
# Insert the parameters cell
new_lines = (
lines[:insert_after] + PARAMS_CELL.rstrip().split("\n") + [""] + lines[insert_after:]
)
new_content = "\n".join(new_lines)
if not DRY_RUN:
nb_path.write_text(new_content)
return True
def main():
missing = find_notebooks()
print(f"Found {len(missing)} notebooks missing parameters cell")
if DRY_RUN:
print("(dry run — no changes)")
modified = 0
for nb in missing:
rel = nb.relative_to(REPO_ROOT)
ok = add_parameters_cell(nb)
status = "ADDED" if ok else "SKIPPED"
if ok:
modified += 1
print(f" {status}: {rel}")
print(f"\nModified: {modified}/{len(missing)}")
if __name__ == "__main__":
main()