Skip to content

API reference

Plugin

RichDocsPlugin

Bases: BasePlugin[RichDocsConfig]

Batteries-included Shades-of-Purple docs experience for MkDocs Material.

Source code in src/richdocs/plugin.py
class RichDocsPlugin(BasePlugin[RichDocsConfig]):
    """Batteries-included Shades-of-Purple docs experience for MkDocs Material."""

    def __init__(self) -> None:
        self._symbol_index: SymbolIndex | None = None
        self._linkifier: Linkifier | None = None
        self._api_indexer: ApiIndexer | None = None
        self._launcher: JupyterLauncher | None = None
        self._config_js: str = ""
        self._palette_css: str = ""
        self._symbols_css: str = ""

    # -- configuration ----------------------------------------------------

    def on_startup(self, *, command: str, dirty: bool) -> None:
        _jupyter.note_command(command)

    def on_config(self, config: Any) -> Any:
        package = self.config.package.strip()
        if not package:
            raise ConfigurationError(
                "richdocs: 'package' is required — set it to the Python package to "
                "document and index, e.g.\n    plugins:\n      - richdocs:\n          package: your_package"
            )
        self._warn_if_after_mkdocstrings(config)

        api = self.config.api
        id_prefix = (api.id_prefix or package).strip()
        token = self.config.live_code.token or f"{package}-docs"
        project_dir = self._project_dir(config)

        lowercase = frozenset(api.registry_exports) | frozenset(api.extra_short_names)
        spec = IndexSpec(
            package=package,
            id_prefix=id_prefix,
            cache_path=project_dir / ".richdocs-cache" / "api-anchor-ids.json",
            registry_exports=dict(api.registry_exports),
            ambiguous_short_names=frozenset(api.ambiguous_short_names),
            prefer_class_for_short=dict(api.prefer_class_for_short),
            short_name_blocklist=frozenset(api.short_name_blocklist),
            lowercase_short_names=lowercase,
            extra_modules=tuple(api.extra_modules),
            section_suffixes=tuple(api.section_suffixes),
        )
        self._symbol_index = SymbolIndex(spec)
        link = api.linkify
        self._linkifier = (
            Linkifier(
                self._symbol_index,
                skip_extensions=tuple(link.skip_extensions),
                link_short_names=bool(link.short_names),
                link_dotted=bool(link.dotted),
                aliases=dict(link.aliases),
            )
            if self.config.features.linkify_inline_code
            else None
        )
        overrides = {k: int(v) for k, v in api.page_priority_overrides.items()}
        priority = build_priority_resolver(config.get("nav"), overrides)
        self._api_indexer = ApiIndexer(spec, priority)

        live = self.config.live_code
        if live.enabled:
            script = (project_dir / live.launcher_script).resolve() if live.launcher_script else None
            self._launcher = JupyterLauncher(
                jupyter_url=live.jupyter_url,
                token=token,
                launcher_port=live.launcher_port,
                launcher_script=script,
                cwd=project_dir,
            )

        self._config_js = generate_config_js(self.config, id_prefix=id_prefix, token=token, assets_dir=ASSETS_DIR)
        self._palette_css = generate_theme_overrides_css(self.config)
        self._symbols_css = generate_symbols_css(self.config)

        register_static_assets(config, self.config)
        set_mkdocstrings_templates(config, ASSETS_DIR)
        # API hover/tooltips rely on Material's tooltip system.
        if self.config.features.api_hover:
            ensure_material_features(config, ["content.tooltips"])
        return config

    # -- files ------------------------------------------------------------

    def on_files(self, files: Files, config: Any) -> Files:
        bundled = [
            *bundled_static_files(ASSETS_DIR, config),
            File.generated(config, CONFIG_JS_URI, content=self._config_js),
            File.generated(config, PALETTE_CSS_URI, content=self._palette_css),
            File.generated(config, SYMBOLS_CSS_URI, content=self._symbols_css),
        ]
        for file in bundled:
            # Plugin assets win over any same-named project file (e.g. before the
            # project removes its own copies). Remove-before-append per the
            # MkDocs Files API to avoid a DeprecationWarning.
            existing = files.get_file_from_path(file.src_uri)
            if existing is not None:
                files.remove(existing)
            files.append(file)
        return files

    # -- markdown ---------------------------------------------------------

    def on_page_markdown(self, markdown: str, *, page: Any, config: Any, files: Any) -> str:
        if self._linkifier is None:
            return markdown
        if not str(getattr(page.file, "src_path", "")).endswith(".md"):
            return markdown
        return self._linkifier.linkify_markdown(markdown)

    # -- post build -------------------------------------------------------

    def on_post_build(self, *, config: Any) -> None:
        site_dir = Path(config["site_dir"])
        if self.config.features.api_hover and self._api_indexer and self._symbol_index:
            anchor_ids = self._api_indexer.write_symbol_index(site_dir)
            self._symbol_index.write_anchor_cache(anchor_ids)
        sync_toc_labels(site_dir)
        # Relabel decorator-label text after the TOC mirror so both are rewritten.
        relabel_decorator_labels(site_dir, dict(self.config.symbols.decorator_labels))
        if self._launcher:
            self._launcher.on_post_build()

    # -- live-code dev server --------------------------------------------

    def on_serve(self, server: Any, *, config: Any, builder: Any) -> Any:
        if self._launcher:
            return self._launcher.on_serve(server)
        return server

    def on_shutdown(self) -> None:
        if self._launcher:
            self._launcher.on_shutdown()

    # -- helpers ----------------------------------------------------------

    @staticmethod
    def _project_dir(config: Any) -> Path:
        config_file = config.get("config_file_path")
        if config_file:
            return Path(config_file).resolve().parent
        return Path(config["docs_dir"]).resolve().parent

    @staticmethod
    def _warn_if_after_mkdocstrings(config: Any) -> None:
        names = list(config["plugins"].keys())
        if "mkdocstrings" in names and "richdocs" in names and names.index("richdocs") > names.index("mkdocstrings"):
            log.warning(
                "richdocs: list the 'richdocs' plugin BEFORE 'mkdocstrings' in mkdocs.yml "
                "so its enum/decorator-badge templates take effect."
            )

