mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
Add Sphinx extension for configuration value documentation
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.
This commit is contained in:
parent
becb073aac
commit
a938449b29
4 changed files with 173 additions and 5 deletions
|
|
@ -6,6 +6,11 @@
|
|||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add custom extensions directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent / "extensions"))
|
||||
|
||||
project = "beets"
|
||||
AUTHOR = "Adrian Sampson"
|
||||
|
|
@ -26,6 +31,7 @@ extensions = [
|
|||
"sphinx.ext.viewcode",
|
||||
"sphinx_design",
|
||||
"sphinx_copybutton",
|
||||
"conf",
|
||||
]
|
||||
|
||||
autosummary_generate = True
|
||||
|
|
|
|||
142
docs/extensions/conf.py
Normal file
142
docs/extensions/conf.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
"""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,
|
||||
}
|
||||
15
poetry.lock
generated
15
poetry.lock
generated
|
|
@ -3473,6 +3473,17 @@ files = [
|
|||
[package.dependencies]
|
||||
types-html5lib = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-docutils"
|
||||
version = "0.22.2.20251006"
|
||||
description = "Typing stubs for docutils"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "types_docutils-0.22.2.20251006-py3-none-any.whl", hash = "sha256:1e61afdeb4fab4ae802034deea3e853ced5c9b5e1d156179000cb68c85daf384"},
|
||||
{file = "types_docutils-0.22.2.20251006.tar.gz", hash = "sha256:c36c0459106eda39e908e9147bcff9dbd88535975cde399433c428a517b9e3b2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-flask-cors"
|
||||
version = "6.0.0.20250520"
|
||||
|
|
@ -3650,7 +3661,7 @@ beatport = ["requests-oauthlib"]
|
|||
bpd = ["PyGObject"]
|
||||
chroma = ["pyacoustid"]
|
||||
discogs = ["python3-discogs-client"]
|
||||
docs = ["pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinx-design"]
|
||||
docs = ["docutils", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinx-design"]
|
||||
embedart = ["Pillow"]
|
||||
embyupdate = ["requests"]
|
||||
fetchart = ["Pillow", "beautifulsoup4", "langdetect", "requests"]
|
||||
|
|
@ -3672,4 +3683,4 @@ web = ["flask", "flask-cors"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9,<4"
|
||||
content-hash = "1db39186aca430ef6f1fd9e51b9dcc3ed91880a458bc21b22d950ed8589fdf5a"
|
||||
content-hash = "aedfeb1ac78ae0120855c6a7d6f35963c63cc50a8750142c95dd07ffd213683f"
|
||||
|
|
|
|||
|
|
@ -77,10 +77,11 @@ resampy = { version = ">=0.4.3", optional = true }
|
|||
requests-oauthlib = { version = ">=0.6.1", optional = true }
|
||||
soco = { version = "*", optional = true }
|
||||
|
||||
docutils = { version = ">=0.20.1", optional = true }
|
||||
pydata-sphinx-theme = { version = "*", optional = true }
|
||||
sphinx = { version = "*", optional = true }
|
||||
sphinx-design = { version = "^0.6.1", optional = true }
|
||||
sphinx-copybutton = { version = "^0.5.2", optional = true }
|
||||
sphinx-design = { version = ">=0.6.1", optional = true }
|
||||
sphinx-copybutton = { version = ">=0.5.2", optional = true }
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
beautifulsoup4 = "*"
|
||||
|
|
@ -109,6 +110,7 @@ sphinx-lint = ">=1.0.0"
|
|||
[tool.poetry.group.typing.dependencies]
|
||||
mypy = "*"
|
||||
types-beautifulsoup4 = "*"
|
||||
types-docutils = ">=0.22.2.20251006"
|
||||
types-mock = "*"
|
||||
types-Flask-Cors = "*"
|
||||
types-Pillow = "*"
|
||||
|
|
@ -131,7 +133,14 @@ beatport = ["requests-oauthlib"]
|
|||
bpd = ["PyGObject"] # gobject-introspection, gstreamer1.0-plugins-base, python3-gst-1.0
|
||||
chroma = ["pyacoustid"] # chromaprint or fpcalc
|
||||
# convert # ffmpeg
|
||||
docs = ["pydata-sphinx-theme", "sphinx", "sphinx-lint", "sphinx-design", "sphinx-copybutton"]
|
||||
docs = [
|
||||
"docutils",
|
||||
"pydata-sphinx-theme",
|
||||
"sphinx",
|
||||
"sphinx-lint",
|
||||
"sphinx-design",
|
||||
"sphinx-copybutton",
|
||||
]
|
||||
discogs = ["python3-discogs-client"]
|
||||
embedart = ["Pillow"] # ImageMagick
|
||||
embyupdate = ["requests"]
|
||||
|
|
|
|||
Loading…
Reference in a new issue