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)