##// END OF EJS Templates
Add file for automatic API doc generation....
Fernando Perez -
Show More
@@ -0,0 +1,426 b''
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 '\n' \
255 ' .. automethod:: __init__\n'
256 if multi_fx:
257 ad += '\n' + 'Functions' + '\n' + \
258 self.rst_section_levels[2] * 9 + '\n\n'
259 elif len(functions) and multi_class:
260 ad += '\n' + 'Function' + '\n' + \
261 self.rst_section_levels[2] * 8 + '\n\n'
262 for f in functions:
263 # must NOT exclude from index to keep cross-refs working
264 ad += '\n.. autofunction:: ' + uri + '.' + f + '\n\n'
265 return ad
266
267 def _survives_exclude(self, matchstr, match_type):
268 ''' Returns True if *matchstr* does not match patterns
269
270 ``self.package_name`` removed from front of string if present
271
272 Examples
273 --------
274 >>> dw = ApiDocWriter('sphinx')
275 >>> dw._survives_exclude('sphinx.okpkg', 'package')
276 True
277 >>> dw.package_skip_patterns.append('^\\.badpkg$')
278 >>> dw._survives_exclude('sphinx.badpkg', 'package')
279 False
280 >>> dw._survives_exclude('sphinx.badpkg', 'module')
281 True
282 >>> dw._survives_exclude('sphinx.badmod', 'module')
283 True
284 >>> dw.module_skip_patterns.append('^\\.badmod$')
285 >>> dw._survives_exclude('sphinx.badmod', 'module')
286 False
287 '''
288 if match_type == 'module':
289 patterns = self.module_skip_patterns
290 elif match_type == 'package':
291 patterns = self.package_skip_patterns
292 else:
293 raise ValueError('Cannot interpret match type "%s"'
294 % match_type)
295 # Match to URI without package name
296 L = len(self.package_name)
297 if matchstr[:L] == self.package_name:
298 matchstr = matchstr[L:]
299 for pat in patterns:
300 try:
301 pat.search
302 except AttributeError:
303 pat = re.compile(pat)
304 if pat.search(matchstr):
305 return False
306 return True
307
308 def discover_modules(self):
309 ''' Return module sequence discovered from ``self.package_name``
310
311
312 Parameters
313 ----------
314 None
315
316 Returns
317 -------
318 mods : sequence
319 Sequence of module names within ``self.package_name``
320
321 Examples
322 --------
323 >>> dw = ApiDocWriter('sphinx')
324 >>> mods = dw.discover_modules()
325 >>> 'sphinx.util' in mods
326 True
327 >>> dw.package_skip_patterns.append('\.util$')
328 >>> 'sphinx.util' in dw.discover_modules()
329 False
330 >>>
331 '''
332 modules = [self.package_name]
333 # raw directory parsing
334 for dirpath, dirnames, filenames in os.walk(self.root_path):
335 # Check directory names for packages
336 root_uri = self._path2uri(os.path.join(self.root_path,
337 dirpath))
338 for dirname in dirnames[:]: # copy list - we modify inplace
339 package_uri = '.'.join((root_uri, dirname))
340 if (self._uri2path(package_uri) and
341 self._survives_exclude(package_uri, 'package')):
342 modules.append(package_uri)
343 else:
344 dirnames.remove(dirname)
345 # Check filenames for modules
346 for filename in filenames:
347 module_name = filename[:-3]
348 module_uri = '.'.join((root_uri, module_name))
349 if (self._uri2path(module_uri) and
350 self._survives_exclude(module_uri, 'module')):
351 modules.append(module_uri)
352 return sorted(modules)
353
354 def write_modules_api(self, modules,outdir):
355 # write the list
356 written_modules = []
357 for m in modules:
358 api_str = self.generate_api_doc(m)
359 if not api_str:
360 continue
361 # write out to file
362 outfile = os.path.join(outdir,
363 m + self.rst_extension)
364 fileobj = open(outfile, 'wt')
365 fileobj.write(api_str)
366 fileobj.close()
367 written_modules.append(m)
368 self.written_modules = written_modules
369
370 def write_api_docs(self, outdir):
371 """Generate API reST files.
372
373 Parameters
374 ----------
375 outdir : string
376 Directory name in which to store files
377 We create automatic filenames for each module
378
379 Returns
380 -------
381 None
382
383 Notes
384 -----
385 Sets self.written_modules to list of written modules
386 """
387 if not os.path.exists(outdir):
388 os.mkdir(outdir)
389 # compose list of modules
390 modules = self.discover_modules()
391 self.write_modules_api(modules,outdir)
392
393 def write_index(self, outdir, froot='gen', relative_to=None):
394 """Make a reST API index file from written files
395
396 Parameters
397 ----------
398 path : string
399 Filename to write index to
400 outdir : string
401 Directory to which to write generated index file
402 froot : string, optional
403 root (filename without extension) of filename to write to
404 Defaults to 'gen'. We add ``self.rst_extension``.
405 relative_to : string
406 path to which written filenames are relative. This
407 component of the written file path will be removed from
408 outdir, in the generated index. Default is None, meaning,
409 leave path as it is.
410 """
411 if self.written_modules is None:
412 raise ValueError('No modules written')
413 # Get full filename path
414 path = os.path.join(outdir, froot+self.rst_extension)
415 # Path written into index is relative to rootpath
416 if relative_to is not None:
417 relpath = outdir.replace(relative_to + os.path.sep, '')
418 else:
419 relpath = outdir
420 idx = open(path,'wt')
421 w = idx.write
422 w('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n')
423 w('.. toctree::\n\n')
424 for f in self.written_modules:
425 w(' %s\n' % os.path.join(relpath,f))
426 idx.close()
General Comments 0
You need to be logged in to leave comments. Login now