Skip to content
helia-ui
Sections
HELIA HUB

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 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.

Terminal window
# 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.json

Pages 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.

Command-line options
OptionDefaultWhat it does
--input <file>requiredThe griffe dump to read.
--out <dir>requiredDirectory the module pages are written into.
--public <dir>publicDirectory 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 setOrigin for the absolute URLs in llms.txt. Without it they are site-root relative.
--package <name>the only oneWhich package to render when the dump holds several.
--sidebar <file>not writtenWrite the Starlight sidebar fragment as JSON.
--source-root <dir>not setStrip this prefix from the source paths in the model.
--source-url <template>not setLink template for source, with {path} and {line}.
--commit <sha>not setCommit of the documented source, recorded in generatedFrom.
--checkoffCompare against what is on disk and exit 1 on drift.
--quietoffReport only the warning count, not each warning.
--route-prefix <path>reference/apiRoute 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 options and their flags
mkdocstrings optionDefault hereFlag
docstring_stylegoogle--docstring-style
show_root_headingfalse--show-root-heading
heading_level2--heading-level
merge_init_into_classtrue--no-merge-init-into-class
members_ordersource--members-order
filters!^_ and ^__init__$--filter (repeatable)
separate_signaturetrue--no-separate-signature
show_signaturetrue--no-show-signature
show_signature_annotationstrue--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.

Alongside the pages, a run writes four kinds of file under <public>/<route prefix>/.

What a run publishes
FileWhat it holds
reference.jsonEvery module in one document, with $schema and a generatedFrom block naming the tool, its version and the source commit.
<module path>.jsonOne module on its own, without its subtree. This is what a page links to.
llms.txtThe title, one line of description, and every module page with its JSON link.
llms-full.txtEvery 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.

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.

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/ --check

Put 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.

--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.

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>

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.

[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.

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.