Skip to content

Commit 1baa5e9

Browse files
romanlutzCopilot
andauthored
FIX: Sync docs TOC with generated API pages + fail-fast TOC validation (#1793)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8003212 commit 1baa5e9

4 files changed

Lines changed: 66 additions & 8 deletions

File tree

build_scripts/gen_api_md.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717
"""
1818

1919
import json
20+
import sys
2021
from pathlib import Path
2122

23+
# Import sibling script for post-generation TOC validation.
24+
sys.path.insert(0, str(Path(__file__).parent))
25+
import validate_docs # noqa: E402
26+
2227
API_JSON_DIR = Path("doc/_api")
2328
API_MD_DIR = Path("doc/api")
2429

@@ -399,6 +404,15 @@ def main() -> None:
399404
index_path.write_text("\n".join(index_parts), encoding="utf-8")
400405
print(f"Written {index_path}")
401406

407+
# Fail loudly if doc/myst.yml's api/ TOC entries no longer match what we
408+
# generated. Without this check, mismatches only manifest as easy-to-miss
409+
# warnings in the jupyter-book log (--strict does not treat them as errors)
410+
# and silently break the Read the Docs build downstream.
411+
print("Validating doc/myst.yml stays in sync with generated API pages...")
412+
rc = validate_docs.main()
413+
if rc != 0:
414+
sys.exit(rc)
415+
402416

403417
if __name__ == "__main__":
404418
main()

build_scripts/validate_docs.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,21 @@ def parse_toc_files(toc_entries: list, files: set | None = None) -> set[str]:
3333

3434

3535
def validate_toc_files(toc_files: set[str], doc_root: Path) -> list[str]:
36-
"""Check that all files referenced in the TOC exist."""
37-
# Directories with auto-generated content (gitignored, created during build)
38-
generated_dirs = {"api/", "api\\"}
36+
"""Check that all files referenced in the TOC exist.
37+
38+
Auto-generated ``api/*.md`` pages are produced by
39+
``build_scripts/gen_api_md.py`` and are gitignored, so they are skipped
40+
while the ``doc/api/`` directory has not been generated yet (e.g. during
41+
pre-commit). Once that directory exists (i.e. after a docs build), the
42+
api/ entries are validated like any other file so the TOC stays in sync
43+
with the generator output.
44+
"""
45+
skip_generated_api = not (doc_root / "api").exists()
46+
api_prefixes = ("api/", "api\\")
3947

4048
errors = []
4149
for file_ref in toc_files:
42-
# Skip files in auto-generated directories
43-
if any(file_ref.startswith(d) for d in generated_dirs):
50+
if skip_generated_api and file_ref.startswith(api_prefixes):
4451
continue
4552
file_path = doc_root / file_ref
4653
if not file_path.exists():
@@ -49,17 +56,24 @@ def validate_toc_files(toc_files: set[str], doc_root: Path) -> list[str]:
4956

5057

5158
def find_orphaned_files(toc_files: set[str], doc_root: Path) -> list[str]:
52-
"""Find documentation files not referenced in the TOC."""
59+
"""Find documentation files not referenced in the TOC.
60+
61+
``doc/api/`` holds auto-generated reference pages. They are skipped while
62+
the directory does not yet exist (pre-commit, before any docs build), but
63+
once present they are checked for orphans so the TOC reflects exactly the
64+
set of files produced by ``build_scripts/gen_api_md.py``.
65+
"""
5366
skip_dirs = {
5467
"_build",
5568
"_api",
56-
"api",
5769
"css",
5870
".ipynb_checkpoints",
5971
"__pycache__",
6072
"playwright_demo",
6173
"generate_docs",
6274
}
75+
if not (doc_root / "api").exists():
76+
skip_dirs.add("api")
6377
skip_files = {
6478
"myst.yml",
6579
"roakey.png",

doc/myst.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ project:
208208
- file: api/pyrit_cli_pyrit_scan.md
209209
- file: api/pyrit_cli_pyrit_shell.md
210210
- file: api/pyrit_common.md
211-
- file: api/pyrit_common_cli_helpers.md
212211
- file: api/pyrit_datasets.md
213212
- file: api/pyrit_embedding.md
214213
- file: api/pyrit_exceptions.md
@@ -221,6 +220,7 @@ project:
221220
- file: api/pyrit_memory.md
222221
- file: api/pyrit_message_normalizer.md
223222
- file: api/pyrit_models.md
223+
- file: api/pyrit_output.md
224224
- file: api/pyrit_prompt_converter.md
225225
- file: api/pyrit_prompt_normalizer.md
226226
- file: api/pyrit_prompt_target.md

tests/unit/build_scripts/test_validate_docs.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,25 @@ def test_validate_toc_files_error_when_file_missing(tmp_path: Path) -> None:
4949

5050

5151
def test_validate_toc_files_skips_api_generated_files(tmp_path: Path) -> None:
52+
# When doc/api/ does not exist (e.g. pre-commit before any docs build),
53+
# api/* TOC entries are skipped because they will be generated later.
5254
errors = validate_toc_files({"api/some_module"}, tmp_path)
5355
assert errors == []
5456

5557

58+
def test_validate_toc_files_validates_api_entries_when_api_dir_exists(tmp_path: Path) -> None:
59+
# Once doc/api/ exists (post gen_api_md.py), api/* TOC entries are
60+
# validated like any other file so stale entries are caught.
61+
api_dir = tmp_path / "api"
62+
api_dir.mkdir()
63+
(api_dir / "pyrit_existing.md").write_text("# existing")
64+
65+
errors = validate_toc_files({"api/pyrit_existing.md", "api/pyrit_missing.md"}, tmp_path)
66+
67+
assert len(errors) == 1
68+
assert "pyrit_missing.md" in errors[0]
69+
70+
5671
def test_validate_toc_files_multiple_missing_files(tmp_path: Path) -> None:
5772
errors = validate_toc_files({"a.md", "b.md"}, tmp_path)
5873
assert len(errors) == 2
@@ -89,3 +104,18 @@ def test_find_orphaned_files_skips_py_companion_files(tmp_path: Path) -> None:
89104
(tmp_path / "notebook.py").write_text("# companion")
90105
orphaned = find_orphaned_files(set(), tmp_path)
91106
assert not any("notebook.py" in o for o in orphaned)
107+
108+
109+
def test_find_orphaned_files_detects_orphaned_api_pages_when_dir_exists(tmp_path: Path) -> None:
110+
# Post-build: doc/api/ exists. Any generated page that isn't in the TOC
111+
# is flagged so stale or unlisted modules surface immediately instead of
112+
# showing up later as a Read the Docs build failure.
113+
api_dir = tmp_path / "api"
114+
api_dir.mkdir()
115+
(api_dir / "pyrit_listed.md").write_text("# listed")
116+
(api_dir / "pyrit_orphan.md").write_text("# orphan")
117+
118+
orphaned = find_orphaned_files({"api/pyrit_listed.md"}, tmp_path)
119+
120+
assert any("pyrit_orphan.md" in o for o in orphaned)
121+
assert not any("pyrit_listed.md" in o for o in orphaned)

0 commit comments

Comments
 (0)