1 | """Attempt to generate templates for module reference with Sphinx |
---|
2 | |
---|
3 | XXX - we exclude extension modules |
---|
4 | |
---|
5 | To include extension modules, first identify them as valid in the |
---|
6 | ``_uri2path`` method, then handle them in the ``_parse_module`` script. |
---|
7 | |
---|
8 | We get functions and classes by parsing the text of .py files. |
---|
9 | Alternatively we could import the modules for discovery, and we'd have |
---|
10 | to do that for extension modules. This would involve changing the |
---|
11 | ``_parse_module`` method to work via import and introspection, and |
---|
12 | might involve changing ``discover_modules`` (which determines which |
---|
13 | files are modules, and therefore which module URIs will be passed to |
---|
14 | ``_parse_module``). |
---|
15 | |
---|
16 | NOTE: this is a modified version of a script originally shipped with the |
---|
17 | PyMVPA project, which we've adapted for NIPY use. PyMVPA is an MIT-licensed |
---|
18 | project.""" |
---|
19 | |
---|
20 | # Stdlib imports |
---|
21 | import os |
---|
22 | import re |
---|
23 | |
---|
24 | # Functions and classes |
---|
25 | class ApiDocWriter(object): |
---|
26 | ''' Class for automatic detection and parsing of API docs |
---|
27 | to Sphinx-parsable reST format''' |
---|
28 | |
---|
29 | # only separating first two levels |
---|
30 | rst_section_levels = ['*', '=', '-', '~', '^'] |
---|
31 | |
---|
32 | def __init__(self, |
---|
33 | package_name, |
---|
34 | rst_extension='.rst', |
---|
35 | package_skip_patterns=None, |
---|
36 | module_skip_patterns=None, |
---|
37 | ): |
---|
38 | ''' Initialize package for parsing |
---|
39 | |
---|
40 | Parameters |
---|
41 | ---------- |
---|
42 | package_name : string |
---|
43 | Name of the top-level package. *package_name* must be the |
---|
44 | name of an importable package |
---|
45 | rst_extension : string, optional |
---|
46 | Extension for reST files, default '.rst' |
---|
47 | package_skip_patterns : None or sequence of {strings, regexps} |
---|
48 | Sequence of strings giving URIs of packages to be excluded |
---|
49 | Operates on the package path, starting at (including) the |
---|
50 | first dot in the package path, after *package_name* - so, |
---|
51 | if *package_name* is ``sphinx``, then ``sphinx.util`` will |
---|
52 | result in ``.util`` being passed for earching by these |
---|
53 | regexps. If is None, gives default. Default is: |
---|
54 | ['\.tests$'] |
---|
55 | module_skip_patterns : None or sequence |
---|
56 | Sequence of strings giving URIs of modules to be excluded |
---|
57 | Operates on the module name including preceding URI path, |
---|
58 | back to the first dot after *package_name*. For example |
---|
59 | ``sphinx.util.console`` results in the string to search of |
---|
60 | ``.util.console`` |
---|
61 | If is None, gives default. Default is: |
---|
62 | ['\.setup$', '\._'] |
---|
63 | ''' |
---|
64 | if package_skip_patterns is None: |
---|
65 | package_skip_patterns = ['\\.tests$'] |
---|
66 | if module_skip_patterns is None: |
---|
67 | module_skip_patterns = ['\\.setup$', '\\._'] |
---|
68 | self.package_name = package_name |
---|
69 | self.rst_extension = rst_extension |
---|
70 | self.package_skip_patterns = package_skip_patterns |
---|
71 | self.module_skip_patterns = module_skip_patterns |
---|
72 | |
---|
73 | def get_package_name(self): |
---|
74 | return self._package_name |
---|
75 | |
---|
76 | def set_package_name(self, package_name): |
---|
77 | ''' Set package_name |
---|
78 | |
---|
79 | >>> docwriter = ApiDocWriter('sphinx') |
---|
80 | >>> import sphinx |
---|
81 | >>> docwriter.root_path == sphinx.__path__[0] |
---|
82 | True |
---|
83 | >>> docwriter.package_name = 'docutils' |
---|
84 | >>> import docutils |
---|
85 | >>> docwriter.root_path == docutils.__path__[0] |
---|
86 | True |
---|
87 | ''' |
---|
88 | # It's also possible to imagine caching the module parsing here |
---|
89 | self._package_name = package_name |
---|
90 | self.root_module = __import__(package_name) |
---|
91 | self.root_path = self.root_module.__path__[0] |
---|
92 | self.written_modules = None |
---|
93 | |
---|
94 | package_name = property(get_package_name, set_package_name, None, |
---|
95 | 'get/set package_name') |
---|
96 | |
---|
97 | def _get_object_name(self, line): |
---|
98 | ''' Get second token in line |
---|
99 | >>> docwriter = ApiDocWriter('sphinx') |
---|
100 | >>> docwriter._get_object_name(" def func(): ") |
---|
101 | 'func' |
---|
102 | >>> docwriter._get_object_name(" class Klass(object): ") |
---|
103 | 'Klass' |
---|
104 | >>> docwriter._get_object_name(" class Klass: ") |
---|
105 | 'Klass' |
---|
106 | ''' |
---|
107 | name = line.split()[1].split('(')[0].strip() |
---|
108 | # in case we have classes which are not derived from object |
---|
109 | # ie. old style classes |
---|
110 | return name.rstrip(':') |
---|
111 | |
---|
112 | def _uri2path(self, uri): |
---|
113 | ''' Convert uri to absolute filepath |
---|
114 | |
---|
115 | Parameters |
---|
116 | ---------- |
---|
117 | uri : string |
---|
118 | URI of python module to return path for |
---|
119 | |
---|
120 | Returns |
---|
121 | ------- |
---|
122 | path : None or string |
---|
123 | Returns None if there is no valid path for this URI |
---|
124 | Otherwise returns absolute file system path for URI |
---|
125 | |
---|
126 | Examples |
---|
127 | -------- |
---|
128 | >>> docwriter = ApiDocWriter('sphinx') |
---|
129 | >>> import sphinx |
---|
130 | >>> modpath = sphinx.__path__[0] |
---|
131 | >>> res = docwriter._uri2path('sphinx.builder') |
---|
132 | >>> res == os.path.join(modpath, 'builder.py') |
---|
133 | True |
---|
134 | >>> res = docwriter._uri2path('sphinx') |
---|
135 | >>> res == os.path.join(modpath, '__init__.py') |
---|
136 | True |
---|
137 | >>> docwriter._uri2path('sphinx.does_not_exist') |
---|
138 | |
---|
139 | ''' |
---|
140 | if uri == self.package_name: |
---|
141 | return os.path.join(self.root_path, '__init__.py') |
---|
142 | path = uri.replace('.', os.path.sep) |
---|
143 | path = path.replace(self.package_name + os.path.sep, '') |
---|
144 | path = os.path.join(self.root_path, path) |
---|
145 | # XXX maybe check for extensions as well? |
---|
146 | if os.path.exists(path + '.py'): # file |
---|
147 | path += '.py' |
---|
148 | elif os.path.exists(os.path.join(path, '__init__.py')): |
---|
149 | path = os.path.join(path, '__init__.py') |
---|
150 | else: |
---|
151 | return None |
---|
152 | return path |
---|
153 | |
---|
154 | def _path2uri(self, dirpath): |
---|
155 | ''' Convert directory path to uri ''' |
---|
156 | relpath = dirpath.replace(self.root_path, self.package_name) |
---|
157 | if relpath.startswith(os.path.sep): |
---|
158 | relpath = relpath[1:] |
---|
159 | return relpath.replace(os.path.sep, '.') |
---|
160 | |
---|
161 | def _parse_module(self, uri): |
---|
162 | ''' Parse module defined in *uri* ''' |
---|
163 | filename = self._uri2path(uri) |
---|
164 | if filename is None: |
---|
165 | # nothing that we could handle here. |
---|
166 | return ([],[]) |
---|
167 | f = open(filename, 'rt') |
---|
168 | functions, classes = self._parse_lines(f) |
---|
169 | f.close() |
---|
170 | return functions, classes |
---|
171 | |
---|
172 | def _parse_lines(self, linesource): |
---|
173 | ''' Parse lines of text for functions and classes ''' |
---|
174 | functions = [] |
---|
175 | classes = [] |
---|
176 | for line in linesource: |
---|
177 | if line.startswith('def ') and line.count('('): |
---|
178 | # exclude private stuff |
---|
179 | name = self._get_object_name(line) |
---|
180 | if not name.startswith('_'): |
---|
181 | functions.append(name) |
---|
182 | elif line.startswith('class '): |
---|
183 | # exclude private stuff |
---|
184 | name = self._get_object_name(line) |
---|
185 | if not name.startswith('_'): |
---|
186 | classes.append(name) |
---|
187 | else: |
---|
188 | pass |
---|
189 | functions.sort() |
---|
190 | classes.sort() |
---|
191 | return functions, classes |
---|
192 | |
---|
193 | def generate_api_doc(self, uri): |
---|
194 | '''Make autodoc documentation template string for a module |
---|
195 | |
---|
196 | Parameters |
---|
197 | ---------- |
---|
198 | uri : string |
---|
199 | python location of module - e.g 'sphinx.builder' |
---|
200 | |
---|
201 | Returns |
---|
202 | ------- |
---|
203 | S : string |
---|
204 | Contents of API doc |
---|
205 | ''' |
---|
206 | # get the names of all classes and functions |
---|
207 | functions, classes = self._parse_module(uri) |
---|
208 | if not len(functions) and not len(classes): |
---|
209 | print 'WARNING: Empty -',uri # dbg |
---|
210 | return '' |
---|
211 | |
---|
212 | # Make a shorter version of the uri that omits the package name for |
---|
213 | # titles |
---|
214 | uri_short = re.sub(r'^%s\.' % self.package_name,'',uri) |
---|
215 | |
---|
216 | ad = '.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n' |
---|
217 | |
---|
218 | chap_title = uri_short |
---|
219 | ad += (chap_title+'\n'+ self.rst_section_levels[1] * len(chap_title) |
---|
220 | + '\n\n') |
---|
221 | |
---|
222 | # Set the chapter title to read 'module' for all modules except for the |
---|
223 | # main packages |
---|
224 | if '.' in uri: |
---|
225 | title = 'Module: :mod:`' + uri_short + '`' |
---|
226 | else: |
---|
227 | title = ':mod:`' + uri_short + '`' |
---|
228 | ad += title + '\n' + self.rst_section_levels[2] * len(title) |
---|
229 | |
---|
230 | if len(classes): |
---|
231 | ad += '\nInheritance diagram for ``%s``:\n\n' % uri |
---|
232 | ad += '.. inheritance-diagram:: %s \n' % uri |
---|
233 | ad += ' :parts: 3\n' |
---|
234 | |
---|
235 | ad += '\n.. automodule:: ' + uri + '\n' |
---|
236 | ad += '\n.. currentmodule:: ' + uri + '\n' |
---|
237 | multi_class = len(classes) > 1 |
---|
238 | multi_fx = len(functions) > 1 |
---|
239 | if multi_class: |
---|
240 | ad += '\n' + 'Classes' + '\n' + \ |
---|
241 | self.rst_section_levels[2] * 7 + '\n' |
---|
242 | elif len(classes) and multi_fx: |
---|
243 | ad += '\n' + 'Class' + '\n' + \ |
---|
244 | self.rst_section_levels[2] * 5 + '\n' |
---|
245 | for c in classes: |
---|
246 | ad += '\n:class:`' + c + '`\n' \ |
---|
247 | + self.rst_section_levels[multi_class + 2 ] * \ |
---|
248 | (len(c)+9) + '\n\n' |
---|
249 | ad += '\n.. autoclass:: ' + c + '\n' |
---|
250 | # must NOT exclude from index to keep cross-refs working |
---|
251 | ad += ' :members:\n' \ |
---|
252 | ' :undoc-members:\n' \ |
---|
253 | ' :show-inheritance:\n' \ |
---|
254 | ' :inherited-members:\n' \ |
---|
255 | '\n' \ |
---|
256 | ' .. automethod:: __init__\n' |
---|
257 | if multi_fx: |
---|
258 | ad += '\n' + 'Functions' + '\n' + \ |
---|
259 | self.rst_section_levels[2] * 9 + '\n\n' |
---|
260 | elif len(functions) and multi_class: |
---|
261 | ad += '\n' + 'Function' + '\n' + \ |
---|
262 | self.rst_section_levels[2] * 8 + '\n\n' |
---|
263 | for f in functions: |
---|
264 | # must NOT exclude from index to keep cross-refs working |
---|
265 | ad += '\n.. autofunction:: ' + uri + '.' + f + '\n\n' |
---|
266 | return ad |
---|
267 | |
---|
268 | def _survives_exclude(self, matchstr, match_type): |
---|
269 | ''' Returns True if *matchstr* does not match patterns |
---|
270 | |
---|
271 | ``self.package_name`` removed from front of string if present |
---|
272 | |
---|
273 | Examples |
---|
274 | -------- |
---|
275 | >>> dw = ApiDocWriter('sphinx') |
---|
276 | >>> dw._survives_exclude('sphinx.okpkg', 'package') |
---|
277 | True |
---|
278 | >>> dw.package_skip_patterns.append('^\\.badpkg$') |
---|
279 | >>> dw._survives_exclude('sphinx.badpkg', 'package') |
---|
280 | False |
---|
281 | >>> dw._survives_exclude('sphinx.badpkg', 'module') |
---|
282 | True |
---|
283 | >>> dw._survives_exclude('sphinx.badmod', 'module') |
---|
284 | True |
---|
285 | >>> dw.module_skip_patterns.append('^\\.badmod$') |
---|
286 | >>> dw._survives_exclude('sphinx.badmod', 'module') |
---|
287 | False |
---|
288 | ''' |
---|
289 | if match_type == 'module': |
---|
290 | patterns = self.module_skip_patterns |
---|
291 | elif match_type == 'package': |
---|
292 | patterns = self.package_skip_patterns |
---|
293 | else: |
---|
294 | raise ValueError('Cannot interpret match type "%s"' |
---|
295 | % match_type) |
---|
296 | # Match to URI without package name |
---|
297 | L = len(self.package_name) |
---|
298 | if matchstr[:L] == self.package_name: |
---|
299 | matchstr = matchstr[L:] |
---|
300 | for pat in patterns: |
---|
301 | try: |
---|
302 | pat.search |
---|
303 | except AttributeError: |
---|
304 | pat = re.compile(pat) |
---|
305 | if pat.search(matchstr): |
---|
306 | return False |
---|
307 | return True |
---|
308 | |
---|
309 | def discover_modules(self): |
---|
310 | ''' Return module sequence discovered from ``self.package_name`` |
---|
311 | |
---|
312 | |
---|
313 | Parameters |
---|
314 | ---------- |
---|
315 | None |
---|
316 | |
---|
317 | Returns |
---|
318 | ------- |
---|
319 | mods : sequence |
---|
320 | Sequence of module names within ``self.package_name`` |
---|
321 | |
---|
322 | Examples |
---|
323 | -------- |
---|
324 | >>> dw = ApiDocWriter('sphinx') |
---|
325 | >>> mods = dw.discover_modules() |
---|
326 | >>> 'sphinx.util' in mods |
---|
327 | True |
---|
328 | >>> dw.package_skip_patterns.append('\.util$') |
---|
329 | >>> 'sphinx.util' in dw.discover_modules() |
---|
330 | False |
---|
331 | >>> |
---|
332 | ''' |
---|
333 | modules = [self.package_name] |
---|
334 | # raw directory parsing |
---|
335 | for dirpath, dirnames, filenames in os.walk(self.root_path): |
---|
336 | # Check directory names for packages |
---|
337 | root_uri = self._path2uri(os.path.join(self.root_path, |
---|
338 | dirpath)) |
---|
339 | for dirname in dirnames[:]: # copy list - we modify inplace |
---|
340 | package_uri = '.'.join((root_uri, dirname)) |
---|
341 | if (self._uri2path(package_uri) and |
---|
342 | self._survives_exclude(package_uri, 'package')): |
---|
343 | modules.append(package_uri) |
---|
344 | else: |
---|
345 | dirnames.remove(dirname) |
---|
346 | # Check filenames for modules |
---|
347 | for filename in filenames: |
---|
348 | module_name = filename[:-3] |
---|
349 | module_uri = '.'.join((root_uri, module_name)) |
---|
350 | if (self._uri2path(module_uri) and |
---|
351 | self._survives_exclude(module_uri, 'module')): |
---|
352 | modules.append(module_uri) |
---|
353 | return sorted(modules) |
---|
354 | |
---|
355 | def write_modules_api(self, modules,outdir): |
---|
356 | # write the list |
---|
357 | written_modules = [] |
---|
358 | for m in modules: |
---|
359 | api_str = self.generate_api_doc(m) |
---|
360 | if not api_str: |
---|
361 | continue |
---|
362 | # write out to file |
---|
363 | outfile = os.path.join(outdir, |
---|
364 | m + self.rst_extension) |
---|
365 | fileobj = open(outfile, 'wt') |
---|
366 | fileobj.write(api_str) |
---|
367 | fileobj.close() |
---|
368 | written_modules.append(m) |
---|
369 | self.written_modules = written_modules |
---|
370 | |
---|
371 | def write_api_docs(self, outdir): |
---|
372 | """Generate API reST files. |
---|
373 | |
---|
374 | Parameters |
---|
375 | ---------- |
---|
376 | outdir : string |
---|
377 | Directory name in which to store files |
---|
378 | We create automatic filenames for each module |
---|
379 | |
---|
380 | Returns |
---|
381 | ------- |
---|
382 | None |
---|
383 | |
---|
384 | Notes |
---|
385 | ----- |
---|
386 | Sets self.written_modules to list of written modules |
---|
387 | """ |
---|
388 | if not os.path.exists(outdir): |
---|
389 | os.mkdir(outdir) |
---|
390 | # compose list of modules |
---|
391 | modules = self.discover_modules() |
---|
392 | self.write_modules_api(modules,outdir) |
---|
393 | |
---|
394 | def write_index(self, outdir, froot='gen', relative_to=None): |
---|
395 | """Make a reST API index file from written files |
---|
396 | |
---|
397 | Parameters |
---|
398 | ---------- |
---|
399 | path : string |
---|
400 | Filename to write index to |
---|
401 | outdir : string |
---|
402 | Directory to which to write generated index file |
---|
403 | froot : string, optional |
---|
404 | root (filename without extension) of filename to write to |
---|
405 | Defaults to 'gen'. We add ``self.rst_extension``. |
---|
406 | relative_to : string |
---|
407 | path to which written filenames are relative. This |
---|
408 | component of the written file path will be removed from |
---|
409 | outdir, in the generated index. Default is None, meaning, |
---|
410 | leave path as it is. |
---|
411 | """ |
---|
412 | if self.written_modules is None: |
---|
413 | raise ValueError('No modules written') |
---|
414 | # Get full filename path |
---|
415 | path = os.path.join(outdir, froot+self.rst_extension) |
---|
416 | # Path written into index is relative to rootpath |
---|
417 | if relative_to is not None: |
---|
418 | relpath = outdir.replace(relative_to + os.path.sep, '') |
---|
419 | else: |
---|
420 | relpath = outdir |
---|
421 | idx = open(path,'wt') |
---|
422 | w = idx.write |
---|
423 | w('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n') |
---|
424 | w('.. toctree::\n\n') |
---|
425 | for f in self.written_modules: |
---|
426 | w(' %s\n' % os.path.join(relpath,f)) |
---|
427 | idx.close() |
---|