Python API reference
MkDocs product sites build their API reference with mkdocstrings, which has
no Starlight equivalent. helia-ui-pyref is the replacement, and it runs in two
steps. An extractor maps a griffe JSON
dump onto the reference model; a renderer turns that model into MDX pages that
compose the package’s RefSymbol, RefParams, RefSection and RefMembers
parts, into the JSON the model is, and into the two text artifacts an agent
reads instead of the pages.
The split is what keeps four languages looking like one reference. Only
scripts/lib/pyref-extract.mjs knows Python. A C, C++ or TypeScript extractor
produces the same model and gets the same pages without touching the renderer.
The generator never imports the package it documents. griffe loads by AST, so a package with hardware or compiler dependencies dumps on any machine, and the site build needs no Python at all.
The contract
Section titled “The contract”The JSON is the source of truth and the pages are a view of it.
That ordering decides everything else on this page. The model is written out whole and per module, with sorted keys and no timestamp, so it diffs. Every symbol keeps a stable id, and that id is the page anchor, so a URL plus a fragment names one symbol and keeps naming it after the reference is regenerated or restyled. A consumer that wants the data takes the JSON and never parses HTML.
The model is @ambiqai/helia-ui/reference-model: TypeScript types plus
validateReferenceModel, a hand-written guard with no dependencies. A site
that loads a reference.json validates it before rendering, and an extractor
that adds a field its own language happens to have fails that guard rather
than reaching the parts.
Running it
Section titled “Running it”# In the Python repository, with griffe installed:griffe dump helia_aot --docstyle google -f > griffe.json
# In the docs site:npx helia-ui-pyref \ --input ../griffe.json \ --out src/content/docs/reference/api \ --public public \ --base /helia-aot/ \ --sidebar src/generated/api-sidebar.jsonPages land at <out>/<module path>/index.mdx, so helia_aot.air.model becomes
reference/api/helia_aot/air/model/index.mdx and serves at
/helia-aot/reference/api/helia_aot/air/model/. That is the same shape the
MkDocs api/ tree had, so inbound links survive the move.
Every route segment is slugged the way Astro derives a content id from a file path, lowercased and with punctuation dropped, so the page a generator writes and the URL it advertises are one string; two module paths that slug to one route fail the render. The module path itself is unchanged as the module’s identity, title and cross-reference target. A module whose source carries no summary still gets a description on its page, worded mechanically per language, since a page without one fails the discoverability check.
| Option | Default | What it does |
|---|---|---|
| --input <file> | required | The griffe dump to read. |
| --out <dir> | required | Directory the module pages are written into. |
| --public <dir> | public | Directory the JSON and text artifacts are written into. |
| --base <path> | / | Site base path, prefixed onto every URL the pages and artifacts carry. |
| --site <origin> | not set | Origin for the absolute URLs in llms.txt. Without it they are site-root relative. |
| --package <name> | the only one | Which package to render when the dump holds several. |
| --sidebar <file> | not written | Write the Starlight sidebar fragment as JSON. |
| --source-root <dir> | not set | Strip this prefix from the source paths in the model. |
| --source-url <template> | not set | Link template for source, with {path} and {line}. |
| --commit <sha> | not set | Commit of the documented source, recorded in generatedFrom. |
| --check | off | Compare against what is on disk and exit 1 on drift. |
| --quiet | off | Report only the warning count, not each warning. |
| --route-prefix <path> | reference/api | Route the pages serve under. |
The nine mkdocstrings options the HELIA product sites set are the defaults,
so a run with no flags reproduces the pages those sites publish today. Each has
a flag that overrides it.
| mkdocstrings option | Default here | Flag |
|---|---|---|
| docstring_style | --docstring-style | |
| show_root_heading | false | --show-root-heading |
| heading_level | 2 | --heading-level |
| merge_init_into_class | true | --no-merge-init-into-class |
| members_order | source | --members-order |
| filters | !^_ and ^__init__$ | --filter (repeatable) |
| separate_signature | true | --no-separate-signature |
| show_signature | true | --no-show-signature |
| show_signature_annotations | true | --no-signature-annotations |
--docstring-style exists for completeness only. The dump arrives already
parsed, so a different style means re-dumping with
griffe dump <package> --docstyle numpy -f; the generator refuses rather than
pretend it can reinterpret sections it is handed.
The artifacts
Section titled “The artifacts”Alongside the pages, a run writes four kinds of file under
<public>/<route prefix>/.
| File | What it holds |
|---|---|
| reference.json | Every module in one document, with $schema and a generatedFrom block naming the tool, its version and the source commit. |
| <module path>.json | One module on its own, without its subtree. This is what a page links to. |
| llms.txt | The title, one line of description, and every module page with its JSON link. |
| llms-full.txt | Every module rendered as Markdown, in module order, concatenated. |
public/ is served at the site root, so reference/api/helia_aot/air.json
resolves at /helia-aot/reference/api/helia_aot/air.json. Every generated page
carries a Machine-readable model link to exactly that file, one line under
the module description.
The JSON is byte-stable across runs: keys are sorted and nothing is stamped
with a time. A reference regenerated from an unchanged dump produces unchanged
files, which is what makes --check mean something.
The griffe pin
Section titled “The griffe pin”griffe’s dump output is a convenience format, not a versioned schema. The
extractor targets griffe 1.7.3 and checks the top-level shape before it
reads anything: a JSON object of packages, each a module with name, path
and members. Anything else is refused with the command that produces a dump
it understands, rather than emitting plausible-looking pages from a tree it
misread.
Pin griffe in the Python repository. When you lift the pin, run --check and
read the diff before committing it.
Keeping the pages honest in CI
Section titled “Keeping the pages honest in CI”Generated pages and artifacts are committed, so the site builds without Python.
--check renders in memory and compares against what is on disk, exiting 1 on
drift and naming every file that is stale, missing or orphaned.
- run: uv run griffe dump helia_aot --docstyle google -f > griffe.json- run: npx helia-ui-pyref --input griffe.json --out docs/src/content/docs/reference/api --public docs/public --base /helia-aot/ --checkPut it in the same workflow that builds the docs. A docstring edited without regenerating the reference then fails the pull request rather than shipping a page that disagrees with the code.
A run records what it wrote in <out>/.pyref-manifest.json and removes only
those files next time. A hand-written index page or a partial kept in the same
directory survives, which was not true when the directory was emptied before
every run.
The sidebar fragment
Section titled “The sidebar fragment”--sidebar writes a Starlight sidebar entry nested by package path, replacing
the literate-nav summary. Spread it into astro.config.mjs:
import apiSidebar from './src/generated/api-sidebar.json';
sidebar: [ { label: 'Guides', autogenerate: { directory: 'guides' } }, { label: 'API reference', items: apiSidebar.items },];Each package becomes a collapsed group whose first item is its own page,
labeled Overview.
What the output looks like
Section titled “What the output looks like”Google-style sections become fields of the model, and the renderer puts each
one where it belongs: parameters, returns and raises become RefParams tables
inside a labeled RefSection, examples become prose and a fenced block, and a
Google admonition stays an aside in the symbol’s description. A module page
opens with its own RefMembers index, because a generated page carries more
symbols than a table of contents will show.
---title: 'handler'description: 'Base class for all AOT code-generation handlers.'sidebar: order: 2---
import RefMembers from '@ambiqai/helia-ui/astro/RefMembers';import RefParams from '@ambiqai/helia-ui/astro/RefParams';import RefSection from '@ambiqai/helia-ui/astro/RefSection';import RefSymbol from '@ambiqai/helia-ui/astro/RefSymbol';
[Machine-readable model](/helia-aot/reference/api/helia_aot/aot/handlers/handler.json)
<RefMembers label="On this page" items={[ { id: 'helia_aot.aot.handlers.handler.AotHandler', name: 'AotHandler', kind: 'class', }, ]}/>
<RefSymbol id={"helia_aot.aot.handlers.handler.AotHandler"} name={"AotHandler"} kind={"class"} language={"python"} signature={"AotHandler(context: CodeGenContext)"} level={2} summary={"Base class for all AOT code-generation handlers."} source={{ "path": "src/helia_aot/aot/handlers/handler.py", "line": 41 }}>
:::note[Lifecycle]An AOT handler resolves, plans and emits, in that order.
:::
<RefSection title="Parameters"> <RefParams caption={'Parameters of AotHandler'} density={'compact'} rows={[ { name: 'context', type: 'CodeGenContext', default: 'Required', description: 'Code generation context.', }, ]} /></RefSection>
</RefSymbol>Anchors
Section titled “Anchors”RefSymbol renders the symbol’s id as the element id, and that id is the fully
qualified dotted path — the same anchor mkdocstrings produced. A link written
against the MkDocs site still lands on the right symbol, and so does a link
written against a page this generator produced two designs ago.
Cross-references resolve to those anchors. The symbol heading is rendered by the
part rather than written as Markdown, so it does not appear in Starlight’s table
of contents; the RefMembers index at the top of each page is what lists the
symbols instead.
Cross-references
Section titled “Cross-references”[Name][module.path] in a docstring becomes a link to the page and anchor that
carries the symbol, prefixed with --base. The extractor leaves the syntax
alone and the renderer resolves it, because only the renderer knows the routes.
A reference that resolves to nothing keeps its text and is reported as a warning
rather than shipped as a dead link. Cross-reference syntax inside backticks or a
fenced block is left alone, so [w][z][y][x] describing an array layout stays
what it is.
Where it differs from mkdocstrings
Section titled “Where it differs from mkdocstrings”Material-specific content in docstrings — :material-icon: shortcodes,
pymdownx fences — is passed through untouched. Converting it is part of moving
the authored pages, not of generating the reference.