|
"""Machinery for documenting traitlets config options with Sphinx. |
|
|
|
This includes: |
|
|
|
- A Sphinx extension defining directives and roles for config options. |
|
- A function to generate an rst file given an Application instance. |
|
|
|
To make this documentation, first set this module as an extension in Sphinx's |
|
conf.py:: |
|
|
|
extensions = [ |
|
# ... |
|
'traitlets.config.sphinxdoc', |
|
] |
|
|
|
Autogenerate the config documentation by running code like this before |
|
Sphinx builds:: |
|
|
|
from traitlets.config.sphinxdoc import write_doc |
|
from myapp import MyApplication |
|
|
|
writedoc('config/options.rst', # File to write |
|
'MyApp config options', # Title |
|
MyApplication() |
|
) |
|
|
|
The generated rST syntax looks like this:: |
|
|
|
.. configtrait:: Application.log_datefmt |
|
|
|
Description goes here. |
|
|
|
Cross reference like this: :configtrait:`Application.log_datefmt`. |
|
""" |
|
from __future__ import annotations |
|
|
|
import typing as t |
|
from collections import defaultdict |
|
from textwrap import dedent |
|
|
|
from traitlets import HasTraits, Undefined |
|
from traitlets.config.application import Application |
|
from traitlets.utils.text import indent |
|
|
|
|
|
def setup(app: t.Any) -> dict[str, t.Any]: |
|
"""Registers the Sphinx extension. |
|
|
|
You shouldn't need to call this directly; configure Sphinx to use this |
|
module instead. |
|
""" |
|
app.add_object_type("configtrait", "configtrait", objname="Config option") |
|
return {"parallel_read_safe": True, "parallel_write_safe": True} |
|
|
|
|
|
def interesting_default_value(dv: t.Any) -> bool: |
|
if (dv is None) or (dv is Undefined): |
|
return False |
|
if isinstance(dv, (str, list, tuple, dict, set)): |
|
return bool(dv) |
|
return True |
|
|
|
|
|
def format_aliases(aliases: list[str]) -> str: |
|
fmted = [] |
|
for a in aliases: |
|
dashes = "-" if len(a) == 1 else "--" |
|
fmted.append(f"``{dashes}{a}``") |
|
return ", ".join(fmted) |
|
|
|
|
|
def class_config_rst_doc(cls: type[HasTraits], trait_aliases: dict[str, t.Any]) -> str: |
|
"""Generate rST documentation for this class' config options. |
|
|
|
Excludes traits defined on parent classes. |
|
""" |
|
lines = [] |
|
classname = cls.__name__ |
|
for _, trait in sorted(cls.class_traits(config=True).items()): |
|
ttype = trait.__class__.__name__ |
|
|
|
fullname = classname + "." + (trait.name or "") |
|
lines += [".. configtrait:: " + fullname, ""] |
|
|
|
help = trait.help.rstrip() or "No description" |
|
lines.append(indent(dedent(help)) + "\n") |
|
|
|
|
|
if "Enum" in ttype: |
|
|
|
lines.append(indent(":options: " + ", ".join("``%r``" % x for x in trait.values))) |
|
else: |
|
lines.append(indent(":trait type: " + ttype)) |
|
|
|
|
|
|
|
if interesting_default_value(trait.default_value): |
|
try: |
|
dvr = trait.default_value_repr() |
|
except Exception: |
|
dvr = None |
|
if dvr is not None: |
|
if len(dvr) > 64: |
|
dvr = dvr[:61] + "..." |
|
|
|
dvr = dvr.replace("\\n", "\\\\n") |
|
lines.append(indent(":default: ``%s``" % dvr)) |
|
|
|
|
|
if trait_aliases[fullname]: |
|
fmt_aliases = format_aliases(trait_aliases[fullname]) |
|
lines.append(indent(":CLI option: " + fmt_aliases)) |
|
|
|
|
|
lines.append("") |
|
|
|
return "\n".join(lines) |
|
|
|
|
|
def reverse_aliases(app: Application) -> dict[str, list[str]]: |
|
"""Produce a mapping of trait names to lists of command line aliases.""" |
|
res = defaultdict(list) |
|
for alias, trait in app.aliases.items(): |
|
res[trait].append(alias) |
|
|
|
|
|
|
|
for flag, (cfg, _) in app.flags.items(): |
|
if len(cfg) == 1: |
|
classname = next(iter(cfg)) |
|
cls_cfg = cfg[classname] |
|
if len(cls_cfg) == 1: |
|
traitname = next(iter(cls_cfg)) |
|
if cls_cfg[traitname] is True: |
|
res[classname + "." + traitname].append(flag) |
|
|
|
return res |
|
|
|
|
|
def write_doc(path: str, title: str, app: Application, preamble: str | None = None) -> None: |
|
"""Write a rst file documenting config options for a traitlets application. |
|
|
|
Parameters |
|
---------- |
|
path : str |
|
The file to be written |
|
title : str |
|
The human-readable title of the document |
|
app : traitlets.config.Application |
|
An instance of the application class to be documented |
|
preamble : str |
|
Extra text to add just after the title (optional) |
|
""" |
|
trait_aliases = reverse_aliases(app) |
|
with open(path, "w") as f: |
|
f.write(title + "\n") |
|
f.write(("=" * len(title)) + "\n") |
|
f.write("\n") |
|
if preamble is not None: |
|
f.write(preamble + "\n\n") |
|
|
|
for c in app._classes_inc_parents(): |
|
f.write(class_config_rst_doc(c, trait_aliases)) |
|
f.write("\n") |
|
|