From bf27f321a876d37a4da3aea685005059bc87444b Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 10 Nov 2017 14:02:55 +0000 Subject: [PATCH] Add Sphinx extension and autogen function for documenting config options --- traitlets/config/sphinxdoc.py | 159 ++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 traitlets/config/sphinxdoc.py diff --git a/traitlets/config/sphinxdoc.py b/traitlets/config/sphinxdoc.py new file mode 100644 index 00000000..8b0bfe76 --- /dev/null +++ b/traitlets/config/sphinxdoc.py @@ -0,0 +1,159 @@ +"""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 traitlets import Undefined +from collections import defaultdict + +from ipython_genutils.text import indent, dedent + +def setup(app): + """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') + metadata = {'parallel_read_safe': True, 'parallel_write_safe': True} + return metadata + +def interesting_default_value(dv): + 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): + fmted = [] + for a in aliases: + dashes = '-' if len(a) == 1 else '--' + fmted.append('``%s%s``' % (dashes, a)) + return ', '.join(fmted) + +def class_config_rst_doc(cls, trait_aliases): + """Generate rST documentation for this class' config options. + + Excludes traits defined on parent classes. + """ + lines = [] + classname = cls.__name__ + for k, trait in sorted(cls.class_traits(config=True).items()): + ttype = trait.__class__.__name__ + + fullname = classname + '.' + trait.name + lines += ['.. configtrait:: ' + fullname, + '' + ] + + help = trait.help.rstrip() or 'No description' + lines.append(indent(dedent(help), 4) + '\n') + + # Choices or type + if 'Enum' in ttype: + # include Enum choices + lines.append(indent( + ':options: ' + ', '.join('``%r``' % x for x in trait.values), 4)) + else: + lines.append(indent(':trait type: ' + ttype, 4)) + + # 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, 4)) + + # Command line aliases + if trait_aliases[fullname]: + fmt_aliases = format_aliases(trait_aliases[fullname]) + lines.append(indent(':CLI option: ' + fmt_aliases, 4)) + + # Blank line + lines.append('') + + return '\n'.join(lines) + +def reverse_aliases(app): + """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 = list(cfg)[0] + cls_cfg = cfg[classname] + if len(cls_cfg) == 1: + traitname = list(cls_cfg)[0] + if cls_cfg[traitname] is True: + res[classname+'.'+traitname].append(flag) + + return res + +def write_doc(path, title, app, preamble=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')