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.rst#
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.md#
# 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.godomain generates and registers it from the installed toolchain.

  • npm_packages() — the npm packages the field types import from. JavaScript has no objects.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.