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