Configuration schema

The typed config classes backing the richdocs: block.

_config

Typed, validated config schema for the richdocs plugin.

Only package is required; everything else has sensible defaults that reproduce the bundled Shades-of-Purple look. The nested groups (api / symbols / live_code / theme / toc / features) keep the surface discoverable and self-documenting.

LinkifyConfig

Bases: Config

Fine-grained control over what inline/code symbols get auto-linked.

Source code in src/richdocs/_config.py
class LinkifyConfig(base.Config):
    """Fine-grained control over what inline/code symbols get auto-linked."""

    # Link bare short names (`Robot`) — not just fully-qualified `pkg.Robot`.
    short_names = c.Type(bool, default=True)
    # Resolve dotted expressions (`fmt.joint_names`) via the rightmost segment.
    dotted = c.Type(bool, default=True)
    # Inline-code spans matching these file extensions are never linked.
    skip_extensions = c.ListOfItems(c.Type(str), default=_DEFAULT_SKIP_EXTENSIONS)
    # Custom aliases: map an arbitrary word to a fully-qualified anchor id, e.g.
    # ``{"the result": "pkg.RetargetingResult"}``.
    aliases = c.Type(dict, default={})

ApiConfig

Bases: Config

API-symbol indexing, code-block hover, and inline auto-linking.

