Skip to content

Cross-references

This plugin links two kinds of names: the references you write in docstring prose, and the names inside type annotations. Both link to the same two places: a page on this site, or another project’s documentation through a Sphinx inventory. An inventory is the objects.inv file that Sphinx-based sites publish. It is a compressed index of object names to URLs, and it is why a pathlib.Path in one of your annotations can link to docs.python.org.

mkdocstrings’ reference syntax works in prose, so docstrings written for mkdocstrings keep their links:

def generate_report(source, /, name: str, *, fmt: str = "md") -> Report:
"""Build a report from a source object.
Returns a [Report][mypkg.report.Report] whose
[mypkg.report.Report.generate][] method writes to
[a path][pathlib.Path].
"""
  • [title][dotted.path]: your own link text, with the object’s dotted path as the target.
  • [dotted.path][]: the shorthand, where the target doubles as the link text.
  • A target outside your own packages resolves through an inventory. That is how pathlib.Path reaches the Python documentation.

Targets resolve against this package’s symbol index first, then the other configured packages’, then the inventories. The resulting href is the same one an annotation to that object would produce.

Both a documented path and the canonical path Griffe found the definition at resolve. If an object is documented at several paths (a re-export and its definition), the shortest one wins. So [Report][mypkg.Report] and [Report][mypkg.report.Report] both resolve to the same page.

The plugin rewrites references before it renders the Markdown, and only when the target resolves. Everything else survives into the page exactly as you typed it:

  • An unresolvable target keeps its brackets, so a typo is visible rather than silently dropped.
  • The plugin never touches fenced code blocks or inline code spans, so you can document the syntax itself.
  • If a target has a real Markdown reference definition ([target]: https://…) in the same docstring, the plugin leaves it to Markdown to handle.
  • The plugin leaves an escaped bracket (\[title][target]) alone.

Griffe serialises annotations as expression trees. Every name in one is a bare source name: Report, not mypkg.report.Report. This plugin resolves each name in a fixed order:

  1. The scope chain of the object the annotation belongs to. For a method of mypkg.report.Report, that means mypkg.report.Report, then mypkg.report, then mypkg, following aliases. If a module imports a name, that name resolves to where it was imported from.
  2. A fully qualified path, when the annotation spells one out (mypkg.report.Report).
  3. The builtins table, a curated list of the types, constants and exceptions that actually appear in annotations (str, dict, ValueError, …). The plugin links these only when an inventory says where they are documented.
  4. The configured Sphinx inventories, in configuration order, restricted to the py domain.

If nothing resolves, the name renders as plain text. The plugin walks compound annotations piece by piece: dict[str, float] | None links dict, str and float independently, and Mapping[str, Report] links out to the standard library and back into your own pages in the same line.

If the scope chain resolves a name to something undocumented, that name stays unlinked. The plugin does not then fall through to an inventory: in that scope the name means only what the scope says it means, and a link elsewhere would be wrong.

Every documented object’s heading id is its dotted path:

<h2 id="mypkg.report.Report"></h2>
<h3 id="mypkg.report.Report.generate"></h3>

This matches mkdocstrings’ anchor scheme rather than a slugified heading. That is why URLs from an existing mkdocstrings site keep working, and why inventories interoperate in both directions without a mapping layer. Module pages have no anchor of their own: a module is a page.

inventories takes a list of entries, each a URL or a local file, plus the base URL its relative URIs resolve against:

astro.config.mjs
starlightPydocs({
packages: [{ name: 'mypkg', search: ['../src'] }],
inventories: [
'python',
{ url: 'https://pandas.pydata.org/docs/objects.inv' },
{ url: 'https://example.com/docs/objects.inv', base: 'https://example.com/docs/' },
{ file: './vendor/internal.inv', base: 'https://docs.internal.example/' },
],
});
  • 'python' is the one built-in preset: CPython’s objects.inv with https://docs.python.org/3/ as its base.
  • base defaults to the inventory URL with its last segment removed, which is right for the conventional …/objects.inv layout. A local file has no URL to derive from, so base is required there.
  • Earlier entries win when two inventories describe the same name.
  • The plugin consults only the py domain. Otherwise, a std:label named str could send a Python annotation to an unrelated page.
  • The plugin caches downloads with ETag revalidation. cache: 'force' | 'revalidate' | 'bypass' controls that per entry. An inventory that cannot be loaded is a warning, not a build failure: annotations stay unlinked.

Each package serves its own objects.inv at <base>/objects.inv unless you set publishInventory: false. It is a Sphinx v2 inventory built from the symbol index, with the roles mkdocstrings uses (py:module, py:class, py:function, py:method, py:attribute). An mkdocs or Sphinx project that wants to link into your API reference can point at it:

mkdocs.yml
plugins:
- mkdocstrings:
handlers:
python:
inventories:
- url: https://example.com/api/mypkg/objects.inv
conf.py
intersphinx_mapping = {
"mypkg": ("https://example.com/api/mypkg", None),
}

Because the anchors are dotted paths, a link from either tool lands on the exact heading. A link from this site into an mkdocstrings site works the same way in reverse. This symmetry is why the anchor scheme exists.