# -*- coding: utf-8 -*- """ sphinx.ext.autosummary.generate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Usable as a library or script to generate automatic RST source files for items referred to in autosummary:: directives. Each generated RST file contains a single auto*:: directive which extracts the docstring of the referred item. Example Makefile rule:: generate: sphinx-autogen -o source/generated source/*.rst :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import re import sys import pydoc import optparse from jinja2 import FileSystemLoader, TemplateNotFound from jinja2.sandbox import SandboxedEnvironment from sphinx import package_dir from sphinx.ext.autosummary import import_by_name, get_documenter from sphinx.jinja2glue import BuiltinTemplateLoader from sphinx.util.osutil import ensuredir from sphinx.util.inspect import safe_getattr # Add documenters to AutoDirective registry from sphinx.ext.autodoc import add_documenter, \ ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, \ FunctionDocumenter, MethodDocumenter, AttributeDocumenter, \ InstanceAttributeDocumenter add_documenter(ModuleDocumenter) add_documenter(ClassDocumenter) add_documenter(ExceptionDocumenter) add_documenter(DataDocumenter) add_documenter(FunctionDocumenter) add_documenter(MethodDocumenter) add_documenter(AttributeDocumenter) add_documenter(InstanceAttributeDocumenter) def main(argv=sys.argv): usage = """%prog [OPTIONS] SOURCEFILE ...""" p = optparse.OptionParser(usage.strip()) p.add_option("-o", "--output-dir", action="store", type="string", dest="output_dir", default=None, help="Directory to place all output in") p.add_option("-s", "--suffix", action="store", type="string", dest="suffix", default="rst", help="Default suffix for files (default: %default)") p.add_option("-t", "--templates", action="store", type="string", dest="templates", default=None, help="Custom template directory (default: %default)") options, args = p.parse_args(argv[1:]) if len(args) < 1: p.error('no input files given') generate_autosummary_docs(args, options.output_dir, "." + options.suffix, template_dir=options.templates) def _simple_info(msg): print msg def _simple_warn(msg): print >> sys.stderr, 'WARNING: ' + msg # -- Generating output --------------------------------------------------------- def generate_autosummary_docs(sources, output_dir=None, suffix='.rst', warn=_simple_warn, info=_simple_info, base_path=None, builder=None, template_dir=None): showed_sources = list(sorted(sources)) if len(showed_sources) > 20: showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:] info('[autosummary] generating autosummary for: %s' % ', '.join(showed_sources)) if output_dir: info('[autosummary] writing to %s' % output_dir) if base_path is not None: sources = [os.path.join(base_path, filename) for filename in sources] # create our own templating environment template_dirs = [os.path.join(package_dir, 'ext', 'autosummary', 'templates')] if builder is not None: # allow the user to override the templates template_loader = BuiltinTemplateLoader() template_loader.init(builder, dirs=template_dirs) else: if template_dir: template_dirs.insert(0, template_dir) template_loader = FileSystemLoader(template_dirs) template_env = SandboxedEnvironment(loader=template_loader) # read items = find_autosummary_in_files(sources) # remove possible duplicates items = dict([(item, True) for item in items]).keys() # keep track of new files new_files = [] # write for name, path, template_name in sorted(items, key=str): if path is None: # The corresponding autosummary:: directive did not have # a :toctree: option continue path = output_dir or os.path.abspath(path) ensuredir(path) try: name, obj, parent, mod_name = import_by_name(name) except ImportError, e: warn('[autosummary] failed to import %r: %s' % (name, e)) continue fn = os.path.join(path, name + suffix) # skip it if it exists if os.path.isfile(fn): continue new_files.append(fn) f = open(fn, 'w') try: doc = get_documenter(obj, parent) if template_name is not None: template = template_env.get_template(template_name) else: try: template = template_env.get_template('autosummary/%s.rst' % doc.objtype) except TemplateNotFound: template = template_env.get_template('autosummary/base.rst') def get_members(obj, typ, include_public=[]): items = [] for name in dir(obj): try: documenter = get_documenter(safe_getattr(obj, name), obj) except AttributeError: continue if documenter.objtype == typ: items.append(name) public = [x for x in items if x in include_public or not x.startswith('_')] return public, items ns = {} if doc.objtype == 'module': ns['members'] = dir(obj) ns['functions'], ns['all_functions'] = \ get_members(obj, 'function') ns['classes'], ns['all_classes'] = \ get_members(obj, 'class') ns['exceptions'], ns['all_exceptions'] = \ get_members(obj, 'exception') elif doc.objtype == 'class': ns['members'] = dir(obj) ns['methods'], ns['all_methods'] = \ get_members(obj, 'method', ['__init__']) ns['attributes'], ns['all_attributes'] = \ get_members(obj, 'attribute') parts = name.split('.') if doc.objtype in ('method', 'attribute'): mod_name = '.'.join(parts[:-2]) cls_name = parts[-2] obj_name = '.'.join(parts[-2:]) ns['class'] = cls_name else: mod_name, obj_name = '.'.join(parts[:-1]), parts[-1] ns['fullname'] = name ns['module'] = mod_name ns['objname'] = obj_name ns['name'] = parts[-1] ns['objtype'] = doc.objtype ns['underline'] = len(name) * '=' rendered = template.render(**ns) f.write(rendered) finally: f.close() # descend recursively to new files if new_files: generate_autosummary_docs(new_files, output_dir=output_dir, suffix=suffix, warn=warn, info=info, base_path=base_path, builder=builder, template_dir=template_dir) # -- Finding documented entries in files --------------------------------------- def find_autosummary_in_files(filenames): """Find out what items are documented in source/*.rst. See `find_autosummary_in_lines`. """ documented = [] for filename in filenames: f = open(filename, 'r') lines = f.read().splitlines() documented.extend(find_autosummary_in_lines(lines, filename=filename)) f.close() return documented def find_autosummary_in_docstring(name, module=None, filename=None): """Find out what items are documented in the given object's docstring. See `find_autosummary_in_lines`. """ try: real_name, obj, parent, modname = import_by_name(name) lines = pydoc.getdoc(obj).splitlines() return find_autosummary_in_lines(lines, module=name, filename=filename) except AttributeError: pass except ImportError, e: print "Failed to import '%s': %s" % (name, e) except SystemExit, e: print("Failed to import '%s'; the module executes module level " "statement and it might call sys.exit()." % name) return [] def find_autosummary_in_lines(lines, module=None, filename=None): """Find out what items appear in autosummary:: directives in the given lines. Returns a list of (name, toctree, template) where *name* is a name of an object and *toctree* the :toctree: path of the corresponding autosummary directive (relative to the root of the file name), and *template* the value of the :template: option. *toctree* and *template* ``None`` if the directive does not have the corresponding options set. """ autosummary_re = re.compile(r'^(\s*)\.\.\s+autosummary::\s*') automodule_re = re.compile( r'^\s*\.\.\s+automodule::\s*([A-Za-z0-9_.]+)\s*$') module_re = re.compile( r'^\s*\.\.\s+(current)?module::\s*([a-zA-Z0-9_.]+)\s*$') autosummary_item_re = re.compile(r'^\s+(~?[_a-zA-Z][a-zA-Z0-9_.]*)\s*.*?') toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$') template_arg_re = re.compile(r'^\s+:template:\s*(.*?)\s*$') documented = [] toctree = None template = None current_module = module in_autosummary = False base_indent = "" for line in lines: if in_autosummary: m = toctree_arg_re.match(line) if m: toctree = m.group(1) if filename: toctree = os.path.join(os.path.dirname(filename), toctree) continue m = template_arg_re.match(line) if m: template = m.group(1).strip() continue if line.strip().startswith(':'): continue # skip options m = autosummary_item_re.match(line) if m: name = m.group(1).strip() if name.startswith('~'): name = name[1:] if current_module and \ not name.startswith(current_module + '.'): name = "%s.%s" % (current_module, name) documented.append((name, toctree, template)) continue if not line.strip() or line.startswith(base_indent + " "): continue in_autosummary = False m = autosummary_re.match(line) if m: in_autosummary = True base_indent = m.group(1) toctree = None template = None continue m = automodule_re.search(line) if m: current_module = m.group(1).strip() # recurse into the automodule docstring documented.extend(find_autosummary_in_docstring( current_module, filename=filename)) continue m = module_re.match(line) if m: current_module = m.group(2) continue return documented if __name__ == '__main__': main()