Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 501 lines (411 sloc) 15.301 kb
4e2cf07 Anthony 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.