sphinxdoc.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. """Machinery for documenting traitlets config options with Sphinx.
  2. This includes:
  3. - A Sphinx extension defining directives and roles for config options.
  4. - A function to generate an rst file given an Application instance.
  5. To make this documentation, first set this module as an extension in Sphinx's
  6. conf.py::
  7. extensions = [
  8. # ...
  9. 'traitlets.config.sphinxdoc',
  10. ]
  11. Autogenerate the config documentation by running code like this before
  12. Sphinx builds::
  13. from traitlets.config.sphinxdoc import write_doc
  14. from myapp import MyApplication
  15. writedoc('config/options.rst', # File to write
  16. 'MyApp config options', # Title
  17. MyApplication()
  18. )
  19. The generated rST syntax looks like this::
  20. .. configtrait:: Application.log_datefmt
  21. Description goes here.
  22. Cross reference like this: :configtrait:`Application.log_datefmt`.
  23. """
  24. from __future__ import annotations
  25. import typing as t
  26. from collections import defaultdict
  27. from textwrap import dedent
  28. from traitlets import HasTraits, Undefined
  29. from traitlets.config.application import Application
  30. from traitlets.utils.text import indent
  31. def setup(app: t.Any) -> dict[str, t.Any]:
  32. """Registers the Sphinx extension.
  33. You shouldn't need to call this directly; configure Sphinx to use this
  34. module instead.
  35. """
  36. app.add_object_type("configtrait", "configtrait", objname="Config option")
  37. return {"parallel_read_safe": True, "parallel_write_safe": True}
  38. def interesting_default_value(dv: t.Any) -> bool:
  39. if (dv is None) or (dv is Undefined):
  40. return False
  41. if isinstance(dv, (str, list, tuple, dict, set)):
  42. return bool(dv)
  43. return True
  44. def format_aliases(aliases: list[str]) -> str:
  45. fmted = []
  46. for a in aliases:
  47. dashes = "-" if len(a) == 1 else "--"
  48. fmted.append(f"``{dashes}{a}``")
  49. return ", ".join(fmted)
  50. def class_config_rst_doc(cls: type[HasTraits], trait_aliases: dict[str, t.Any]) -> str:
  51. """Generate rST documentation for this class' config options.
  52. Excludes traits defined on parent classes.
  53. """
  54. lines = []
  55. classname = cls.__name__
  56. for _, trait in sorted(cls.class_traits(config=True).items()):
  57. ttype = trait.__class__.__name__
  58. fullname = classname + "." + (trait.name or "")
  59. lines += [".. configtrait:: " + fullname, ""]
  60. help = trait.help.rstrip() or "No description"
  61. lines.append(indent(dedent(help)) + "\n")
  62. # Choices or type
  63. if "Enum" in ttype:
  64. # include Enum choices
  65. lines.append(indent(":options: " + ", ".join("``%r``" % x for x in trait.values))) # type:ignore[attr-defined]
  66. else:
  67. lines.append(indent(":trait type: " + ttype))
  68. # Default value
  69. # Ignore boring default values like None, [] or ''
  70. if interesting_default_value(trait.default_value):
  71. try:
  72. dvr = trait.default_value_repr()
  73. except Exception:
  74. dvr = None # ignore defaults we can't construct
  75. if dvr is not None:
  76. if len(dvr) > 64:
  77. dvr = dvr[:61] + "..."
  78. # Double up backslashes, so they get to the rendered docs
  79. dvr = dvr.replace("\\n", "\\\\n")
  80. lines.append(indent(":default: ``%s``" % dvr))
  81. # Command line aliases
  82. if trait_aliases[fullname]:
  83. fmt_aliases = format_aliases(trait_aliases[fullname])
  84. lines.append(indent(":CLI option: " + fmt_aliases))
  85. # Blank line
  86. lines.append("")
  87. return "\n".join(lines)
  88. def reverse_aliases(app: Application) -> dict[str, list[str]]:
  89. """Produce a mapping of trait names to lists of command line aliases."""
  90. res = defaultdict(list)
  91. for alias, trait in app.aliases.items():
  92. res[trait].append(alias)
  93. # Flags also often act as aliases for a boolean trait.
  94. # Treat flags which set one trait to True as aliases.
  95. for flag, (cfg, _) in app.flags.items():
  96. if len(cfg) == 1:
  97. classname = next(iter(cfg))
  98. cls_cfg = cfg[classname]
  99. if len(cls_cfg) == 1:
  100. traitname = next(iter(cls_cfg))
  101. if cls_cfg[traitname] is True:
  102. res[classname + "." + traitname].append(flag)
  103. return res
  104. def write_doc(path: str, title: str, app: Application, preamble: str | None = None) -> None:
  105. """Write a rst file documenting config options for a traitlets application.
  106. Parameters
  107. ----------
  108. path : str
  109. The file to be written
  110. title : str
  111. The human-readable title of the document
  112. app : traitlets.config.Application
  113. An instance of the application class to be documented
  114. preamble : str
  115. Extra text to add just after the title (optional)
  116. """
  117. trait_aliases = reverse_aliases(app)
  118. with open(path, "w") as f:
  119. f.write(title + "\n")
  120. f.write(("=" * len(title)) + "\n")
  121. f.write("\n")
  122. if preamble is not None:
  123. f.write(preamble + "\n\n")
  124. for c in app._classes_inc_parents():
  125. f.write(class_config_rst_doc(c, trait_aliases))
  126. f.write("\n")