Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 501 lines (411 sloc) 15.301 kB
4e2cf07 @scopatz Added some sphinx extentions from numpy and ipython.
scopatz authored
1 """Extract reference documentation from the NumPy source tree.
2
3 """
4
5 import inspect
6 import textwrap
7 import re
8 import pydoc
9 from StringIO import StringIO
10 from warnings import warn
11
12 class Reader(object):
13 """A line-based string reader.
14
15 """
16 def __init__(self, data):
17 """
18 Parameters
19 ----------
20 data : str
21 String with lines separated by '\n'.
22
23 """
24 if isinstance(data,list):
25 self._str = data
26 else:
27 self._str = data.split('\n') # store string as list of lines
28
29 self.reset()
30
31 def __getitem__(self, n):
32 return self._str[n]
33
34 def reset(self):
35 self._l = 0 # current line nr
36
37 def read(self):
38 if not self.eof():
39 out = self[self._l]
40 self._l += 1
41 return out
42 else:
43 return ''
44
45 def seek_next_non_empty_line(self):
46 for l in self[self._l:]:
47 if l.strip():
48 break
49 else:
50 self._l += 1
51
52 def eof(self):
53 return self._l >= len(self._str)
54
55 def read_to_condition(self, condition_func):
56 start = self._l
57 for line in self[start:]:
58 if condition_func(line):
59 return self[start:self._l]
60 self._l += 1
61 if self.eof():
62 return self[start:self._l+1]
63 return []
64
65 def read_to_next_empty_line(self):
66 self.seek_next_non_empty_line()
67 def is_empty(line):
68 return not line.strip()
69 return self.read_to_condition(is_empty)
70
71 def read_to_next_unindented_line(self):
72 def is_unindented(line):
73 return (line.strip() and (len(line.lstrip()) == len(line)))
74 return self.read_to_condition(is_unindented)
75
76 def peek(self,n=0):
77 if self._l + n < len(self._str):
78 return self[self._l + n]
79 else:
80 return ''
81
82 def is_empty(self):
83 return not ''.join(self._str).strip()
84
85
86 class NumpyDocString(object):
87 def __init__(self, docstring, config={}):
88 docstring = textwrap.dedent(docstring).split('\n')
89
90 self._doc = Reader(docstring)
91 self._parsed_data = {
92 'Signature': '',
93 'Summary': [''],
94 'Extended Summary': [],
95 'Parameters': [],
96 'Returns': [],
97 'Raises': [],
98 'Warns': [],
99 'Other Parameters': [],
100 'Attributes': [],
101 'Methods': [],
102 'See Also': [],
103 'Notes': [],
104 'Warnings': [],
105 'References': '',
106 'Examples': '',
107 'index': {}
108 }
109
110 self._parse()
111
112 def __getitem__(self,key):
113 return self._parsed_data[key]
114
115 def __setitem__(self,key,val):
116 if not self._parsed_data.has_key(key):
117 warn("Unknown section %s" % key)
118 else:
119 self._parsed_data[key] = val
120
121 def _is_at_section(self):
122 self._doc.seek_next_non_empty_line()
123
124 if self._doc.eof():
125 return False
126
127 l1 = self._doc.peek().strip() # e.g. Parameters
128
129 if l1.startswith('.. index::'):
130 return True
131
132 l2 = self._doc.peek(1).strip() # ---------- or ==========
133 return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1))
134
135 def _strip(self,doc):
136 i = 0
137 j = 0
138 for i,line in enumerate(doc):
139 if line.strip(): break
140
141 for j,line in enumerate(doc[::-1]):
142 if line.strip(): break
143
144 return doc[i:len(doc)-j]
145
146 def _read_to_next_section(self):
147 section = self._doc.read_to_next_empty_line()
148
149 while not self._is_at_section() and not self._doc.eof():
150 if not self._doc.peek(-1).strip(): # previous line was empty
151 section += ['']
152
153 section += self._doc.read_to_next_empty_line()
154
155 return section
156
157 def _read_sections(self):
158 while not self._doc.eof():
159 data = self._read_to_next_section()
160 name = data[0].strip()
161
162 if name.startswith('..'): # index section
163 yield name, data[1:]
164 elif len(data) < 2:
165 yield StopIteration
166 else:
167 yield name, self._strip(data[2:])
168
169 def _parse_param_list(self,content):
170 r = Reader(content)
171 params = []
172 while not r.eof():
173 header = r.read().strip()
174 if ' : ' in header:
175 arg_name, arg_type = header.split(' : ')[:2]
176 else:
177 arg_name, arg_type = header, ''
178
179 desc = r.read_to_next_unindented_line()
180 desc = dedent_lines(desc)
181
182 params.append((arg_name,arg_type,desc))
183
184 return params
185
186
187 _name_rgx = re.compile(r"^\s*(:(?P<role>\w+):`(?P<name>[a-zA-Z0-9_.-]+)`|"
188 r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X)
189 def _parse_see_also(self, content):
190 """
191 func_name : Descriptive text
192 continued text
193 another_func_name : Descriptive text
194 func_name1, func_name2, :meth:`func_name`, func_name3
195
196 """
197 items = []
198
199 def parse_item_name(text):
200 """Match ':role:`name`' or 'name'"""
201 m = self._name_rgx.match(text)
202 if m:
203 g = m.groups()
204 if g[1] is None:
205 return g[3], None
206 else:
207 return g[2], g[1]
208 raise ValueError("%s is not a item name" % text)
209
210 def push_item(name, rest):
211 if not name:
212 return
213 name, role = parse_item_name(name)
214 items.append((name, list(rest), role))
215 del rest[:]
216
217 current_func = None
218 rest = []
219
220 for line in content:
221 if not line.strip(): continue
222
223 m = self._name_rgx.match(line)
224 if m and line[m.end():].strip().startswith(':'):
225 push_item(current_func, rest)
226 current_func, line = line[:m.end()], line[m.end():]
227 rest = [line.split(':', 1)[1].strip()]
228 if not rest[0]:
229 rest = []
230 elif not line.startswith(' '):
231 push_item(current_func, rest)
232 current_func = None
233 if ',' in line:
234 for func in line.split(','):
235 if func.strip():
236 push_item(func, [])
237 elif line.strip():
238 current_func = line
239 elif current_func is not None:
240 rest.append(line.strip())
241 push_item(current_func, rest)
242 return items
243
244 def _parse_index(self, section, content):
245 """
246 .. index: default
247 :refguide: something, else, and more
248
249 """
250 def strip_each_in(lst):
251 return [s.strip() for s in lst]
252
253 out = {}
254 section = section.split('::')
255 if len(section) > 1:
256 out['default'] = strip_each_in(section[1].split(','))[0]
257 for line in content:
258 line = line.split(':')
259 if len(line) > 2:
260 out[line[1]] = strip_each_in(line[2].split(','))
261 return out
262
263 def _parse_summary(self):
264 """Grab signature (if given) and summary"""
265 if self._is_at_section():
266 return
267
268 summary = self._doc.read_to_next_empty_line()
269 summary_str = " ".join([s.strip() for s in summary]).strip()
270 if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str):
271 self['Signature'] = summary_str
272 if not self._is_at_section():
273 self['Summary'] = self._doc.read_to_next_empty_line()
274 else:
275 self['Summary'] = summary
276
277 if not self._is_at_section():
278 self['Extended Summary'] = self._read_to_next_section()
279
280 def _parse(self):
281 self._doc.reset()
282 self._parse_summary()
283
284 for (section,content) in self._read_sections():
285 if not section.startswith('..'):
286 section = ' '.join([s.capitalize() for s in section.split(' ')])
287 if section in ('Parameters', 'Returns', 'Raises', 'Warns',
288 'Other Parameters', 'Attributes', 'Methods'):
289 self[section] = self._parse_param_list(content)
290 elif section.startswith('.. index::'):
291 self['index'] = self._parse_index(section, content)
292 elif section == 'See Also':
293 self['See Also'] = self._parse_see_also(content)
294 else:
295 self[section] = content
296
297 # string conversion routines
298
299 def _str_header(self, name, symbol='-'):
300 return [name, len(name)*symbol]
301
302 def _str_indent(self, doc, indent=4):
303 out = []
304 for line in doc:
305 out += [' '*indent + line]
306 return out
307
308 def _str_signature(self):
309 if self['Signature']:
310 return [self['Signature'].replace('*','\*')] + ['']
311 else:
312 return ['']
313
314 def _str_summary(self):
315 if self['Summary']:
316 return self['Summary'] + ['']
317 else:
318 return []
319
320 def _str_extended_summary(self):
321 if self['Extended Summary']:
322 return self['Extended Summary'] + ['']
323 else:
324 return []
325
326 def _str_param_list(self, name):
327 out = []
328 if self[name]:
329 out += self._str_header(name)
330 for param,param_type,desc in self[name]:
331 out += ['%s : %s' % (param, param_type)]
332 out += self._str_indent(desc)
333 out += ['']
334 return out
335
336 def _str_section(self, name):
337 out = []
338 if self[name]:
339 out += self._str_header(name)
340 out += self[name]
341 out += ['']
342 return out
343
344 def _str_see_also(self, func_role):
345 if not self['See Also']: return []
346 out = []
347 out += self._str_header("See Also")
348 last_had_desc = True
349 for func, desc, role in self['See Also']:
350 if role:
351 link = ':%s:`%s`' % (role, func)
352 elif func_role:
353 link = ':%s:`%s`' % (func_role, func)
354 else:
355 link = "`%s`_" % func
356 if desc or last_had_desc:
357 out += ['']
358 out += [link]
359 else:
360 out[-1] += ", %s" % link
361 if desc:
362 out += self._str_indent([' '.join(desc)])
363 last_had_desc = True
364 else:
365 last_had_desc = False
366 out += ['']
367 return out
368
369 def _str_index(self):
370 idx = self['index']
371 out = []
372 out += ['.. index:: %s' % idx.get('default','')]
373 for section, references in idx.iteritems():
374 if section == 'default':
375 continue
376 out += [' :%s: %s' % (section, ', '.join(references))]
377 return out
378
379 def __str__(self, func_role=''):
380 out = []
381 out += self._str_signature()
382 out += self._str_summary()
383 out += self._str_extended_summary()
384 for param_list in ('Parameters', 'Returns', 'Other Parameters',
385 'Raises', 'Warns'):
386 out += self._str_param_list(param_list)
387 out += self._str_section('Warnings')
388 out += self._str_see_also(func_role)
389 for s in ('Notes','References','Examples'):
390 out += self._str_section(s)
391 for param_list in ('Attributes', 'Methods'):
392 out += self._str_param_list(param_list)
393 out += self._str_index()
394 return '\n'.join(out)
395
396
397 def indent(str,indent=4):
398 indent_str = ' '*indent
399 if str is None:
400 return indent_str
401 lines = str.split('\n')
402 return '\n'.join(indent_str + l for l in lines)
403
404 def dedent_lines(lines):
405 """Deindent a list of lines maximally"""
406 return textwrap.dedent("\n".join(lines)).split("\n")
407
408 def header(text, style='-'):
409 return text + '\n' + style*len(text) + '\n'
410
411
412 class FunctionDoc(NumpyDocString):
413 def __init__(self, func, role='func', doc=None, config={}):
414 self._f = func
415 self._role = role # e.g. "func" or "meth"
416
417 if doc is None:
418 if func is None:
419 raise ValueError("No function or docstring given")
420 doc = inspect.getdoc(func) or ''
421 NumpyDocString.__init__(self, doc)
422
423 if not self['Signature'] and func is not None:
424 func, func_name = self.get_func()
425 try:
426 # try to read signature
427 argspec = inspect.getargspec(func)
428 argspec = inspect.formatargspec(*argspec)
429 argspec = argspec.replace('*','\*')
430 signature = '%s%s' % (func_name, argspec)
431 except TypeError, e:
432 signature = '%s()' % func_name
433 self['Signature'] = signature
434
435 def get_func(self):
436 func_name = getattr(self._f, '__name__', self.__class__.__name__)
437 if inspect.isclass(self._f):
438 func = getattr(self._f, '__call__', self._f.__init__)
439 else:
440 func = self._f
441 return func, func_name
442
443 def __str__(self):
444 out = ''
445
446 func, func_name = self.get_func()
447 signature = self['Signature'].replace('*', '\*')
448
449 roles = {'func': 'function',
450 'meth': 'method'}
451
452 if self._role:
453 if not roles.has_key(self._role):
454 print "Warning: invalid role %s" % self._role
455 out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''),
456 func_name)
457
458 out += super(FunctionDoc, self).__str__(func_role=self._role)
459 return out
460
461
462 class ClassDoc(NumpyDocString):
463 def __init__(self, cls, doc=None, modulename='', func_doc=FunctionDoc,
464 config={}):
465 if not inspect.isclass(cls) and cls is not None:
466 raise ValueError("Expected a class or None, but got %r" % cls)
467 self._cls = cls
468
469 if modulename and not modulename.endswith('.'):
470 modulename += '.'
471 self._mod = modulename
472
473 if doc is None:
474 if cls is None:
475 raise ValueError("No class or documentation string given")
476 doc = pydoc.getdoc(cls)
477
478 NumpyDocString.__init__(self, doc)
479
480 if config.get('show_class_members', True):
481 if not self['Methods']:
482 self['Methods'] = [(name, '', '')
483 for name in sorted(self.methods)]
484 if not self['Attributes']:
485 self['Attributes'] = [(name, '', '')
486 for name in sorted(self.properties)]
487
488 @property
489 def methods(self):
490 if self._cls is None:
491 return []
492 return [name for name,func in inspect.getmembers(self._cls)
493 if not name.startswith('_') and callable(func)]
494
495 @property
496 def properties(self):
497 if self._cls is None:
498 return []
499 return [name for name,func in inspect.getmembers(self._cls)
500 if not name.startswith('_') and func is None]
Something went wrong with that request. Please try again.