Skip to content

Commit 37706fe

Browse files
maxrjonesd-v-b
andauthored
docs: show package tag instead of monorepo release in subpackage docs (#4229)
* docs: show package tag instead of monorepo release in subpackage doc headers * refactor: DRY mkdocs configuration * perf: cache tag * fix(docs): ship real files in package sdists; build sites on shared-file changes The docs/_static/source-version.js symlink points outside the package, so each sdist carried a dangling link and pip refused to unpack it ("would link to ... outside the destination"). mkdocs.yml's INHERIT of ../mkdocs-base.yml likewise broke `mkdocs build` from an unpacked sdist. Link mkdocs-base.yml into each package too, INHERIT it from the package directory, and have the sdists exclude both links and force-include the files they point at. The sdist is self-contained again. Package CI and the RTD PR-build skip only watched packages/<name>, so an edit to the shared base or script built no site. Add both shared files to every package workflow's paths and to each .readthedocs.yaml diff. Assisted-by: ClaudeCode:claude-opus-5-5 * build(docs): align sub-package docs pins with the root docs environment The three sites share packages/mkdocs-base.yml but resolved different plugin versions (mkdocstrings 1.0.4 vs 1.0.6, mkdocstrings-python 2.0.5, ruff 0.15.20 / 0.15.22 / 0.16.0), and two packages' comments claimed to match the root docs group while not doing so. Pin all three to the root's versions (mkdocstrings 1.0.6, mkdocstrings-python 2.0.8, ruff 0.16.8) and relock zarr-indexing and zarr-http-server. Assisted-by: ClaudeCode:claude-opus-5-5 --------- Co-authored-by: Davis Bennett <[email protected]>
1 parent d7686f6 commit 37706fe

24 files changed

Lines changed: 378 additions & 375 deletions

‎.github/workflows/zarr-http-server.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ on:
1111
paths:
1212
- 'packages/zarr-http-server/**'
1313
- '.github/workflows/zarr-http-server.yml'
14+
# The docs config and header script shared by all package sites.
15+
- 'packages/mkdocs-base.yml'
16+
- 'packages/source-version.js'
1417
# The package resolves zarr from the repo root for its own tests, so a
1518
# core change can break it. Run this suite when core changes too.
1619
- 'src/zarr/**'
1720
pull_request:
1821
paths:
1922
- 'packages/zarr-http-server/**'
2023
- '.github/workflows/zarr-http-server.yml'
24+
# The docs config and header script shared by all package sites.
25+
- 'packages/mkdocs-base.yml'
26+
- 'packages/source-version.js'
2127
# The package resolves zarr from the repo root for its own tests, so a
2228
# core change can break it. Run this suite when core changes too.
2329
- 'src/zarr/**'

‎.github/workflows/zarr-indexing.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ on:
66
paths:
77
- 'packages/zarr-indexing/**'
88
- '.github/workflows/zarr-indexing.yml'
9+
# The docs config and header script shared by all package sites.
10+
- 'packages/mkdocs-base.yml'
11+
- 'packages/source-version.js'
912
pull_request:
1013
paths:
1114
- 'packages/zarr-indexing/**'
1215
- '.github/workflows/zarr-indexing.yml'
16+
# The docs config and header script shared by all package sites.
17+
- 'packages/mkdocs-base.yml'
18+
- 'packages/source-version.js'
1319
workflow_dispatch:
1420

1521
permissions:

‎.github/workflows/zarr-metadata.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ on:
99
paths:
1010
- 'packages/zarr-metadata/**'
1111
- '.github/workflows/zarr-metadata.yml'
12+
# The docs config and header script shared by all package sites.
13+
- 'packages/mkdocs-base.yml'
14+
- 'packages/source-version.js'
1215
pull_request:
1316
paths:
1417
- 'packages/zarr-metadata/**'
1518
- '.github/workflows/zarr-metadata.yml'
19+
# The docs config and header script shared by all package sites.
20+
- 'packages/mkdocs-base.yml'
21+
- 'packages/source-version.js'
1622
workflow_dispatch:
1723

1824
permissions:

‎docs/contributing.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,26 @@ The resulting built documentation will be available in the `site` folder.
246246
just docs-serve
247247
```
248248

249+
#### Sub-package documentation sites
250+
251+
Each sub-package under `packages/` ships its own Material for MkDocs site, built from within the package directory:
252+
253+
```bash
254+
cd packages/zarr-metadata
255+
uv run --group docs mkdocs build --strict
256+
```
257+
258+
Configuration shared by all package sites (theme, plugins, markdown extensions) lives in `packages/mkdocs-base.yml`, symlinked into each package directory; each package's `mkdocs.yml` pulls it in via `INHERIT` and defines only the site identity (name, description, repo and site URLs) and nav. `INHERIT` merges mappings but replaces lists, so a package that sets `theme.features` or `markdown_extensions` silently drops every shared entry; add extensions and features to the base instead. The header source widget's version fact is overridden by `packages/source-version.js`, symlinked into each package's `docs/_static/`, which replaces the monorepo's latest release with the latest tag matching the package's `zarr_<name>-v` prefix (both derived from `repo_url` at runtime, so the file needs no per-package edits). On Windows, enable symlinks in git (`git config core.symlinks true`, with Developer Mode on) before cloning, or local builds will ship a file containing the link target instead of the script.
259+
260+
To add a site for a new sub-package, copy `mkdocs.yml`, the `docs/` folder, `.readthedocs.yaml`, and the `docs` dependency group in `pyproject.toml` from an existing package, then adjust the site name, description, URLs, and nav. Recreate the `mkdocs-base.yml` and `docs/_static/source-version.js` symlinks rather than copying the files, and copy the sdist `exclude` and `force-include` entries from an existing package's `pyproject.toml`, which ship the linked files in place of the links. Add `packages/mkdocs-base.yml` and `packages/source-version.js` to the package workflow's `paths` filters and to the `git diff` in its `.readthedocs.yaml`, so changes to the shared files build the site.
261+
262+
Each site is hosted as its own Read the Docs project. To set one up for a new sub-package:
263+
264+
1. Create a new project on [readthedocs.org](https://app.readthedocs.org) importing the `zarr-python` repository, named after the package (e.g. `zarr-metadata`).
265+
2. In the project's admin settings, set the configuration file path to `packages/<name>/.readthedocs.yaml`. That file also cancels pull request builds that don't touch the package.
266+
3. Add an automation rule matching the package's release tags (custom match `^zarr_<name>-v`, note the underscore) with the action "Activate version". Automation rules only apply to versions detected after the rule is created, so activate any earlier release tags manually from the versions list.
267+
4. When activating a version, edit its slug to the bare version number (`0.4.0`, not `v0.4.0`). RTD keeps a version record for every tag in the monorepo, including inactive ones not shown in the dashboard's versions list, and all of zarr-python's own release tags are `v`-prefixed — so a `v`-prefixed slug collides with zarr-python's release history ("A version with that slug already exists"), while bare version numbers cannot collide.
268+
249269
#### Adding executable code blocks in the documentation
250270

251271
Zarr uses [Markdown Exec](https://pawamoy.github.io/markdown-exec/usage/) to execute code blocks in Markdown files. Add `exec="true"` to a code block header for it to be executed when the docs are built. For example:

‎packages/mkdocs-base.yml‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Shared Material for MkDocs configuration for the sub-package docs sites.
2+
# Each package's mkdocs.yml pulls this in via INHERIT and defines only its
3+
# identity (site_name, repo_*, site_description, site_url) and nav.
4+
#
5+
# INHERIT deep-merges mappings but *replaces* lists: a package that sets
6+
# theme.features or markdown_extensions drops every entry below. So the lists
7+
# here are the superset used by all packages (features, extensions, snippet
8+
# base paths, and inventories a package never exercises are harmless); add to
9+
# them here rather than in a package's mkdocs.yml.
10+
site_author: Davis Bennett
11+
use_directory_urls: true
12+
13+
watch:
14+
- src
15+
16+
theme:
17+
language: en
18+
name: material
19+
logo: _static/logo_bw.png
20+
favicon: _static/favicon-96x96.png
21+
22+
palette:
23+
# Light mode
24+
- media: "(prefers-color-scheme: light)"
25+
scheme: default
26+
toggle:
27+
icon: material/brightness-7
28+
name: Switch to dark mode
29+
30+
# Dark mode
31+
- media: "(prefers-color-scheme: dark)"
32+
scheme: slate
33+
toggle:
34+
icon: material/brightness-4
35+
name: Switch to light mode
36+
37+
font:
38+
text: Roboto
39+
code: Roboto Mono
40+
41+
features:
42+
- content.code.annotate
43+
- content.code.copy
44+
- content.tabs.link
45+
- navigation.indexes
46+
- navigation.instant
47+
- navigation.tracking
48+
- search.suggest
49+
- search.share
50+
51+
plugins:
52+
- autorefs
53+
- search
54+
- mkdocstrings:
55+
enable_inventory: true
56+
handlers:
57+
python:
58+
paths: [src]
59+
options:
60+
allow_inspection: true
61+
docstring_section_style: list
62+
docstring_style: numpy
63+
inherited_members: true
64+
line_length: 60
65+
separate_signature: true
66+
show_root_heading: true
67+
show_signature_annotations: true
68+
show_source: true
69+
show_symbol_type_toc: true
70+
signature_crossrefs: true
71+
show_if_no_docstring: true
72+
extensions:
73+
- griffe_inherited_docstrings
74+
75+
inventories:
76+
- https://docs.python.org/3/objects.inv
77+
- https://numpy.org/doc/stable/objects.inv
78+
- https://zarr.readthedocs.io/en/stable/objects.inv
79+
80+
# Override the header source widget's version fact (the monorepo's latest
81+
# release) with the latest tag matching the package's prefix.
82+
extra_javascript:
83+
- _static/source-version.js
84+
85+
markdown_extensions:
86+
- admonition
87+
- attr_list
88+
- def_list
89+
- footnotes
90+
- md_in_html
91+
- pymdownx.details
92+
- pymdownx.superfences
93+
- toc:
94+
permalink: true
95+
- pymdownx.highlight:
96+
anchor_linenums: true
97+
line_spans: __span
98+
pygments_lang_class: true
99+
- pymdownx.inlinehilite
100+
# Content tabs; content.tabs.link in the theme features keeps tab sets
101+
# switched together.
102+
- pymdownx.tabbed:
103+
alternate_style: true
104+
- pymdownx.snippets:
105+
# The package root is last so docs/release-notes.md can include the
106+
# towncrier-managed CHANGELOG.md without moving it out of the package
107+
# root, where it is distributed and linked from the package metadata.
108+
# examples/ holds runnable example scripts (zarr-indexing); a missing
109+
# base path is skipped.
110+
base_path: [docs, examples, .]
111+
# Fail the build on an unresolvable include or missing region instead
112+
# of silently rendering nothing. zarr-indexing's
113+
# tests/test_doc_examples.py mirrors base_path.
114+
check_paths: true

‎packages/source-version.js‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/* Material's header source widget shows the monorepo's latest release
2+
(e.g. v3.3.0). Replace it with the latest tag for this package, and
3+
patch the theme's sessionStorage cache so instant navigation keeps it.
4+
5+
The GitHub repo and the package's tag prefix (e.g. zarr_metadata-) are
6+
derived from repo_url, whose final path segment is the package directory,
7+
so this file is identical across all sub-package docs sites. */
8+
(() => {
9+
const source = document.querySelector('[data-md-component="source"]')
10+
if (!source || !source.href)
11+
return
12+
const segments = new URL(source.href).pathname.split("/").filter(Boolean)
13+
const REPO = segments.slice(0, 2).join("/")
14+
const PREFIX = segments[segments.length - 1].replace(/-/g, "_") + "-"
15+
16+
/* Final releases only (v1.2.3), matching the theme's own releases/latest
17+
lookup; pre-release parts like 0rc1 would otherwise parse as 0 and tie
18+
with (or outrank) the final release. */
19+
const RELEASE = /^v?\d+(\.\d+)*$/
20+
21+
const parse = version => version
22+
.replace(/^v/, "")
23+
.split(".")
24+
.map(part => parseInt(part, 10))
25+
26+
const newestFirst = (a, b) => {
27+
const va = parse(a), vb = parse(b)
28+
for (let i = 0; i < Math.max(va.length, vb.length); i++)
29+
if ((vb[i] || 0) !== (va[i] || 0))
30+
return (vb[i] || 0) - (va[i] || 0)
31+
return 0
32+
}
33+
34+
/* Cache the tag for the tab's lifetime (like the theme's own __source
35+
cache) so navigation doesn't burn GitHub's unauthenticated API rate
36+
limit. Keyed by prefix in case sub-package sites share an origin. */
37+
const CACHE_KEY = `__package_tag/${PREFIX}`
38+
39+
async function latestPackageTag() {
40+
const cached = sessionStorage.getItem(CACHE_KEY)
41+
if (cached)
42+
return cached
43+
const response = await fetch(
44+
`https://api.github.com/repos/${REPO}/tags?per_page=100`
45+
)
46+
if (!response.ok)
47+
return undefined
48+
const tags = await response.json()
49+
const versions = tags
50+
.map(tag => tag.name)
51+
.filter(name => name.startsWith(PREFIX))
52+
.map(name => name.slice(PREFIX.length))
53+
.filter(version => RELEASE.test(version))
54+
.sort(newestFirst)
55+
if (versions[0])
56+
sessionStorage.setItem(CACHE_KEY, versions[0])
57+
return versions[0]
58+
}
59+
60+
function patchDom(version) {
61+
for (const el of document.querySelectorAll(".md-source__fact--version"))
62+
el.textContent = version
63+
}
64+
65+
function patchCache(version) {
66+
const facts = __md_get("__source", sessionStorage)
67+
if (!facts)
68+
return false
69+
facts.version = version
70+
__md_set("__source", facts, sessionStorage)
71+
return true
72+
}
73+
74+
latestPackageTag().then(version => {
75+
if (!version)
76+
return
77+
patchDom(version)
78+
if (patchCache(version))
79+
return
80+
/* The theme's own API request hasn't resolved yet — patch both the
81+
cache and the DOM once it renders the facts list. */
82+
new MutationObserver((_, observer) => {
83+
if (patchCache(version)) {
84+
patchDom(version)
85+
observer.disconnect()
86+
}
87+
}).observe(source, { childList: true, subtree: true })
88+
})
89+
})()

‎packages/zarr-http-server/.readthedocs.yaml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ build:
1313
# Cancel pull request builds that do not touch this package. Exit code
1414
# 183 cancels the build and reports success to the Git provider. Scoped
1515
# to PR builds ("external" versions) because origin/main is only a
16-
# meaningful diff base there.
16+
# meaningful diff base there. The shared docs config and header script
17+
# live outside the package directory but change this site too.
1718
- |
18-
if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- packages/zarr-http-server;
19+
if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- packages/zarr-http-server packages/mkdocs-base.yml packages/source-version.js;
1920
then
2021
exit 183;
2122
fi
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../source-version.js
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../mkdocs-base.yml

0 commit comments

Comments
 (0)