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 | 4 |
---|
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): |
---|
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 | push_item(func, []) |
---|
236 | elif line.strip(): |
---|
237 | current_func = line |
---|
238 | elif current_func is not None: |
---|
239 | rest.append(line.strip()) |
---|
240 | push_item(current_func, rest) |
---|
241 | return items |
---|
242 | |
---|
243 | def _parse_index(self, section, content): |
---|
244 | """ |
---|
245 | .. index: default |
---|
246 | :refguide: something, else, and more |
---|
247 | |
---|
248 | """ |
---|
249 | def strip_each_in(lst): |
---|
250 | return [s.strip() for s in lst] |
---|
251 | |
---|
252 | out = {} |
---|
253 | section = section.split('::') |
---|
254 | if len(section) > 1: |
---|
255 | out['default'] = strip_each_in(section[1].split(','))[0] |
---|
256 | for line in content: |
---|
257 | line = line.split(':') |
---|
258 | if len(line) > 2: |
---|
259 | out[line[1]] = strip_each_in(line[2].split(',')) |
---|
260 | return out |
---|
261 | |
---|
262 | def _parse_summary(self): |
---|
263 | """Grab signature (if given) and summary""" |
---|
264 | if self._is_at_section(): |
---|
265 | return |
---|
266 | |
---|
267 | summary = self._doc.read_to_next_empty_line() |
---|
268 | summary_str = " ".join([s.strip() for s in summary]).strip() |
---|
269 | if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str): |
---|
270 | self['Signature'] = summary_str |
---|
271 | if not self._is_at_section(): |
---|
272 | self['Summary'] = self._doc.read_to_next_empty_line() |
---|
273 | else: |
---|
274 | self['Summary'] = summary |
---|
275 | |
---|
276 | if not self._is_at_section(): |
---|
277 | self['Extended Summary'] = self._read_to_next_section() |
---|
278 | |
---|
279 | def _parse(self): |
---|
280 | self._doc.reset() |
---|
281 | self._parse_summary() |
---|
282 | |
---|
283 | for (section,content) in self._read_sections(): |
---|
284 | if not section.startswith('..'): |
---|
285 | section = ' '.join([s.capitalize() for s in section.split(' ')]) |
---|
286 | if section in ('Parameters', 'Attributes', 'Methods', |
---|
287 | 'Returns', 'Raises', 'Warns'): |
---|
288 | self[section] = self._parse_param_list(content) |
---|
289 | elif section.startswith('.. index::'): |
---|
290 | self['index'] = self._parse_index(section, content) |
---|
291 | elif section == 'See Also': |
---|
292 | self['See Also'] = self._parse_see_also(content) |
---|
293 | else: |
---|
294 | self[section] = content |
---|
295 | |
---|
296 | # string conversion routines |
---|
297 | |
---|
298 | def _str_header(self, name, symbol='-'): |
---|
299 | return [name, len(name)*symbol] |
---|
300 | |
---|
301 | def _str_indent(self, doc, indent=4): |
---|
302 | out = [] |
---|
303 | for line in doc: |
---|
304 | out += [' '*indent + line] |
---|
305 | return out |
---|
306 | |
---|
307 | def _str_signature(self): |
---|
308 | if self['Signature']: |
---|
309 | return [self['Signature'].replace('*','\*')] + [''] |
---|
310 | else: |
---|
311 | return [''] |
---|
312 | |
---|
313 | def _str_summary(self): |
---|
314 | if self['Summary']: |
---|
315 | return self['Summary'] + [''] |
---|
316 | else: |
---|
317 | return [] |
---|
318 | |
---|
319 | def _str_extended_summary(self): |
---|
320 | if self['Extended Summary']: |
---|
321 | return self['Extended Summary'] + [''] |
---|
322 | else: |
---|
323 | return [] |
---|
324 | |
---|
325 | def _str_param_list(self, name): |
---|
326 | out = [] |
---|
327 | if self[name]: |
---|
328 | out += self._str_header(name) |
---|
329 | for param,param_type,desc in self[name]: |
---|
330 | out += ['%s : %s' % (param, param_type)] |
---|
331 | out += self._str_indent(desc) |
---|
332 | out += [''] |
---|
333 | return out |
---|
334 | |
---|
335 | def _str_section(self, name): |
---|
336 | out = [] |
---|
337 | if self[name]: |
---|
338 | out += self._str_header(name) |
---|
339 | out += self[name] |
---|
340 | out += [''] |
---|
341 | return out |
---|
342 | |
---|
343 | def _str_see_also(self, func_role): |
---|
344 | if not self['See Also']: return [] |
---|
345 | out = [] |
---|
346 | out += self._str_header("See Also") |
---|
347 | last_had_desc = True |
---|
348 | for func, desc, role in self['See Also']: |
---|
349 | if role: |
---|
350 | link = ':%s:`%s`' % (role, func) |
---|
351 | elif func_role: |
---|
352 | link = ':%s:`%s`' % (func_role, func) |
---|
353 | else: |
---|
354 | link = "`%s`_" % func |
---|
355 | if desc or last_had_desc: |
---|
356 | out += [''] |
---|
357 | out += [link] |
---|
358 | else: |
---|
359 | out[-1] += ", %s" % link |
---|
360 | if desc: |
---|
361 | out += self._str_indent([' '.join(desc)]) |
---|
362 | last_had_desc = True |
---|
363 | else: |
---|
364 | last_had_desc = False |
---|
365 | out += [''] |
---|
366 | return out |
---|
367 | |
---|
368 | def _str_index(self): |
---|
369 | idx = self['index'] |
---|
370 | out = [] |
---|
371 | out += ['.. index:: %s' % idx.get('default','')] |
---|
372 | for section, references in idx.iteritems(): |
---|
373 | if section == 'default': |
---|
374 | continue |
---|
375 | out += [' :%s: %s' % (section, ', '.join(references))] |
---|
376 | return out |
---|
377 | |
---|
378 | def __str__(self, func_role=''): |
---|
379 | out = [] |
---|
380 | out += self._str_signature() |
---|
381 | out += self._str_summary() |
---|
382 | out += self._str_extended_summary() |
---|
383 | for param_list in ('Parameters','Returns','Raises'): |
---|
384 | out += self._str_param_list(param_list) |
---|
385 | out += self._str_section('Warnings') |
---|
386 | out += self._str_see_also(func_role) |
---|
387 | for s in ('Notes','References','Examples'): |
---|
388 | out += self._str_section(s) |
---|
389 | out += self._str_index() |
---|
390 | return '\n'.join(out) |
---|
391 | |
---|
392 | |
---|
393 | def indent(str,indent=4): |
---|
394 | indent_str = ' '*indent |
---|
395 | if str is None: |
---|
396 | return indent_str |
---|
397 | lines = str.split('\n') |
---|
398 | return '\n'.join(indent_str + l for l in lines) |
---|
399 | |
---|
400 | def dedent_lines(lines): |
---|
401 | """Deindent a list of lines maximally""" |
---|
402 | return textwrap.dedent("\n".join(lines)).split("\n") |
---|
403 | |
---|
404 | def header(text, style='-'): |
---|
405 | return text + '\n' + style*len(text) + '\n' |
---|
406 | |
---|
407 | |
---|
408 | class FunctionDoc(NumpyDocString): |
---|
409 | def __init__(self, func, role='func', doc=None): |
---|
410 | self._f = func |
---|
411 | self._role = role # e.g. "func" or "meth" |
---|
412 | if doc is None: |
---|
413 | doc = inspect.getdoc(func) or '' |
---|
414 | try: |
---|
415 | NumpyDocString.__init__(self, doc) |
---|
416 | except ValueError, e: |
---|
417 | print '*'*78 |
---|
418 | print "ERROR: '%s' while parsing `%s`" % (e, self._f) |
---|
419 | print '*'*78 |
---|
420 | #print "Docstring follows:" |
---|
421 | #print doclines |
---|
422 | #print '='*78 |
---|
423 | |
---|
424 | if not self['Signature']: |
---|
425 | func, func_name = self.get_func() |
---|
426 | try: |
---|
427 | # try to read signature |
---|
428 | argspec = inspect.getargspec(func) |
---|
429 | argspec = inspect.formatargspec(*argspec) |
---|
430 | argspec = argspec.replace('*','\*') |
---|
431 | signature = '%s%s' % (func_name, argspec) |
---|
432 | except TypeError, e: |
---|
433 | signature = '%s()' % func_name |
---|
434 | self['Signature'] = signature |
---|
435 | |
---|
436 | def get_func(self): |
---|
437 | func_name = getattr(self._f, '__name__', self.__class__.__name__) |
---|
438 | if inspect.isclass(self._f): |
---|
439 | func = getattr(self._f, '__call__', self._f.__init__) |
---|
440 | else: |
---|
441 | func = self._f |
---|
442 | return func, func_name |
---|
443 | |
---|
444 | def __str__(self): |
---|
445 | out = '' |
---|
446 | |
---|
447 | func, func_name = self.get_func() |
---|
448 | signature = self['Signature'].replace('*', '\*') |
---|
449 | |
---|
450 | roles = {'func': 'function', |
---|
451 | 'meth': 'method'} |
---|
452 | |
---|
453 | if self._role: |
---|
454 | if not roles.has_key(self._role): |
---|
455 | print "Warning: invalid role %s" % self._role |
---|
456 | out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''), |
---|
457 | func_name) |
---|
458 | |
---|
459 | out += super(FunctionDoc, self).__str__(func_role=self._role) |
---|
460 | return out |
---|
461 | |
---|
462 | |
---|
463 | class ClassDoc(NumpyDocString): |
---|
464 | def __init__(self,cls,modulename='',func_doc=FunctionDoc,doc=None): |
---|
465 | if not inspect.isclass(cls): |
---|
466 | raise ValueError("Initialise using a class. Got %r" % cls) |
---|
467 | self._cls = cls |
---|
468 | |
---|
469 | if modulename and not modulename.endswith('.'): |
---|
470 | modulename += '.' |
---|
471 | self._mod = modulename |
---|
472 | self._name = cls.__name__ |
---|
473 | self._func_doc = func_doc |
---|
474 | |
---|
475 | if doc is None: |
---|
476 | doc = pydoc.getdoc(cls) |
---|
477 | |
---|
478 | NumpyDocString.__init__(self, doc) |
---|
479 | |
---|
480 | @property |
---|
481 | def methods(self): |
---|
482 | return [name for name,func in inspect.getmembers(self._cls) |
---|
483 | if not name.startswith('_') and callable(func)] |
---|
484 | |
---|
485 | def __str__(self): |
---|
486 | out = '' |
---|
487 | out += super(ClassDoc, self).__str__() |
---|
488 | out += "\n\n" |
---|
489 | |
---|
490 | #for m in self.methods: |
---|
491 | # print "Parsing `%s`" % m |
---|
492 | # out += str(self._func_doc(getattr(self._cls,m), 'meth')) + '\n\n' |
---|
493 | # out += '.. index::\n single: %s; %s\n\n' % (self._name, m) |
---|
494 | |
---|
495 | return out |
---|
496 | |
---|
497 | |
---|