Skip to content

refactor(template): extract the docs build steps out of hooks.py - #185

Merged
gtauzin merged 1 commit into
mainfrom
refactor/extract-docs-build-steps
Jul 20, 2026
Merged

gtauzin merged 1 commit into
mainfrom
refactor/extract-docs-build-steps

Conversation

@gtauzin

@gtauzin gtauzin commented Jul 20, 2026

Copy link
Copy Markdown
Member

Why

docs/hooks.py was 2678 lines behind five MkDocs event hooks. Two of them did work that has nothing to do with rendering a page:

  • on_pre_build reads src/, writes docs/pages/api/*.md, exports the marimo notebooks — and ignores its config argument entirely
  • on_post_build reads site/, writes markdown back into it — using config for two directory strings

Neither imports a MkDocs type. They are file-in/file-out programs wearing a hook as a costume, and the costume cost real things: the only way to run either was a full mkdocs build, so they lived in the slowest test tier for logic needing no theme, no server and no markdown; the generated .md was an intermediate nobody could produce on demand; and the discovery layer was reinvented independently by four generated repos before the template consolidated one.

What changed

~1000 lines moved into three modules that import nothing from mkdocs:

module contents
docs/_api_pages.py discovery layer + API page generation
docs/_notebooks.py marimo export + content-hash cache (examples only)
docs/_markdown_export.py HTML→markdown conversion + llms.txt

Each has a main() and runs standalone. New nox -s build_steps runs them without a docs build.

The hooks stay as adapters. Deleting them would break the documented preview workflow — mkdocs.yml watches src so adding a class appears without restarting mkdocs serve, and that only works because on_pre_build regenerates on every rebuild.

Verification

Byte-identity. The generated API pages are identical before and after, under both include_examples values. The one site file that differs is proven nondeterministic by a control (two builds of the same code differ — marimo embeds a per-export UUID).

A real leak fixed. mkdocs treats every non-page file under docs_dir as a static asset, so it was publishing the build tooling's source and bytecode to the docs site. Already true of hooks.py; splitting it made it four modules. exclude_docs now covers *.py and __pycache__/.

Cache guarantee kept. Two caches moved with their functions, so on_config calls _api_pages.reset_caches() — a global statement cannot rebind another module's globals. The registration test was scanning only the hooks module, so it had silently narrowed to "every cache that stayed behind" while still passing; it now scans every module the hooks load, and is mutation-tested (removing the reset fails it, naming both caches).

Checked against a live server. Adding a class to src/ and retitling a notebook's __gallery__ both appear without a restart.

  • 333 fast tests pass · 9 slow tests pass (4 pre-existing skips) · prek clean
  • mkdocs build --strict clean on both variants

Lint config narrowed

docs/**/* split into two entries. The hooks keep their unused-argument exemptions (they implement mkdocs' signatures); the extracted steps get only T201/PLW0603 — no blanket ARG exemption, because they implement no imposed signature. That immediately caught a genuinely unused parameter with examples disabled.

Not done here (deliberately)

  • The print("[hooks] …") prefix still says [hooks] from _api_pages — a rename, excluded from a move
  • docs_examples could use docs_dir / "examples" instead of project_root / "docs" / "examples" — more correct, but a behaviour change
  • Pre-existing bug found, not fixed: editing a source file under mkdocs serve triggers a rebuild loop (_generate_api_pages writes into a watched path). Symmetric control: baseline +18 rebuilds, this branch +19 — unchanged by this PR

Downstream

Fan-out to the seven generated repos follows release. hooks.py is Tier 1, but Dynamic Reclassification promotes a locally-edited Tier 1 file to Tier 2, so copier update merges rather than overwrites — a one-file-into-four split against a fork is not a case the tiering was designed for. Establish each repo's actual fork status first (render the template with that repo's .copier-answers.yml and diff; cross-repo md5 comparison proves nothing, since the template interpolates {{ package_name }} 20 times).

`docs/hooks.py` was 2678 lines behind five MkDocs event hooks. Two of them
did work that has nothing to do with rendering a page: `on_pre_build` read
`src/` and wrote `docs/pages/api/*.md`, then exported the marimo notebooks;
`on_post_build` read `site/` and wrote markdown back into it. Neither
imported a MkDocs type -- `on_pre_build` ignored its `config` argument
entirely -- yet the only way to run either was a full `mkdocs build`. That
put file-in/file-out logic in the slowest test tier the template has, made
the generated `.md` an intermediate nobody could produce on demand, and left
a discovery layer that four generated repos each reinvented before the
template consolidated one.

Move ~1000 lines into three modules that import nothing from mkdocs:

  docs/_api_pages.py        discovery + API page generation
  docs/_notebooks.py        marimo export and its content-hash cache
  docs/_markdown_export.py  HTML->markdown conversion and llms.txt

The hooks stay as adapters. Deleting them would break the documented
preview workflow: `mkdocs.yml` watches `src` so that adding a class appears
without restarting `mkdocs serve`, and that only works because
`on_pre_build` regenerates on every rebuild.

`on_config` keeps resetting the per-build caches, but two of them moved with
the functions that own them, so it now calls `_api_pages.reset_caches()` --
a `global` statement cannot rebind another module's globals. The cache
registration test was scanning only the hooks module, so it had silently
narrowed to "every cache that stayed behind" while still passing; it now
scans every module the hooks load.

Verified as a move, not a rewrite: the generated API pages are byte-identical
under both `include_examples` values. The only site difference is deliberate
-- mkdocs treats every non-page file under `docs_dir` as a static asset, so
it was publishing the build tooling's source and bytecode to the
documentation site. That was already true of `hooks.py`; splitting it made it
four modules, so `exclude_docs` now covers `*.py` and `__pycache__/`.

Also checked by hand against a live server: adding a class to `src/` and
retitling a notebook's `__gallery__` both appear without a restart.

BREAKING CHANGE: `docs/` gains `_api_pages.py`, `_markdown_export.py` and,
with examples enabled, `_notebooks.py`. Projects that forked `docs/hooks.py`
will need to reconcile a one-file-into-four split rather than a line diff.
The site no longer publishes `docs/*.py` or `docs/__pycache__/`.
@gtauzin
gtauzin merged commit 8e53fcb into main Jul 20, 2026
14 checks passed
@gtauzin
gtauzin deleted the refactor/extract-docs-build-steps branch July 20, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant