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

## References in docstring prose

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

```python "[Report][mypkg.report.Report]" "[mypkg.report.Report.generate][]" "[a path][pathlib.Path]"
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.

### What is left as written

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.

:::note[The `llms.txt` rendition keeps the raw syntax]
The plugin passes docstring prose through to
[`llms.txt`](/starlight-pydocs/guides/llms-txt/) verbatim, so references appear there as
`[title][dotted.path]`. That output is for machines, and a dotted path works better for them
than a resolved URL.
:::

## Linked annotations

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.

## Anchors

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

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

## Consuming inventories

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

```js title="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.

## Publishing your inventory

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:

```yaml title="mkdocs.yml"
plugins:
  - mkdocstrings:
      handlers:
        python:
          inventories:
            - url: https://example.com/api/mypkg/objects.inv
```

```python title="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.

[Docstring styles](/starlight-pydocs/guides/docstring-styles/)
  [Multiple packages](/starlight-pydocs/guides/multiple-packages/)