mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
Create a custom Sphinx extension to document configuration values with
a simplified syntax. It is based on the `confval` but takes less space
when rendered. The extension provides:
- A `conf` directive for documenting individual configuration values
with optional type and default parameters
- A `conf` role for cross-referencing configuration values
- Automatic formatting of default values in the signature
- A custom domain that handles indexing and cross-references
For example, if we have
.. conf:: search_limit
:default: 5
We refer to this configuration option with :conf:`plugins.discogs:search_limit`.
The extension is loaded by adding the docs/extensions directory to the
Python path and registering it in the Sphinx extensions list.
142 lines
4.5 KiB
Python
142 lines
4.5 KiB
Python
"""Sphinx extension for simple configuration value documentation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any, ClassVar
|
|
|
|
from docutils import nodes
|
|
from docutils.parsers.rst import directives
|
|
from sphinx import addnodes
|
|
from sphinx.directives import ObjectDescription
|
|
from sphinx.domains import Domain, ObjType
|
|
from sphinx.roles import XRefRole
|
|
from sphinx.util.nodes import make_refnode
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterable, Sequence
|
|
|
|
from docutils.nodes import Element
|
|
from docutils.parsers.rst.states import Inliner
|
|
from sphinx.addnodes import desc_signature, pending_xref
|
|
from sphinx.application import Sphinx
|
|
from sphinx.builders import Builder
|
|
from sphinx.environment import BuildEnvironment
|
|
from sphinx.util.typing import ExtensionMetadata, OptionSpec
|
|
|
|
|
|
class Conf(ObjectDescription[str]):
|
|
"""Directive for documenting a single configuration value."""
|
|
|
|
option_spec: ClassVar[OptionSpec] = {
|
|
"default": directives.unchanged,
|
|
}
|
|
|
|
def handle_signature(self, sig: str, signode: desc_signature) -> str:
|
|
"""Process the directive signature (the config name)."""
|
|
signode += addnodes.desc_name(sig, sig)
|
|
|
|
# Add default value if provided
|
|
if "default" in self.options:
|
|
signode += nodes.Text(" ")
|
|
default_container = nodes.inline("", "")
|
|
default_container += nodes.Text("(default: ")
|
|
default_container += nodes.literal("", self.options["default"])
|
|
default_container += nodes.Text(")")
|
|
signode += default_container
|
|
|
|
return sig
|
|
|
|
def add_target_and_index(
|
|
self, name: str, sig: str, signode: desc_signature
|
|
) -> None:
|
|
"""Add cross-reference target and index entry."""
|
|
target = f"conf-{name}"
|
|
if target not in self.state.document.ids:
|
|
signode["ids"].append(target)
|
|
self.state.document.note_explicit_target(signode)
|
|
|
|
# A unique full name which includes the document name
|
|
index_name = f"{self.env.docname.replace('/', '.')}:{name}"
|
|
# Register with the conf domain
|
|
domain = self.env.get_domain("conf")
|
|
domain.data["objects"][index_name] = (self.env.docname, target)
|
|
|
|
# Add to index
|
|
self.indexnode["entries"].append(
|
|
("single", f"{name} (configuration value)", target, "", None)
|
|
)
|
|
|
|
|
|
class ConfDomain(Domain):
|
|
"""Domain for simple configuration values."""
|
|
|
|
name = "conf"
|
|
label = "Simple Configuration"
|
|
object_types = {"conf": ObjType("conf", "conf")}
|
|
directives = {"conf": Conf}
|
|
roles = {"conf": XRefRole()}
|
|
initial_data: dict[str, Any] = {"objects": {}}
|
|
|
|
def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]:
|
|
"""Return an iterable of object tuples for the inventory."""
|
|
for name, (docname, targetname) in self.data["objects"].items():
|
|
# Remove the document name prefix for display
|
|
display_name = name.split(":")[-1]
|
|
yield (name, display_name, "conf", docname, targetname, 1)
|
|
|
|
def resolve_xref(
|
|
self,
|
|
env: BuildEnvironment,
|
|
fromdocname: str,
|
|
builder: Builder,
|
|
typ: str,
|
|
target: str,
|
|
node: pending_xref,
|
|
contnode: Element,
|
|
) -> Element | None:
|
|
if entry := self.data["objects"].get(target):
|
|
docname, targetid = entry
|
|
return make_refnode(
|
|
builder, fromdocname, docname, targetid, contnode
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
# sphinx.util.typing.RoleFunction
|
|
def conf_role(
|
|
name: str,
|
|
rawtext: str,
|
|
text: str,
|
|
lineno: int,
|
|
inliner: Inliner,
|
|
/,
|
|
options: dict[str, Any] | None = None,
|
|
content: Sequence[str] = (),
|
|
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
|
|
"""Role for referencing configuration values."""
|
|
node = addnodes.pending_xref(
|
|
"",
|
|
refdomain="conf",
|
|
reftype="conf",
|
|
reftarget=text,
|
|
refwarn=True,
|
|
**(options or {}),
|
|
)
|
|
node += nodes.literal(text, text.split(":")[-1])
|
|
return [node], []
|
|
|
|
|
|
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
app.add_domain(ConfDomain)
|
|
|
|
# register a top-level directive so users can use ".. conf:: ..."
|
|
app.add_directive("conf", Conf)
|
|
|
|
# Register role with short name
|
|
app.add_role("conf", conf_role)
|
|
return {
|
|
"version": "0.1",
|
|
"parallel_read_safe": True,
|
|
"parallel_write_safe": True,
|
|
}
|