|
| 1 | +#!/usr/bin/env python |
| 2 | +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- |
| 3 | +# vi: set ft=python sts=4 ts=4 sw=4 et: |
| 4 | +r""" |
| 5 | +autosummary_generate.py OPTIONS FILES |
| 6 | +
|
| 7 | +Generate automatic RST source files for items referred to in |
| 8 | +autosummary:: directives. |
| 9 | +
|
| 10 | +Each generated RST file contains a single auto*:: directive which |
| 11 | +extracts the docstring of the referred item. |
| 12 | +
|
| 13 | +Example Makefile rule:: |
| 14 | +
|
| 15 | + generate: |
| 16 | + ./ext/autosummary_generate.py -o source/generated source/*.rst |
| 17 | +
|
| 18 | +""" |
| 19 | +import glob, re, inspect, os, optparse, pydoc |
| 20 | +from autosummary import import_by_name |
| 21 | + |
| 22 | +try: |
| 23 | + from phantom_import import import_phantom_module |
| 24 | +except ImportError: |
| 25 | + import_phantom_module = lambda x: x |
| 26 | + |
| 27 | +def main(): |
| 28 | + p = optparse.OptionParser(__doc__.strip()) |
| 29 | + p.add_option("-p", "--phantom", action="store", type="string", |
| 30 | + dest="phantom", default=None, |
| 31 | + help="Phantom import modules from a file") |
| 32 | + p.add_option("-o", "--output-dir", action="store", type="string", |
| 33 | + dest="output_dir", default=None, |
| 34 | + help=("Write all output files to the given directory (instead " |
| 35 | + "of writing them as specified in the autosummary:: " |
| 36 | + "directives)")) |
| 37 | + options, args = p.parse_args() |
| 38 | + |
| 39 | + if len(args) == 0: |
| 40 | + p.error("wrong number of arguments") |
| 41 | + |
| 42 | + if options.phantom and os.path.isfile(options.phantom): |
| 43 | + import_phantom_module(options.phantom) |
| 44 | + |
| 45 | + # read |
| 46 | + names = {} |
| 47 | + for name, loc in get_documented(args).items(): |
| 48 | + for (filename, sec_title, keyword, toctree) in loc: |
| 49 | + if toctree is not None: |
| 50 | + path = os.path.join(os.path.dirname(filename), toctree) |
| 51 | + names[name] = os.path.abspath(path) |
| 52 | + |
| 53 | + # write |
| 54 | + for name, path in sorted(names.items()): |
| 55 | + if options.output_dir is not None: |
| 56 | + path = options.output_dir |
| 57 | + |
| 58 | + if not os.path.isdir(path): |
| 59 | + os.makedirs(path) |
| 60 | + |
| 61 | + try: |
| 62 | + obj, name = import_by_name(name) |
| 63 | + except ImportError, e: |
| 64 | + print "Failed to import '%s': %s" % (name, e) |
| 65 | + continue |
| 66 | + |
| 67 | + fn = os.path.join(path, '%s.rst' % name) |
| 68 | + |
| 69 | + if os.path.exists(fn): |
| 70 | + # skip |
| 71 | + continue |
| 72 | + |
| 73 | + f = open(fn, 'w') |
| 74 | + |
| 75 | + try: |
| 76 | + f.write('%s\n%s\n\n' % (name, '='*len(name))) |
| 77 | + |
| 78 | + if inspect.isclass(obj): |
| 79 | + if issubclass(obj, Exception): |
| 80 | + f.write(format_modulemember(name, 'autoexception')) |
| 81 | + else: |
| 82 | + f.write(format_modulemember(name, 'autoclass')) |
| 83 | + elif inspect.ismodule(obj): |
| 84 | + f.write(format_modulemember(name, 'automodule')) |
| 85 | + elif inspect.ismethod(obj) or inspect.ismethoddescriptor(obj): |
| 86 | + f.write(format_classmember(name, 'automethod')) |
| 87 | + elif callable(obj): |
| 88 | + f.write(format_modulemember(name, 'autofunction')) |
| 89 | + elif hasattr(obj, '__get__'): |
| 90 | + f.write(format_classmember(name, 'autoattribute')) |
| 91 | + else: |
| 92 | + f.write(format_modulemember(name, 'autofunction')) |
| 93 | + finally: |
| 94 | + f.close() |
| 95 | + |
| 96 | +def format_modulemember(name, directive): |
| 97 | + parts = name.split('.') |
| 98 | + mod, name = '.'.join(parts[:-1]), parts[-1] |
| 99 | + return ".. currentmodule:: %s\n\n.. %s:: %s\n" % (mod, directive, name) |
| 100 | + |
| 101 | +def format_classmember(name, directive): |
| 102 | + parts = name.split('.') |
| 103 | + mod, name = '.'.join(parts[:-2]), '.'.join(parts[-2:]) |
| 104 | + return ".. currentmodule:: %s\n\n.. %s:: %s\n" % (mod, directive, name) |
| 105 | + |
| 106 | +def get_documented(filenames): |
| 107 | + """ |
| 108 | + Find out what items are documented in source/*.rst |
| 109 | + See `get_documented_in_lines`. |
| 110 | +
|
| 111 | + """ |
| 112 | + documented = {} |
| 113 | + for filename in filenames: |
| 114 | + f = open(filename, 'r') |
| 115 | + lines = f.read().splitlines() |
| 116 | + documented.update(get_documented_in_lines(lines, filename=filename)) |
| 117 | + f.close() |
| 118 | + return documented |
| 119 | + |
| 120 | +def get_documented_in_docstring(name, module=None, filename=None): |
| 121 | + """ |
| 122 | + Find out what items are documented in the given object's docstring. |
| 123 | + See `get_documented_in_lines`. |
| 124 | + |
| 125 | + """ |
| 126 | + try: |
| 127 | + obj, real_name = import_by_name(name) |
| 128 | + lines = pydoc.getdoc(obj).splitlines() |
| 129 | + return get_documented_in_lines(lines, module=name, filename=filename) |
| 130 | + except AttributeError: |
| 131 | + pass |
| 132 | + except ImportError, e: |
| 133 | + print "Failed to import '%s': %s" % (name, e) |
| 134 | + return {} |
| 135 | + |
| 136 | +def get_documented_in_lines(lines, module=None, filename=None): |
| 137 | + """ |
| 138 | + Find out what items are documented in the given lines |
| 139 | + |
| 140 | + Returns |
| 141 | + ------- |
| 142 | + documented : dict of list of (filename, title, keyword, toctree) |
| 143 | + Dictionary whose keys are documented names of objects. |
| 144 | + The value is a list of locations where the object was documented. |
| 145 | + Each location is a tuple of filename, the current section title, |
| 146 | + the name of the directive, and the value of the :toctree: argument |
| 147 | + (if present) of the directive. |
| 148 | +
|
| 149 | + """ |
| 150 | + title_underline_re = re.compile("^[-=*_^#]{3,}\s*$") |
| 151 | + autodoc_re = re.compile(".. auto(function|method|attribute|class|exception|module)::\s*([A-Za-z0-9_.]+)\s*$") |
| 152 | + autosummary_re = re.compile(r'^\.\.\s+autosummary::\s*') |
| 153 | + module_re = re.compile(r'^\.\.\s+(current)?module::\s*([a-zA-Z0-9_.]+)\s*$') |
| 154 | + autosummary_item_re = re.compile(r'^\s+([_a-zA-Z][a-zA-Z0-9_.]*)\s*.*?') |
| 155 | + toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$') |
| 156 | + |
| 157 | + documented = {} |
| 158 | + |
| 159 | + current_title = [] |
| 160 | + last_line = None |
| 161 | + toctree = None |
| 162 | + current_module = module |
| 163 | + in_autosummary = False |
| 164 | + |
| 165 | + for line in lines: |
| 166 | + try: |
| 167 | + if in_autosummary: |
| 168 | + m = toctree_arg_re.match(line) |
| 169 | + if m: |
| 170 | + toctree = m.group(1) |
| 171 | + continue |
| 172 | + |
| 173 | + if line.strip().startswith(':'): |
| 174 | + continue # skip options |
| 175 | + |
| 176 | + m = autosummary_item_re.match(line) |
| 177 | + if m: |
| 178 | + name = m.group(1).strip() |
| 179 | + if current_module and not name.startswith(current_module + '.'): |
| 180 | + name = "%s.%s" % (current_module, name) |
| 181 | + documented.setdefault(name, []).append( |
| 182 | + (filename, current_title, 'autosummary', toctree)) |
| 183 | + continue |
| 184 | + if line.strip() == '': |
| 185 | + continue |
| 186 | + in_autosummary = False |
| 187 | + |
| 188 | + m = autosummary_re.match(line) |
| 189 | + if m: |
| 190 | + in_autosummary = True |
| 191 | + continue |
| 192 | + |
| 193 | + m = autodoc_re.search(line) |
| 194 | + if m: |
| 195 | + name = m.group(2).strip() |
| 196 | + if m.group(1) == "module": |
| 197 | + current_module = name |
| 198 | + documented.update(get_documented_in_docstring( |
| 199 | + name, filename=filename)) |
| 200 | + elif current_module and not name.startswith(current_module+'.'): |
| 201 | + name = "%s.%s" % (current_module, name) |
| 202 | + documented.setdefault(name, []).append( |
| 203 | + (filename, current_title, "auto" + m.group(1), None)) |
| 204 | + continue |
| 205 | + |
| 206 | + m = title_underline_re.match(line) |
| 207 | + if m and last_line: |
| 208 | + current_title = last_line.strip() |
| 209 | + continue |
| 210 | + |
| 211 | + m = module_re.match(line) |
| 212 | + if m: |
| 213 | + current_module = m.group(2) |
| 214 | + continue |
| 215 | + finally: |
| 216 | + last_line = line |
| 217 | + |
| 218 | + return documented |
| 219 | + |
| 220 | +if __name__ == "__main__": |
| 221 | + main() |
0 commit comments