-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathcheck_render_order.py
More file actions
110 lines (90 loc) · 4.8 KB
/
Copy pathcheck_render_order.py
File metadata and controls
110 lines (90 loc) · 4.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""Fail the docs build when API rendering depends on page processing order.
Zensical discovers pages with an unsorted directory walk and renders them
all through one shared mkdocstrings handler, so the griffe collection that
resolves `::: module` blocks accumulates in filesystem-dependent order, and
nothing in the toolchain checks that the order is safe: a cross-package
re-export that only resolved when its target package had been collected
earlier built fine on one machine and died with `AliasResolutionError` on
another (a GitHub runner-image update reshuffled readdir order and broke
every CI docs build this way). `load_external_modules: true` in `mkdocs.yml`
makes resolution order-independent; this check enforces that property,
because a regular build only ever exercises one arbitrary order.
mkdocstrings applies module loading and per-page options only on the first
collect of a package (later pages find the package already collected), so
each package under `docs/api/` gets the two hostile sides of that
asymmetry, each from a fresh handler with an empty collection:
- its subpages first and its package index last, so pages rendering
cross-package re-exports (the index above all) come after a plain
subpage has already collected the package and nothing they declare
themselves can still affect collection;
- its package index alone, so the page with the most re-exports is itself
the first collect over an empty collection.
Only the package's own pages are rendered: resolving `mcp` re-exports
without ever rendering an `mcp_types` page is exactly the property under
test.
Usage:
python scripts/docs/check_render_order.py [--config mkdocs.gen.yml]
Run after `build_config.py` has produced the config and the `docs/api/`
tree.
"""
from __future__ import annotations
import argparse
import traceback
from pathlib import Path
# Sibling modules, same direct-invocation pattern as build_config.py:
# gen_ref_pages owns the docs/api layout, llms_txt owns the page-URL mapping.
import gen_ref_pages
from llms_txt import page_url
from zensical.compat import mkdocstrings as zensical_mkdocstrings
from zensical.config import parse_config
from zensical.markdown.render import render
API_DIR = gen_ref_pages.API_DIR
DOCS_DIR = API_DIR.parent
def _passes(package: str, pages: list[Path]) -> list[tuple[str, list[Path]]]:
"""The labeled render orders exercising both sides of the first-collect asymmetry."""
index = API_DIR / package / "index.md"
if index not in pages:
return [(f"'{package}'", pages)]
subpages = [page for page in pages if page != index]
if not subpages:
return [(f"'{package}' index-alone", [index])]
return [(f"'{package}' index-last", [*subpages, index]), (f"'{package}' index-alone", [index])]
def _render(page: Path) -> None:
"""Render one page the way Zensical's Rust core drives the Python side."""
rel = page.relative_to(DOCS_DIR).as_posix()
render(page.read_text(encoding="utf-8"), rel, page_url(rel))
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--config", default=str(gen_ref_pages.ROOT / "mkdocs.gen.yml"), help="Built config to render with"
)
args = parser.parse_args()
parse_config(args.config)
packages: dict[str, list[Path]] = {}
for page in sorted(API_DIR.rglob("*.md")):
packages.setdefault(page.relative_to(API_DIR).parts[0], []).append(page)
if not packages:
raise SystemExit(f"check_render_order: no pages under {API_DIR} (run build_config.py first)")
for package in sorted(packages):
for label, order in _passes(package, packages[package]):
# Fresh Handlers -> empty griffe collection. Autorefs anchors
# accumulate across passes, but they play no part in collection
# or alias resolution (check_crossrefs owns link health).
zensical_mkdocstrings.reset()
for position, page in enumerate(order):
try:
_render(page)
# Top-level handler: any exception from any page fails the
# check; the traceback identifies whether the order was at
# fault or something else broke (network, missing file).
except Exception:
traceback.print_exc()
rel = page.relative_to(DOCS_DIR).as_posix()
raise SystemExit(
f"check_render_order: {rel} failed at position {position + 1}/{len(order)} of the"
f" {label} order (traceback above; an AliasResolutionError means API rendering"
" depends on page order — see `load_external_modules` in mkdocs.yml)"
) from None
print(f"check_render_order: {label} order OK ({len(order)} pages)")
if __name__ == "__main__":
main()