Mini Shell
"""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")
# Choices or type
if "Enum" in ttype:
# include Enum choices
lines.append(indent(":options: " + ", ".join("``%r``" % x for x in trait.values))) # type:ignore[attr-defined]
else:
lines.append(indent(":trait type: " + ttype))
# Default value
# Ignore boring default values like None, [] or ''
if interesting_default_value(trait.default_value):
try:
dvr = trait.default_value_repr()
except Exception:
dvr = None # ignore defaults we can't construct
if dvr is not None:
if len(dvr) > 64:
dvr = dvr[:61] + "..."
# Double up backslashes, so they get to the rendered docs
dvr = dvr.replace("\\n", "\\\\n")
lines.append(indent(":default: ``%s``" % dvr))
# Command line aliases
if trait_aliases[fullname]:
fmt_aliases = format_aliases(trait_aliases[fullname])
lines.append(indent(":CLI option: " + fmt_aliases))
# Blank line
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)
# Flags also often act as aliases for a boolean trait.
# Treat flags which set one trait to True as aliases.
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")
Zerion Mini Shell 1.0