Source code in src/richdocs/_config.py
class ApiConfig(base.Config):
    """API-symbol indexing, code-block hover, and inline auto-linking."""

    # mkdocstrings anchor-id prefix. Defaults to ``package`` (anchors are the
    # fully-qualified Python paths, which start with the top-level package).
    id_prefix = c.Optional(c.Type(str))
    # Per-page canonical-page preference. Maps a page-URL suffix (e.g.
    # ``/api/models/``) to an integer; lower = preferred when a symbol is
    # documented on several pages. Merged over the nav-derived defaults.
    page_priority_overrides = c.Type(dict, default={})
    # Registry singletons (no stable qualname) → their mkdocstrings anchor id.
    registry_exports = c.Type(dict, default={})
    # Short names too ambiguous to auto-link.
    ambiguous_short_names = c.ListOfItems(c.Type(str), default=[])
    # When a short name resolves to several anchors, prefer the one under this
    # parent class. Maps short-name → class name.
    prefer_class_for_short = c.Type(dict, default={})
    # Short names never linked from inline code.
    short_name_blocklist = c.ListOfItems(c.Type(str), default=[])
    # Extra lowercase short names to treat as linkable (registry-export keys are
    # included automatically).
    extra_short_names = c.ListOfItems(c.Type(str), default=[])
    # Additional submodules whose ``__all__`` should be indexed (dotted paths).
    extra_modules = c.ListOfItems(c.Type(str), default=[])
    # mkdocstrings category-section anchor suffixes (not primary symbols).
    section_suffixes = c.ListOfItems(c.Type(str), default=["-functions", "-attributes", "-classes"])
    linkify = c.SubConfig(LinkifyConfig)

SymbolsConfig

Bases: Config

Relabel and recolor the API symbol-kind badges (headings + TOC).

labels maps a kind to the text shown in its badge ("" hides it); colors maps a kind to {fg: ..., bg: ...} (any CSS color). Both are override-only — unset kinds keep the bundled defaults. Recognized kinds: module, class, dataclass, enum, function, method, attribute, type_alias, member, property.

Source code in src/richdocs/_config.py
class SymbolsConfig(base.Config):
    """Relabel and recolor the API symbol-kind badges (headings + TOC).

    ``labels`` maps a kind to the text shown in its badge ("" hides it);
    ``colors`` maps a kind to ``{fg: ..., bg: ...}`` (any CSS color). Both are
    override-only — unset kinds keep the bundled defaults. Recognized kinds:
    ``module``, ``class``, ``dataclass``, ``enum``, ``function``, ``method``,
    ``attribute``, ``type_alias``, ``member``, ``property``.
    """

    labels = c.Type(dict, default={})
    colors = c.Type(dict, default={})
    # Relabel the *text* of mkdocstrings decorator labels (rendered by griffe),
    # e.g. ``{property: "prop", classmethod: "cls", dataclass: "data"}``. ``""``
    # removes the label. Applied to both headings and the TOC at build time.
    decorator_labels = c.Type(dict, default={})

HighlightConfig

Bases: Config

Shiki syntax highlighting.

Source code in src/richdocs/_config.py
class HighlightConfig(base.Config):
    """Shiki syntax highlighting."""

    # Bundled theme id (``shades-of-purple``) or a path to a Shiki theme JSON.
    # Falls back to ``theme.shiki_theme`` when unset.
    theme = c.Optional(c.Type(str))
    # Highlight inline code spans too (not just fenced blocks).
    inline = c.Type(bool, default=True)
    # Language assumed for fenced blocks with no explicit language.
    default_language = c.Type(str, default="python")
    # Grammars to preload.
    languages = c.ListOfItems(c.Type(str), default=_DEFAULT_LANGUAGES)
    # Language aliases (``py`` → ``python``).
    aliases = c.Type(dict, default=_DEFAULT_LANG_ALIASES)

PyodideConfig

Bases: Config

In-browser (WebAssembly) Python runtime for live code on static sites.

Source code in src/richdocs/_config.py
class PyodideConfig(base.Config):
    """In-browser (WebAssembly) Python runtime for live code on static sites."""

    # Pyodide version + CDN. ``index_url`` overrides the derived jsdelivr URL.
    version = c.Type(str, default="0.27.2")
    index_url = c.Optional(c.Type(str))
    # Packages to install (via micropip) before running — only those with
    # WASM wheels work (e.g. numpy, pandas; not torch/mujoco).
    packages = c.ListOfItems(c.Type(str), default=[])

LiveCodeConfig

Bases: Config

Runnable code blocks.

runtime: jupyter (local kernel), pyodide (in-browser WASM, works on a published static site), or auto (Jupyter if reachable, else Pyodide).

