Skip to content

Multiple packages

One plugin instance documents any number of packages. Examples: the packages in a monorepo, a package plus its plugins, or several released versions of one package. Each entry gets its own pages, its own symbols.json, objects.inv and llms.txt, and its own sidebar group.

astro.config.mjs
import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import starlightPydocs, { pydocsSidebarGroup } from 'starlight-pydocs';
export default defineConfig({
integrations: [
starlight({
title: 'My project',
plugins: [
starlightPydocs({
packages: [
{ name: 'mycore', search: ['../packages/core/src'] },
{ name: 'myplugins', search: ['../packages/plugins/src'], docstringStyle: 'numpy' },
{ name: 'legacy', base: 'api/legacy', source: { file: './dumps/legacy.json' } },
],
}),
],
sidebar: [{ label: 'API reference', items: [pydocsSidebarGroup] }],
}),
],
});

That builds /api/mycore/, /api/myplugins/ and /api/legacy/, and puts all three trees under one API reference section in the sidebar. Almost every option applies per package. Packages that use different docstring styles, griffe extensions or source links do not need a second plugin instance. Only a handful of options are shared by all of them: the Sphinx inventories, the symbolSearch, publishInventory and llmsTxt switches, the components overrides and cacheDir.

Entries do not need to agree about where their model comes from either. legacy sets its own base (the URL path its pages are generated under) instead of using the default. It also reads a checked-in pre-generated dump instead of a search path, so griffe never runs for it. The other two entries extract from source in the same build.

This documentation site works this way. One starlightPydocs() call configures four entries: demopkg (google style, extracted with a griffe extension), numpkg (numpy style), sphpkg (sphinx style, from a pre-generated dump), and a second demopkg entry at its own base that reads a dump instead of extracting.

base is the URL path where an entry’s pages are generated. It defaults to api/<name>. base is also the identity of that entry everywhere else: in the sidebar tree, in route matching, and in the package prop of <Autodoc> and <SymbolSearch>. The Python import name is not an identity. You can document the same package several times, one release per entry. This is how several versions of one package live in one build.

Two rules follow from that.

Each entry needs a distinct base. No base may contain another’s. api/mypkg and api/mypkg_plugins are fine. api and api/mypkg are rejected: the pages of one would land inside the other’s page tree:

packages[1].base: 'api/mypkg' overlaps packages[0].base ('api'); give each package a distinct base

A name documented once resolves on its own. A name documented twice does not say which entry you mean. So the components resolve their package prop as a base first, then as an import name:

package prop Resolves to
package="1x/api/mypkg" The entry with that base. Always unambiguous.
package="mypkg" The single entry documenting mypkg, or an error listing the candidate bases.
omitted, on <Autodoc> The entry whose import name starts the dotted name prop, if only one does.
omitted, on <SymbolSearch> The first configured entry.

With mypkg documented at two bases, <Autodoc name="mypkg.Report" /> cannot pick one:

starlight-pydocs: <Autodoc name="mypkg.Report"> is ambiguous: 'mypkg' is documented at 2 bases. Set the package prop to one of these bases: 'api/mypkg', '1x/api/mypkg'.

<SymbolSearch> does not throw an error. An ambiguous or unknown package renders no search box at all, instead of searching a version of the package you did not ask for.

By default, every package’s tree replaces the shared pydocsSidebarGroup placeholder, in configuration order. This produces the single API reference section in the first example above.

To put one package somewhere else, give it a placeholder of its own. Three steps do this: create the placeholder with createPydocsSidebarGroup(), pass it to the package as sidebar.group, and add it to your sidebar where that package belongs.

astro.config.mjs
import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import starlightPydocs, { createPydocsSidebarGroup, pydocsSidebarGroup } from 'starlight-pydocs';
const pluginsApi = createPydocsSidebarGroup();
export default defineConfig({
integrations: [
starlight({
title: 'My project',
plugins: [
starlightPydocs({
packages: [
{ name: 'mycore', search: ['../packages/core/src'] },
{
name: 'myplugins',
search: ['../packages/plugins/src'],
sidebar: { group: pluginsApi },
},
],
}),
],
sidebar: [
{ label: 'Core API', items: [pydocsSidebarGroup] },
{ label: 'Plugin API', collapsed: true, items: [pluginsApi] },
],
}),
],
});

Each placeholder is replaced by the trees of the packages assigned to it. A package with its own group leaves the shared placeholder to the rest. If you create a placeholder but never add it to your sidebar, it is never substituted. That package’s pages still build, but they do not appear in the sidebar.

sidebar.label renames a package’s group and sidebar.collapsed collapses it:

astro.config.mjs
8 collapsed lines
import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import starlightPydocs, { pydocsSidebarGroup } from 'starlight-pydocs';
export default defineConfig({
integrations: [
starlight({
title: 'My project',
plugins: [
starlightPydocs({
packages: [
{
name: 'mypkg',
search: ['../src'],
sidebar: {
label: 'mypkg (unstable)',
collapsed: true,
},
},
],
6 collapsed lines
}),
],
sidebar: [{ label: 'API reference', items: [pydocsSidebarGroup] }],
}),
],
});

Inside a package’s group, the first entry is the package’s own page. It is labelled with the translated overview string (Overview in English). One entry per module follows, nested to mirror the module tree.

search paths resolve against the Astro project root. They can point anywhere, including sibling directories, so the Python sources do not have to live inside the docs site:

  • Directorypackages/
    • Directorycore/
      • Directorysrc/
        • Directorymycore/
    • Directoryplugins/
      • Directorysrc/
        • Directorymyplugins/
  • Directorydocs/ the Astro project, where astro.config.mjs lives
docs/astro.config.mjs
starlightPydocs({
packages: [
{
name: 'mycore',
search: ['../packages/core/src'],
sourceLink: { host: 'github', repo: 'you/repo', ref: 'main', root: '..' },
},
{
name: 'myplugins',
search: ['../packages/plugins/src'],
sourceLink: { host: 'github', repo: 'you/repo', ref: 'main', root: '..' },
},
],
});

root: '..' makes source links repository-relative from a docs/ subdirectory. See Source links. Give a package several search paths when its sources are split across directories.

Point each search path at the parent of the package directory, not at a whole checkout. The size and modification time of every .py and .pyi file underneath goes into that package’s extraction cache key. A path that covers unrelated code makes griffe re-run when code you are not documenting changes.

Docstring cross-references reach across packages. For example, [Report][mycore.Report] in a myplugins docstring links into the core package’s pages, with no inventory involved. Targets resolve in this order:

  1. the symbol index of the package being rendered;
  2. the symbol indexes of the other configured packages, in configuration order;
  3. the configured Sphinx inventories.

Entries sharing an import name are skipped at step 2. This stops one documented version of a package from linking into another version’s pages and silently mixing two APIs.

Type annotations do not cross packages. A name in an annotation resolves against the rendered package’s own scopes and symbol index, then against the inventories. Otherwise it renders as plain text, even when it names a class documented elsewhere on the same site.

Each entry serves its own files under its own base:

/api/mypkg/symbols.json /1x/api/mypkg/symbols.json
/api/mypkg/objects.inv /1x/api/mypkg/objects.inv
/api/mypkg/llms.txt /1x/api/mypkg/llms.txt

There is no combined index across entries. Symbol search covers one entry at a time. <SymbolSearch package="1x/api/mypkg" /> chooses which entry, by base or by an unambiguous import name. A project that publishes several inventories must point consumers at each one.