"""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')