Source code in src/richdocs/_config.py
class LiveCodeConfig(base.Config):
    """Runnable code blocks.

    ``runtime``: ``jupyter`` (local kernel), ``pyodide`` (in-browser WASM, works
    on a published static site), or ``auto`` (Jupyter if reachable, else Pyodide).
    """

    enabled = c.Type(bool, default=True)
    # Default is the safe `jupyter` (deployed sites show a "needs Jupyter" notice
    # rather than erroring). Opt into `pyodide`/`auto` only when your runnable
    # blocks are browser-compatible (stdlib or packages with WASM wheels — your
    # own package usually is NOT, so `import yourpkg` would fail in the browser).
    runtime = c.Choice(("auto", "jupyter", "pyodide"), default="jupyter")
    pyodide = c.SubConfig(PyodideConfig)
    jupyter_url = c.Type(str, default="http://127.0.0.1:8888/")
    # Auth token. Defaults to ``<package>-docs``.
    token = c.Optional(c.Type(str))
    # Dev-only helper port that ``mkdocs serve`` exposes so the header switch can
    # spawn Jupyter without restarting the build.
    launcher_port = c.Type(int, default=8889)
    # Path (relative to the mkdocs.yml dir) to a script that starts Jupyter.
    launcher_script = c.Optional(c.Type(str))
    kernel = c.Type(str, default="python3")
    runnable_languages = c.ListOfItems(c.Type(str), default=["python", "bash"])
    connect_timeout_ms = c.Type(int, default=25000)
    execute_timeout_ms = c.Type(int, default=90000)

ThemeConfig

Bases: Config

Shades-of-Purple palette, layout, and Shiki highlighting.

Source code in src/richdocs/_config.py
class ThemeConfig(base.Config):
    """Shades-of-Purple palette, layout, and Shiki highlighting."""

    # Deprecated alias for ``theme.highlight.theme`` (kept for convenience).
    shiki_theme = c.Type(str, default="shades-of-purple")
    # Override any ``--rd-*`` palette token, e.g. ``page_bg: "#1e1e3f"``.
    palette = c.Type(dict, default={})
    # Override layout tokens, e.g. ``content_max_width: "61rem"``.
    layout = c.Type(dict, default={})
    highlight = c.SubConfig(HighlightConfig)

TocConfig

Bases: Config

In-page table-of-contents behavior.

Source code in src/richdocs/_config.py
class TocConfig(base.Config):
    """In-page table-of-contents behavior."""

    # Collapse nested API symbol/category sections by default (expand on click).
    collapse_default = c.Type(bool, default=True)
    # Extra px subtracted when scroll-spy decides the active heading (tune for
    # tall sticky headers). 0 = engine default.
    scrollspy_offset = c.Type(int, default=0)

FeaturesConfig

Bases: Config

Coarse on/off toggles for each piece of the experience.

Source code in src/richdocs/_config.py
class FeaturesConfig(base.Config):
    """Coarse on/off toggles for each piece of the experience."""

    shiki = c.Type(bool, default=True)
    api_hover = c.Type(bool, default=True)  # code-block symbol hover/click
    linkify_inline_code = c.Type(bool, default=True)  # inline `Symbol` → API link
    toc_collapsible = c.Type(bool, default=True)
    toc_scrollspy = c.Type(bool, default=True)
    hide_empty_toc = c.Type(bool, default=True)

RichDocsConfig

Bases: Config

Top-level config for the richdocs plugin.

Source code in src/richdocs/_config.py
class RichDocsConfig(base.Config):
    """Top-level config for the ``richdocs`` plugin."""

    # The Python package to document, index, and auto-link. Required.
    package = c.Type(str, default="")
    api = c.SubConfig(ApiConfig)
    symbols = c.SubConfig(SymbolsConfig)
    live_code = c.SubConfig(LiveCodeConfig)
    theme = c.SubConfig(ThemeConfig)
    toc = c.SubConfig(TocConfig)
    features = c.SubConfig(FeaturesConfig)