Setting up a project#
How to point Sphinx at a model directory and get a reference page — every struct, enum, and union in it, with a tab per generated language — out of it.
Install#
The extension ships with model, so the package is all you need at runtime.
Sphinx itself is optional — nothing else in model imports model.sphinx.
pip install model sphinx sphinx-design sphinxcontrib-godomain
sphinx-design and sphinxcontrib-godomain are not optional. automodel
emits a tab-set for the per-language output, and the Go tab uses the go
domain; without either one the build fails on an unknown directive.
Minimal conf.py#
project = "my-project"
extensions = [
"sphinx_design",
"sphinxcontrib.godomain",
"model.sphinx",
]
Document a model directory#
Add the directive to any page, with the model directory relative to that page (or absolute from the source root, as Sphinx paths are):
Reference
=========
.. automodel:: ../my-model
Markdown pages take the directive natively — the generated content is parsed
with its own reStructuredText parser either way, so no eval-rst wrapper is
needed:
# Reference
```{automodel} ../my-model
```
Every type in the directory is documented, in dependency order, with each struct’s fields, each enum’s values, and each union’s variants, plus the Python, TypeScript, and Go declarations the generator emits for it.
The page rebuilds whenever a .yaml in the directory changes, and when one
is added or removed.
Cross-references#
The extension registers a model domain. Each type and member is a real
object, so anything can link to it:
:model:struct:`Example`
:model:enum:`Sample`
:model:union:`Shape`
:model:field:`Example.money`
:model:value:`Sample.foo`
:model:variant:`Shape.Circle`
:model:type: reaches any of the three top-level kinds, which is what a
model field or a union variant’s inner_type names. :model:obj:
reaches those and members too — set it as the default role if the site is
mostly about the model, and a bare `Example` links to it:
default_role = "model:obj"
The generated declarations get their own objects in their own domains:
:py:class:`Example` , :js:class:`Example` , and
:go:struct:`Example` . Model
objects claim the clean anchor (#Example, #Example.money), so those
three fall back to generated ids.
Markdown descriptions#
A desc: in the model YAML is reStructuredText by default. To write them in
Markdown instead, name a parser in the model directory’s model.toml:
[tool.model]
docstring-parser-regexes = [
["Example.*", "myst"],
[".*", "rst"],
]
Patterns are matched against an object’s full name — Example,
Example.money, Sample.foo — and the first match wins. A name no pattern
matches stays reStructuredText, so an empty list changes nothing. rst
listed ahead of a catch-all carves one name back out of it.
myst needs myst_parser in extensions; the build says so if it is
missing.
model.toml#
automodel reads the nearest model.toml at or above the model directory,
stopping at the repository root — the same file the model CLI reads. Two
keys reach the docs:
[tool.model.go] pascal-case— what the Go generator would emit, so the Go tab spells identifiers the way the generator will.docstring-parser-regexes— above.
Putting them here rather than in conf.py keeps the docs and the CLI from
disagreeing about the same model.
Linking generated annotations#
The declarations in the tabs name types this site does not own —
decimal.Decimal, time.Time, Map. Nothing links unless an inventory
covers them, which only matters with nitpicky = True. docs/conf.py in
this repository is the worked example: it vendors inventories for CPython,
MDN, third-party Go modules, and decimal.js, and raises a ConfigError when
one has gone stale rather than emitting a pile of unresolved-reference
warnings.
Two helpers back that check, both importable from model.sphinx:
go_modules()— the third-party Go modules the field types import. Each needs an inventory. The standard library needs none:sphinxcontrib.godomaingenerates and registers it from the installed toolchain.npm_packages()— the npm packages the field types import from. JavaScript has noobjects.inv, so each needs whatever one-off scraper its docs allow.
Skip all of this if the build is not nitpicky.
Preview#
sphinx-build docs docs/_build/html
Or with live reload, which re-runs sphinx-build in a fresh subprocess per
change, so edits to a model YAML and to model/sphinx.py both take
effect:
sphinx-autobuild docs docs/_build/html --watch ../my-model
In this repository that is nix run .#docs.