You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			165 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			165 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
"""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")
 |