##// END OF EJS Templates
fix more exeptions...
Matthias BUSSONNIER -
Show More
@@ -1,497 +1,497 b''
1 """Extract reference documentation from the NumPy source tree.
1 """Extract reference documentation from the NumPy source tree.
2
2
3 """
3 """
4
4
5 import inspect
5 import inspect
6 import textwrap
6 import textwrap
7 import re
7 import re
8 import pydoc
8 import pydoc
9 from StringIO import StringIO
9 from StringIO import StringIO
10 from warnings import warn
10 from warnings import warn
11
11
12 class Reader(object):
12 class Reader(object):
13 """A line-based string reader.
13 """A line-based string reader.
14
14
15 """
15 """
16 def __init__(self, data):
16 def __init__(self, data):
17 """
17 """
18 Parameters
18 Parameters
19 ----------
19 ----------
20 data : str
20 data : str
21 String with lines separated by '\n'.
21 String with lines separated by '\n'.
22
22
23 """
23 """
24 if isinstance(data,list):
24 if isinstance(data,list):
25 self._str = data
25 self._str = data
26 else:
26 else:
27 self._str = data.split('\n') # store string as list of lines
27 self._str = data.split('\n') # store string as list of lines
28
28
29 self.reset()
29 self.reset()
30
30
31 def __getitem__(self, n):
31 def __getitem__(self, n):
32 return self._str[n]
32 return self._str[n]
33
33
34 def reset(self):
34 def reset(self):
35 self._l = 0 # current line nr
35 self._l = 0 # current line nr
36
36
37 def read(self):
37 def read(self):
38 if not self.eof():
38 if not self.eof():
39 out = self[self._l]
39 out = self[self._l]
40 self._l += 1
40 self._l += 1
41 return out
41 return out
42 else:
42 else:
43 return ''
43 return ''
44
44
45 def seek_next_non_empty_line(self):
45 def seek_next_non_empty_line(self):
46 for l in self[self._l:]:
46 for l in self[self._l:]:
47 if l.strip():
47 if l.strip():
48 break
48 break
49 else:
49 else:
50 self._l += 1
50 self._l += 1
51
51
52 def eof(self):
52 def eof(self):
53 return self._l >= len(self._str)
53 return self._l >= len(self._str)
54
54
55 def read_to_condition(self, condition_func):
55 def read_to_condition(self, condition_func):
56 start = self._l
56 start = self._l
57 for line in self[start:]:
57 for line in self[start:]:
58 if condition_func(line):
58 if condition_func(line):
59 return self[start:self._l]
59 return self[start:self._l]
60 self._l += 1
60 self._l += 1
61 if self.eof():
61 if self.eof():
62 return self[start:self._l+1]
62 return self[start:self._l+1]
63 return []
63 return []
64
64
65 def read_to_next_empty_line(self):
65 def read_to_next_empty_line(self):
66 self.seek_next_non_empty_line()
66 self.seek_next_non_empty_line()
67 def is_empty(line):
67 def is_empty(line):
68 return not line.strip()
68 return not line.strip()
69 return self.read_to_condition(is_empty)
69 return self.read_to_condition(is_empty)
70
70
71 def read_to_next_unindented_line(self):
71 def read_to_next_unindented_line(self):
72 def is_unindented(line):
72 def is_unindented(line):
73 return (line.strip() and (len(line.lstrip()) == len(line)))
73 return (line.strip() and (len(line.lstrip()) == len(line)))
74 return self.read_to_condition(is_unindented)
74 return self.read_to_condition(is_unindented)
75
75
76 def peek(self,n=0):
76 def peek(self,n=0):
77 if self._l + n < len(self._str):
77 if self._l + n < len(self._str):
78 return self[self._l + n]
78 return self[self._l + n]
79 else:
79 else:
80 return ''
80 return ''
81
81
82 def is_empty(self):
82 def is_empty(self):
83 return not ''.join(self._str).strip()
83 return not ''.join(self._str).strip()
84
84
85
85
86 class NumpyDocString(object):
86 class NumpyDocString(object):
87 def __init__(self,docstring):
87 def __init__(self,docstring):
88 docstring = textwrap.dedent(docstring).split('\n')
88 docstring = textwrap.dedent(docstring).split('\n')
89
89
90 self._doc = Reader(docstring)
90 self._doc = Reader(docstring)
91 self._parsed_data = {
91 self._parsed_data = {
92 'Signature': '',
92 'Signature': '',
93 'Summary': [''],
93 'Summary': [''],
94 'Extended Summary': [],
94 'Extended Summary': [],
95 'Parameters': [],
95 'Parameters': [],
96 'Returns': [],
96 'Returns': [],
97 'Raises': [],
97 'Raises': [],
98 'Warns': [],
98 'Warns': [],
99 'Other Parameters': [],
99 'Other Parameters': [],
100 'Attributes': [],
100 'Attributes': [],
101 'Methods': [],
101 'Methods': [],
102 'See Also': [],
102 'See Also': [],
103 'Notes': [],
103 'Notes': [],
104 'Warnings': [],
104 'Warnings': [],
105 'References': '',
105 'References': '',
106 'Examples': '',
106 'Examples': '',
107 'index': {}
107 'index': {}
108 }
108 }
109
109
110 self._parse()
110 self._parse()
111
111
112 def __getitem__(self,key):
112 def __getitem__(self,key):
113 return self._parsed_data[key]
113 return self._parsed_data[key]
114
114
115 def __setitem__(self,key,val):
115 def __setitem__(self,key,val):
116 if not self._parsed_data.has_key(key):
116 if not self._parsed_data.has_key(key):
117 warn("Unknown section %s" % key)
117 warn("Unknown section %s" % key)
118 else:
118 else:
119 self._parsed_data[key] = val
119 self._parsed_data[key] = val
120
120
121 def _is_at_section(self):
121 def _is_at_section(self):
122 self._doc.seek_next_non_empty_line()
122 self._doc.seek_next_non_empty_line()
123
123
124 if self._doc.eof():
124 if self._doc.eof():
125 return False
125 return False
126
126
127 l1 = self._doc.peek().strip() # e.g. Parameters
127 l1 = self._doc.peek().strip() # e.g. Parameters
128
128
129 if l1.startswith('.. index::'):
129 if l1.startswith('.. index::'):
130 return True
130 return True
131
131
132 l2 = self._doc.peek(1).strip() # ---------- or ==========
132 l2 = self._doc.peek(1).strip() # ---------- or ==========
133 return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1))
133 return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1))
134
134
135 def _strip(self,doc):
135 def _strip(self,doc):
136 i = 0
136 i = 0
137 j = 0
137 j = 0
138 for i,line in enumerate(doc):
138 for i,line in enumerate(doc):
139 if line.strip(): break
139 if line.strip(): break
140
140
141 for j,line in enumerate(doc[::-1]):
141 for j,line in enumerate(doc[::-1]):
142 if line.strip(): break
142 if line.strip(): break
143
143
144 return doc[i:len(doc)-j]
144 return doc[i:len(doc)-j]
145
145
146 def _read_to_next_section(self):
146 def _read_to_next_section(self):
147 section = self._doc.read_to_next_empty_line()
147 section = self._doc.read_to_next_empty_line()
148
148
149 while not self._is_at_section() and not self._doc.eof():
149 while not self._is_at_section() and not self._doc.eof():
150 if not self._doc.peek(-1).strip(): # previous line was empty
150 if not self._doc.peek(-1).strip(): # previous line was empty
151 section += ['']
151 section += ['']
152
152
153 section += self._doc.read_to_next_empty_line()
153 section += self._doc.read_to_next_empty_line()
154
154
155 return section
155 return section
156
156
157 def _read_sections(self):
157 def _read_sections(self):
158 while not self._doc.eof():
158 while not self._doc.eof():
159 data = self._read_to_next_section()
159 data = self._read_to_next_section()
160 name = data[0].strip()
160 name = data[0].strip()
161
161
162 if name.startswith('..'): # index section
162 if name.startswith('..'): # index section
163 yield name, data[1:]
163 yield name, data[1:]
164 elif len(data) < 2:
164 elif len(data) < 2:
165 yield StopIteration
165 yield StopIteration
166 else:
166 else:
167 yield name, self._strip(data[2:])
167 yield name, self._strip(data[2:])
168
168
169 def _parse_param_list(self,content):
169 def _parse_param_list(self,content):
170 r = Reader(content)
170 r = Reader(content)
171 params = []
171 params = []
172 while not r.eof():
172 while not r.eof():
173 header = r.read().strip()
173 header = r.read().strip()
174 if ' : ' in header:
174 if ' : ' in header:
175 arg_name, arg_type = header.split(' : ')[:2]
175 arg_name, arg_type = header.split(' : ')[:2]
176 else:
176 else:
177 arg_name, arg_type = header, ''
177 arg_name, arg_type = header, ''
178
178
179 desc = r.read_to_next_unindented_line()
179 desc = r.read_to_next_unindented_line()
180 desc = dedent_lines(desc)
180 desc = dedent_lines(desc)
181
181
182 params.append((arg_name,arg_type,desc))
182 params.append((arg_name,arg_type,desc))
183
183
184 return params
184 return params
185
185
186
186
187 _name_rgx = re.compile(r"^\s*(:(?P<role>\w+):`(?P<name>[a-zA-Z0-9_.-]+)`|"
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)
188 r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X)
189 def _parse_see_also(self, content):
189 def _parse_see_also(self, content):
190 """
190 """
191 func_name : Descriptive text
191 func_name : Descriptive text
192 continued text
192 continued text
193 another_func_name : Descriptive text
193 another_func_name : Descriptive text
194 func_name1, func_name2, :meth:`func_name`, func_name3
194 func_name1, func_name2, :meth:`func_name`, func_name3
195
195
196 """
196 """
197 items = []
197 items = []
198
198
199 def parse_item_name(text):
199 def parse_item_name(text):
200 """Match ':role:`name`' or 'name'"""
200 """Match ':role:`name`' or 'name'"""
201 m = self._name_rgx.match(text)
201 m = self._name_rgx.match(text)
202 if m:
202 if m:
203 g = m.groups()
203 g = m.groups()
204 if g[1] is None:
204 if g[1] is None:
205 return g[3], None
205 return g[3], None
206 else:
206 else:
207 return g[2], g[1]
207 return g[2], g[1]
208 raise ValueError("%s is not a item name" % text)
208 raise ValueError("%s is not a item name" % text)
209
209
210 def push_item(name, rest):
210 def push_item(name, rest):
211 if not name:
211 if not name:
212 return
212 return
213 name, role = parse_item_name(name)
213 name, role = parse_item_name(name)
214 items.append((name, list(rest), role))
214 items.append((name, list(rest), role))
215 del rest[:]
215 del rest[:]
216
216
217 current_func = None
217 current_func = None
218 rest = []
218 rest = []
219
219
220 for line in content:
220 for line in content:
221 if not line.strip(): continue
221 if not line.strip(): continue
222
222
223 m = self._name_rgx.match(line)
223 m = self._name_rgx.match(line)
224 if m and line[m.end():].strip().startswith(':'):
224 if m and line[m.end():].strip().startswith(':'):
225 push_item(current_func, rest)
225 push_item(current_func, rest)
226 current_func, line = line[:m.end()], line[m.end():]
226 current_func, line = line[:m.end()], line[m.end():]
227 rest = [line.split(':', 1)[1].strip()]
227 rest = [line.split(':', 1)[1].strip()]
228 if not rest[0]:
228 if not rest[0]:
229 rest = []
229 rest = []
230 elif not line.startswith(' '):
230 elif not line.startswith(' '):
231 push_item(current_func, rest)
231 push_item(current_func, rest)
232 current_func = None
232 current_func = None
233 if ',' in line:
233 if ',' in line:
234 for func in line.split(','):
234 for func in line.split(','):
235 push_item(func, [])
235 push_item(func, [])
236 elif line.strip():
236 elif line.strip():
237 current_func = line
237 current_func = line
238 elif current_func is not None:
238 elif current_func is not None:
239 rest.append(line.strip())
239 rest.append(line.strip())
240 push_item(current_func, rest)
240 push_item(current_func, rest)
241 return items
241 return items
242
242
243 def _parse_index(self, section, content):
243 def _parse_index(self, section, content):
244 """
244 """
245 .. index: default
245 .. index: default
246 :refguide: something, else, and more
246 :refguide: something, else, and more
247
247
248 """
248 """
249 def strip_each_in(lst):
249 def strip_each_in(lst):
250 return [s.strip() for s in lst]
250 return [s.strip() for s in lst]
251
251
252 out = {}
252 out = {}
253 section = section.split('::')
253 section = section.split('::')
254 if len(section) > 1:
254 if len(section) > 1:
255 out['default'] = strip_each_in(section[1].split(','))[0]
255 out['default'] = strip_each_in(section[1].split(','))[0]
256 for line in content:
256 for line in content:
257 line = line.split(':')
257 line = line.split(':')
258 if len(line) > 2:
258 if len(line) > 2:
259 out[line[1]] = strip_each_in(line[2].split(','))
259 out[line[1]] = strip_each_in(line[2].split(','))
260 return out
260 return out
261
261
262 def _parse_summary(self):
262 def _parse_summary(self):
263 """Grab signature (if given) and summary"""
263 """Grab signature (if given) and summary"""
264 if self._is_at_section():
264 if self._is_at_section():
265 return
265 return
266
266
267 summary = self._doc.read_to_next_empty_line()
267 summary = self._doc.read_to_next_empty_line()
268 summary_str = " ".join([s.strip() for s in summary]).strip()
268 summary_str = " ".join([s.strip() for s in summary]).strip()
269 if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str):
269 if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str):
270 self['Signature'] = summary_str
270 self['Signature'] = summary_str
271 if not self._is_at_section():
271 if not self._is_at_section():
272 self['Summary'] = self._doc.read_to_next_empty_line()
272 self['Summary'] = self._doc.read_to_next_empty_line()
273 else:
273 else:
274 self['Summary'] = summary
274 self['Summary'] = summary
275
275
276 if not self._is_at_section():
276 if not self._is_at_section():
277 self['Extended Summary'] = self._read_to_next_section()
277 self['Extended Summary'] = self._read_to_next_section()
278
278
279 def _parse(self):
279 def _parse(self):
280 self._doc.reset()
280 self._doc.reset()
281 self._parse_summary()
281 self._parse_summary()
282
282
283 for (section,content) in self._read_sections():
283 for (section,content) in self._read_sections():
284 if not section.startswith('..'):
284 if not section.startswith('..'):
285 section = ' '.join([s.capitalize() for s in section.split(' ')])
285 section = ' '.join([s.capitalize() for s in section.split(' ')])
286 if section in ('Parameters', 'Attributes', 'Methods',
286 if section in ('Parameters', 'Attributes', 'Methods',
287 'Returns', 'Raises', 'Warns'):
287 'Returns', 'Raises', 'Warns'):
288 self[section] = self._parse_param_list(content)
288 self[section] = self._parse_param_list(content)
289 elif section.startswith('.. index::'):
289 elif section.startswith('.. index::'):
290 self['index'] = self._parse_index(section, content)
290 self['index'] = self._parse_index(section, content)
291 elif section == 'See Also':
291 elif section == 'See Also':
292 self['See Also'] = self._parse_see_also(content)
292 self['See Also'] = self._parse_see_also(content)
293 else:
293 else:
294 self[section] = content
294 self[section] = content
295
295
296 # string conversion routines
296 # string conversion routines
297
297
298 def _str_header(self, name, symbol='-'):
298 def _str_header(self, name, symbol='-'):
299 return [name, len(name)*symbol]
299 return [name, len(name)*symbol]
300
300
301 def _str_indent(self, doc, indent=4):
301 def _str_indent(self, doc, indent=4):
302 out = []
302 out = []
303 for line in doc:
303 for line in doc:
304 out += [' '*indent + line]
304 out += [' '*indent + line]
305 return out
305 return out
306
306
307 def _str_signature(self):
307 def _str_signature(self):
308 if self['Signature']:
308 if self['Signature']:
309 return [self['Signature'].replace('*','\*')] + ['']
309 return [self['Signature'].replace('*','\*')] + ['']
310 else:
310 else:
311 return ['']
311 return ['']
312
312
313 def _str_summary(self):
313 def _str_summary(self):
314 if self['Summary']:
314 if self['Summary']:
315 return self['Summary'] + ['']
315 return self['Summary'] + ['']
316 else:
316 else:
317 return []
317 return []
318
318
319 def _str_extended_summary(self):
319 def _str_extended_summary(self):
320 if self['Extended Summary']:
320 if self['Extended Summary']:
321 return self['Extended Summary'] + ['']
321 return self['Extended Summary'] + ['']
322 else:
322 else:
323 return []
323 return []
324
324
325 def _str_param_list(self, name):
325 def _str_param_list(self, name):
326 out = []
326 out = []
327 if self[name]:
327 if self[name]:
328 out += self._str_header(name)
328 out += self._str_header(name)
329 for param,param_type,desc in self[name]:
329 for param,param_type,desc in self[name]:
330 out += ['%s : %s' % (param, param_type)]
330 out += ['%s : %s' % (param, param_type)]
331 out += self._str_indent(desc)
331 out += self._str_indent(desc)
332 out += ['']
332 out += ['']
333 return out
333 return out
334
334
335 def _str_section(self, name):
335 def _str_section(self, name):
336 out = []
336 out = []
337 if self[name]:
337 if self[name]:
338 out += self._str_header(name)
338 out += self._str_header(name)
339 out += self[name]
339 out += self[name]
340 out += ['']
340 out += ['']
341 return out
341 return out
342
342
343 def _str_see_also(self, func_role):
343 def _str_see_also(self, func_role):
344 if not self['See Also']: return []
344 if not self['See Also']: return []
345 out = []
345 out = []
346 out += self._str_header("See Also")
346 out += self._str_header("See Also")
347 last_had_desc = True
347 last_had_desc = True
348 for func, desc, role in self['See Also']:
348 for func, desc, role in self['See Also']:
349 if role:
349 if role:
350 link = ':%s:`%s`' % (role, func)
350 link = ':%s:`%s`' % (role, func)
351 elif func_role:
351 elif func_role:
352 link = ':%s:`%s`' % (func_role, func)
352 link = ':%s:`%s`' % (func_role, func)
353 else:
353 else:
354 link = "`%s`_" % func
354 link = "`%s`_" % func
355 if desc or last_had_desc:
355 if desc or last_had_desc:
356 out += ['']
356 out += ['']
357 out += [link]
357 out += [link]
358 else:
358 else:
359 out[-1] += ", %s" % link
359 out[-1] += ", %s" % link
360 if desc:
360 if desc:
361 out += self._str_indent([' '.join(desc)])
361 out += self._str_indent([' '.join(desc)])
362 last_had_desc = True
362 last_had_desc = True
363 else:
363 else:
364 last_had_desc = False
364 last_had_desc = False
365 out += ['']
365 out += ['']
366 return out
366 return out
367
367
368 def _str_index(self):
368 def _str_index(self):
369 idx = self['index']
369 idx = self['index']
370 out = []
370 out = []
371 out += ['.. index:: %s' % idx.get('default','')]
371 out += ['.. index:: %s' % idx.get('default','')]
372 for section, references in idx.iteritems():
372 for section, references in idx.iteritems():
373 if section == 'default':
373 if section == 'default':
374 continue
374 continue
375 out += [' :%s: %s' % (section, ', '.join(references))]
375 out += [' :%s: %s' % (section, ', '.join(references))]
376 return out
376 return out
377
377
378 def __str__(self, func_role=''):
378 def __str__(self, func_role=''):
379 out = []
379 out = []
380 out += self._str_signature()
380 out += self._str_signature()
381 out += self._str_summary()
381 out += self._str_summary()
382 out += self._str_extended_summary()
382 out += self._str_extended_summary()
383 for param_list in ('Parameters','Returns','Raises'):
383 for param_list in ('Parameters','Returns','Raises'):
384 out += self._str_param_list(param_list)
384 out += self._str_param_list(param_list)
385 out += self._str_section('Warnings')
385 out += self._str_section('Warnings')
386 out += self._str_see_also(func_role)
386 out += self._str_see_also(func_role)
387 for s in ('Notes','References','Examples'):
387 for s in ('Notes','References','Examples'):
388 out += self._str_section(s)
388 out += self._str_section(s)
389 out += self._str_index()
389 out += self._str_index()
390 return '\n'.join(out)
390 return '\n'.join(out)
391
391
392
392
393 def indent(str,indent=4):
393 def indent(str,indent=4):
394 indent_str = ' '*indent
394 indent_str = ' '*indent
395 if str is None:
395 if str is None:
396 return indent_str
396 return indent_str
397 lines = str.split('\n')
397 lines = str.split('\n')
398 return '\n'.join(indent_str + l for l in lines)
398 return '\n'.join(indent_str + l for l in lines)
399
399
400 def dedent_lines(lines):
400 def dedent_lines(lines):
401 """Deindent a list of lines maximally"""
401 """Deindent a list of lines maximally"""
402 return textwrap.dedent("\n".join(lines)).split("\n")
402 return textwrap.dedent("\n".join(lines)).split("\n")
403
403
404 def header(text, style='-'):
404 def header(text, style='-'):
405 return text + '\n' + style*len(text) + '\n'
405 return text + '\n' + style*len(text) + '\n'
406
406
407
407
408 class FunctionDoc(NumpyDocString):
408 class FunctionDoc(NumpyDocString):
409 def __init__(self, func, role='func', doc=None):
409 def __init__(self, func, role='func', doc=None):
410 self._f = func
410 self._f = func
411 self._role = role # e.g. "func" or "meth"
411 self._role = role # e.g. "func" or "meth"
412 if doc is None:
412 if doc is None:
413 doc = inspect.getdoc(func) or ''
413 doc = inspect.getdoc(func) or ''
414 try:
414 try:
415 NumpyDocString.__init__(self, doc)
415 NumpyDocString.__init__(self, doc)
416 except ValueError, e:
416 except ValueError as e:
417 print '*'*78
417 print '*'*78
418 print "ERROR: '%s' while parsing `%s`" % (e, self._f)
418 print "ERROR: '%s' while parsing `%s`" % (e, self._f)
419 print '*'*78
419 print '*'*78
420 #print "Docstring follows:"
420 #print "Docstring follows:"
421 #print doclines
421 #print doclines
422 #print '='*78
422 #print '='*78
423
423
424 if not self['Signature']:
424 if not self['Signature']:
425 func, func_name = self.get_func()
425 func, func_name = self.get_func()
426 try:
426 try:
427 # try to read signature
427 # try to read signature
428 argspec = inspect.getargspec(func)
428 argspec = inspect.getargspec(func)
429 argspec = inspect.formatargspec(*argspec)
429 argspec = inspect.formatargspec(*argspec)
430 argspec = argspec.replace('*','\*')
430 argspec = argspec.replace('*','\*')
431 signature = '%s%s' % (func_name, argspec)
431 signature = '%s%s' % (func_name, argspec)
432 except TypeError, e:
432 except TypeError as e:
433 signature = '%s()' % func_name
433 signature = '%s()' % func_name
434 self['Signature'] = signature
434 self['Signature'] = signature
435
435
436 def get_func(self):
436 def get_func(self):
437 func_name = getattr(self._f, '__name__', self.__class__.__name__)
437 func_name = getattr(self._f, '__name__', self.__class__.__name__)
438 if inspect.isclass(self._f):
438 if inspect.isclass(self._f):
439 func = getattr(self._f, '__call__', self._f.__init__)
439 func = getattr(self._f, '__call__', self._f.__init__)
440 else:
440 else:
441 func = self._f
441 func = self._f
442 return func, func_name
442 return func, func_name
443
443
444 def __str__(self):
444 def __str__(self):
445 out = ''
445 out = ''
446
446
447 func, func_name = self.get_func()
447 func, func_name = self.get_func()
448 signature = self['Signature'].replace('*', '\*')
448 signature = self['Signature'].replace('*', '\*')
449
449
450 roles = {'func': 'function',
450 roles = {'func': 'function',
451 'meth': 'method'}
451 'meth': 'method'}
452
452
453 if self._role:
453 if self._role:
454 if not roles.has_key(self._role):
454 if not roles.has_key(self._role):
455 print "Warning: invalid role %s" % self._role
455 print "Warning: invalid role %s" % self._role
456 out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''),
456 out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''),
457 func_name)
457 func_name)
458
458
459 out += super(FunctionDoc, self).__str__(func_role=self._role)
459 out += super(FunctionDoc, self).__str__(func_role=self._role)
460 return out
460 return out
461
461
462
462
463 class ClassDoc(NumpyDocString):
463 class ClassDoc(NumpyDocString):
464 def __init__(self,cls,modulename='',func_doc=FunctionDoc,doc=None):
464 def __init__(self,cls,modulename='',func_doc=FunctionDoc,doc=None):
465 if not inspect.isclass(cls):
465 if not inspect.isclass(cls):
466 raise ValueError("Initialise using a class. Got %r" % cls)
466 raise ValueError("Initialise using a class. Got %r" % cls)
467 self._cls = cls
467 self._cls = cls
468
468
469 if modulename and not modulename.endswith('.'):
469 if modulename and not modulename.endswith('.'):
470 modulename += '.'
470 modulename += '.'
471 self._mod = modulename
471 self._mod = modulename
472 self._name = cls.__name__
472 self._name = cls.__name__
473 self._func_doc = func_doc
473 self._func_doc = func_doc
474
474
475 if doc is None:
475 if doc is None:
476 doc = pydoc.getdoc(cls)
476 doc = pydoc.getdoc(cls)
477
477
478 NumpyDocString.__init__(self, doc)
478 NumpyDocString.__init__(self, doc)
479
479
480 @property
480 @property
481 def methods(self):
481 def methods(self):
482 return [name for name,func in inspect.getmembers(self._cls)
482 return [name for name,func in inspect.getmembers(self._cls)
483 if not name.startswith('_') and callable(func)]
483 if not name.startswith('_') and callable(func)]
484
484
485 def __str__(self):
485 def __str__(self):
486 out = ''
486 out = ''
487 out += super(ClassDoc, self).__str__()
487 out += super(ClassDoc, self).__str__()
488 out += "\n\n"
488 out += "\n\n"
489
489
490 #for m in self.methods:
490 #for m in self.methods:
491 # print "Parsing `%s`" % m
491 # print "Parsing `%s`" % m
492 # out += str(self._func_doc(getattr(self._cls,m), 'meth')) + '\n\n'
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)
493 # out += '.. index::\n single: %s; %s\n\n' % (self._name, m)
494
494
495 return out
495 return out
496
496
497
497
@@ -1,155 +1,155 b''
1 """Define text roles for GitHub
1 """Define text roles for GitHub
2
2
3 * ghissue - Issue
3 * ghissue - Issue
4 * ghpull - Pull Request
4 * ghpull - Pull Request
5 * ghuser - User
5 * ghuser - User
6
6
7 Adapted from bitbucket example here:
7 Adapted from bitbucket example here:
8 https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py
8 https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py
9
9
10 Authors
10 Authors
11 -------
11 -------
12
12
13 * Doug Hellmann
13 * Doug Hellmann
14 * Min RK
14 * Min RK
15 """
15 """
16 #
16 #
17 # Original Copyright (c) 2010 Doug Hellmann. All rights reserved.
17 # Original Copyright (c) 2010 Doug Hellmann. All rights reserved.
18 #
18 #
19
19
20 from docutils import nodes, utils
20 from docutils import nodes, utils
21 from docutils.parsers.rst.roles import set_classes
21 from docutils.parsers.rst.roles import set_classes
22
22
23 def make_link_node(rawtext, app, type, slug, options):
23 def make_link_node(rawtext, app, type, slug, options):
24 """Create a link to a github resource.
24 """Create a link to a github resource.
25
25
26 :param rawtext: Text being replaced with link node.
26 :param rawtext: Text being replaced with link node.
27 :param app: Sphinx application context
27 :param app: Sphinx application context
28 :param type: Link type (issues, changeset, etc.)
28 :param type: Link type (issues, changeset, etc.)
29 :param slug: ID of the thing to link to
29 :param slug: ID of the thing to link to
30 :param options: Options dictionary passed to role func.
30 :param options: Options dictionary passed to role func.
31 """
31 """
32
32
33 try:
33 try:
34 base = app.config.github_project_url
34 base = app.config.github_project_url
35 if not base:
35 if not base:
36 raise AttributeError
36 raise AttributeError
37 if not base.endswith('/'):
37 if not base.endswith('/'):
38 base += '/'
38 base += '/'
39 except AttributeError, err:
39 except AttributeError as err:
40 raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
40 raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
41
41
42 ref = base + type + '/' + slug + '/'
42 ref = base + type + '/' + slug + '/'
43 set_classes(options)
43 set_classes(options)
44 prefix = "#"
44 prefix = "#"
45 if type == 'pull':
45 if type == 'pull':
46 prefix = "PR " + prefix
46 prefix = "PR " + prefix
47 node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref,
47 node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref,
48 **options)
48 **options)
49 return node
49 return node
50
50
51 def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
51 def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
52 """Link to a GitHub issue.
52 """Link to a GitHub issue.
53
53
54 Returns 2 part tuple containing list of nodes to insert into the
54 Returns 2 part tuple containing list of nodes to insert into the
55 document and a list of system messages. Both are allowed to be
55 document and a list of system messages. Both are allowed to be
56 empty.
56 empty.
57
57
58 :param name: The role name used in the document.
58 :param name: The role name used in the document.
59 :param rawtext: The entire markup snippet, with role.
59 :param rawtext: The entire markup snippet, with role.
60 :param text: The text marked with the role.
60 :param text: The text marked with the role.
61 :param lineno: The line number where rawtext appears in the input.
61 :param lineno: The line number where rawtext appears in the input.
62 :param inliner: The inliner instance that called us.
62 :param inliner: The inliner instance that called us.
63 :param options: Directive options for customization.
63 :param options: Directive options for customization.
64 :param content: The directive content for customization.
64 :param content: The directive content for customization.
65 """
65 """
66
66
67 try:
67 try:
68 issue_num = int(text)
68 issue_num = int(text)
69 if issue_num <= 0:
69 if issue_num <= 0:
70 raise ValueError
70 raise ValueError
71 except ValueError:
71 except ValueError:
72 msg = inliner.reporter.error(
72 msg = inliner.reporter.error(
73 'GitHub issue number must be a number greater than or equal to 1; '
73 'GitHub issue number must be a number greater than or equal to 1; '
74 '"%s" is invalid.' % text, line=lineno)
74 '"%s" is invalid.' % text, line=lineno)
75 prb = inliner.problematic(rawtext, rawtext, msg)
75 prb = inliner.problematic(rawtext, rawtext, msg)
76 return [prb], [msg]
76 return [prb], [msg]
77 app = inliner.document.settings.env.app
77 app = inliner.document.settings.env.app
78 #app.info('issue %r' % text)
78 #app.info('issue %r' % text)
79 if 'pull' in name.lower():
79 if 'pull' in name.lower():
80 category = 'pull'
80 category = 'pull'
81 elif 'issue' in name.lower():
81 elif 'issue' in name.lower():
82 category = 'issues'
82 category = 'issues'
83 else:
83 else:
84 msg = inliner.reporter.error(
84 msg = inliner.reporter.error(
85 'GitHub roles include "ghpull" and "ghissue", '
85 'GitHub roles include "ghpull" and "ghissue", '
86 '"%s" is invalid.' % name, line=lineno)
86 '"%s" is invalid.' % name, line=lineno)
87 prb = inliner.problematic(rawtext, rawtext, msg)
87 prb = inliner.problematic(rawtext, rawtext, msg)
88 return [prb], [msg]
88 return [prb], [msg]
89 node = make_link_node(rawtext, app, category, str(issue_num), options)
89 node = make_link_node(rawtext, app, category, str(issue_num), options)
90 return [node], []
90 return [node], []
91
91
92 def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
92 def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
93 """Link to a GitHub user.
93 """Link to a GitHub user.
94
94
95 Returns 2 part tuple containing list of nodes to insert into the
95 Returns 2 part tuple containing list of nodes to insert into the
96 document and a list of system messages. Both are allowed to be
96 document and a list of system messages. Both are allowed to be
97 empty.
97 empty.
98
98
99 :param name: The role name used in the document.
99 :param name: The role name used in the document.
100 :param rawtext: The entire markup snippet, with role.
100 :param rawtext: The entire markup snippet, with role.
101 :param text: The text marked with the role.
101 :param text: The text marked with the role.
102 :param lineno: The line number where rawtext appears in the input.
102 :param lineno: The line number where rawtext appears in the input.
103 :param inliner: The inliner instance that called us.
103 :param inliner: The inliner instance that called us.
104 :param options: Directive options for customization.
104 :param options: Directive options for customization.
105 :param content: The directive content for customization.
105 :param content: The directive content for customization.
106 """
106 """
107 app = inliner.document.settings.env.app
107 app = inliner.document.settings.env.app
108 #app.info('user link %r' % text)
108 #app.info('user link %r' % text)
109 ref = 'https://www.github.com/' + text
109 ref = 'https://www.github.com/' + text
110 node = nodes.reference(rawtext, text, refuri=ref, **options)
110 node = nodes.reference(rawtext, text, refuri=ref, **options)
111 return [node], []
111 return [node], []
112
112
113 def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
113 def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
114 """Link to a GitHub commit.
114 """Link to a GitHub commit.
115
115
116 Returns 2 part tuple containing list of nodes to insert into the
116 Returns 2 part tuple containing list of nodes to insert into the
117 document and a list of system messages. Both are allowed to be
117 document and a list of system messages. Both are allowed to be
118 empty.
118 empty.
119
119
120 :param name: The role name used in the document.
120 :param name: The role name used in the document.
121 :param rawtext: The entire markup snippet, with role.
121 :param rawtext: The entire markup snippet, with role.
122 :param text: The text marked with the role.
122 :param text: The text marked with the role.
123 :param lineno: The line number where rawtext appears in the input.
123 :param lineno: The line number where rawtext appears in the input.
124 :param inliner: The inliner instance that called us.
124 :param inliner: The inliner instance that called us.
125 :param options: Directive options for customization.
125 :param options: Directive options for customization.
126 :param content: The directive content for customization.
126 :param content: The directive content for customization.
127 """
127 """
128 app = inliner.document.settings.env.app
128 app = inliner.document.settings.env.app
129 #app.info('user link %r' % text)
129 #app.info('user link %r' % text)
130 try:
130 try:
131 base = app.config.github_project_url
131 base = app.config.github_project_url
132 if not base:
132 if not base:
133 raise AttributeError
133 raise AttributeError
134 if not base.endswith('/'):
134 if not base.endswith('/'):
135 base += '/'
135 base += '/'
136 except AttributeError, err:
136 except AttributeError as err:
137 raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
137 raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
138
138
139 ref = base + text
139 ref = base + text
140 node = nodes.reference(rawtext, text[:6], refuri=ref, **options)
140 node = nodes.reference(rawtext, text[:6], refuri=ref, **options)
141 return [node], []
141 return [node], []
142
142
143
143
144 def setup(app):
144 def setup(app):
145 """Install the plugin.
145 """Install the plugin.
146
146
147 :param app: Sphinx application context.
147 :param app: Sphinx application context.
148 """
148 """
149 app.info('Initializing GitHub plugin')
149 app.info('Initializing GitHub plugin')
150 app.add_role('ghissue', ghissue_role)
150 app.add_role('ghissue', ghissue_role)
151 app.add_role('ghpull', ghissue_role)
151 app.add_role('ghpull', ghissue_role)
152 app.add_role('ghuser', ghuser_role)
152 app.add_role('ghuser', ghuser_role)
153 app.add_role('ghcommit', ghcommit_role)
153 app.add_role('ghcommit', ghcommit_role)
154 app.add_config_value('github_project_url', None, 'env')
154 app.add_config_value('github_project_url', None, 'env')
155 return
155 return
@@ -1,407 +1,407 b''
1 """
1 """
2 Defines a docutils directive for inserting inheritance diagrams.
2 Defines a docutils directive for inserting inheritance diagrams.
3
3
4 Provide the directive with one or more classes or modules (separated
4 Provide the directive with one or more classes or modules (separated
5 by whitespace). For modules, all of the classes in that module will
5 by whitespace). For modules, all of the classes in that module will
6 be used.
6 be used.
7
7
8 Example::
8 Example::
9
9
10 Given the following classes:
10 Given the following classes:
11
11
12 class A: pass
12 class A: pass
13 class B(A): pass
13 class B(A): pass
14 class C(A): pass
14 class C(A): pass
15 class D(B, C): pass
15 class D(B, C): pass
16 class E(B): pass
16 class E(B): pass
17
17
18 .. inheritance-diagram: D E
18 .. inheritance-diagram: D E
19
19
20 Produces a graph like the following:
20 Produces a graph like the following:
21
21
22 A
22 A
23 / \
23 / \
24 B C
24 B C
25 / \ /
25 / \ /
26 E D
26 E D
27
27
28 The graph is inserted as a PNG+image map into HTML and a PDF in
28 The graph is inserted as a PNG+image map into HTML and a PDF in
29 LaTeX.
29 LaTeX.
30 """
30 """
31
31
32 import inspect
32 import inspect
33 import os
33 import os
34 import re
34 import re
35 import subprocess
35 import subprocess
36 try:
36 try:
37 from hashlib import md5
37 from hashlib import md5
38 except ImportError:
38 except ImportError:
39 from md5 import md5
39 from md5 import md5
40
40
41 from docutils.nodes import Body, Element
41 from docutils.nodes import Body, Element
42 from docutils.parsers.rst import directives
42 from docutils.parsers.rst import directives
43 from sphinx.roles import xfileref_role
43 from sphinx.roles import xfileref_role
44
44
45 def my_import(name):
45 def my_import(name):
46 """Module importer - taken from the python documentation.
46 """Module importer - taken from the python documentation.
47
47
48 This function allows importing names with dots in them."""
48 This function allows importing names with dots in them."""
49
49
50 mod = __import__(name)
50 mod = __import__(name)
51 components = name.split('.')
51 components = name.split('.')
52 for comp in components[1:]:
52 for comp in components[1:]:
53 mod = getattr(mod, comp)
53 mod = getattr(mod, comp)
54 return mod
54 return mod
55
55
56 class DotException(Exception):
56 class DotException(Exception):
57 pass
57 pass
58
58
59 class InheritanceGraph(object):
59 class InheritanceGraph(object):
60 """
60 """
61 Given a list of classes, determines the set of classes that
61 Given a list of classes, determines the set of classes that
62 they inherit from all the way to the root "object", and then
62 they inherit from all the way to the root "object", and then
63 is able to generate a graphviz dot graph from them.
63 is able to generate a graphviz dot graph from them.
64 """
64 """
65 def __init__(self, class_names, show_builtins=False):
65 def __init__(self, class_names, show_builtins=False):
66 """
66 """
67 *class_names* is a list of child classes to show bases from.
67 *class_names* is a list of child classes to show bases from.
68
68
69 If *show_builtins* is True, then Python builtins will be shown
69 If *show_builtins* is True, then Python builtins will be shown
70 in the graph.
70 in the graph.
71 """
71 """
72 self.class_names = class_names
72 self.class_names = class_names
73 self.classes = self._import_classes(class_names)
73 self.classes = self._import_classes(class_names)
74 self.all_classes = self._all_classes(self.classes)
74 self.all_classes = self._all_classes(self.classes)
75 if len(self.all_classes) == 0:
75 if len(self.all_classes) == 0:
76 raise ValueError("No classes found for inheritance diagram")
76 raise ValueError("No classes found for inheritance diagram")
77 self.show_builtins = show_builtins
77 self.show_builtins = show_builtins
78
78
79 py_sig_re = re.compile(r'''^([\w.]*\.)? # class names
79 py_sig_re = re.compile(r'''^([\w.]*\.)? # class names
80 (\w+) \s* $ # optionally arguments
80 (\w+) \s* $ # optionally arguments
81 ''', re.VERBOSE)
81 ''', re.VERBOSE)
82
82
83 def _import_class_or_module(self, name):
83 def _import_class_or_module(self, name):
84 """
84 """
85 Import a class using its fully-qualified *name*.
85 Import a class using its fully-qualified *name*.
86 """
86 """
87 try:
87 try:
88 path, base = self.py_sig_re.match(name).groups()
88 path, base = self.py_sig_re.match(name).groups()
89 except:
89 except:
90 raise ValueError(
90 raise ValueError(
91 "Invalid class or module '%s' specified for inheritance diagram" % name)
91 "Invalid class or module '%s' specified for inheritance diagram" % name)
92 fullname = (path or '') + base
92 fullname = (path or '') + base
93 path = (path and path.rstrip('.'))
93 path = (path and path.rstrip('.'))
94 if not path:
94 if not path:
95 path = base
95 path = base
96 try:
96 try:
97 module = __import__(path, None, None, [])
97 module = __import__(path, None, None, [])
98 # We must do an import of the fully qualified name. Otherwise if a
98 # We must do an import of the fully qualified name. Otherwise if a
99 # subpackage 'a.b' is requested where 'import a' does NOT provide
99 # subpackage 'a.b' is requested where 'import a' does NOT provide
100 # 'a.b' automatically, then 'a.b' will not be found below. This
100 # 'a.b' automatically, then 'a.b' will not be found below. This
101 # second call will force the equivalent of 'import a.b' to happen
101 # second call will force the equivalent of 'import a.b' to happen
102 # after the top-level import above.
102 # after the top-level import above.
103 my_import(fullname)
103 my_import(fullname)
104
104
105 except ImportError:
105 except ImportError:
106 raise ValueError(
106 raise ValueError(
107 "Could not import class or module '%s' specified for inheritance diagram" % name)
107 "Could not import class or module '%s' specified for inheritance diagram" % name)
108
108
109 try:
109 try:
110 todoc = module
110 todoc = module
111 for comp in fullname.split('.')[1:]:
111 for comp in fullname.split('.')[1:]:
112 todoc = getattr(todoc, comp)
112 todoc = getattr(todoc, comp)
113 except AttributeError:
113 except AttributeError:
114 raise ValueError(
114 raise ValueError(
115 "Could not find class or module '%s' specified for inheritance diagram" % name)
115 "Could not find class or module '%s' specified for inheritance diagram" % name)
116
116
117 # If a class, just return it
117 # If a class, just return it
118 if inspect.isclass(todoc):
118 if inspect.isclass(todoc):
119 return [todoc]
119 return [todoc]
120 elif inspect.ismodule(todoc):
120 elif inspect.ismodule(todoc):
121 classes = []
121 classes = []
122 for cls in todoc.__dict__.values():
122 for cls in todoc.__dict__.values():
123 if inspect.isclass(cls) and cls.__module__ == todoc.__name__:
123 if inspect.isclass(cls) and cls.__module__ == todoc.__name__:
124 classes.append(cls)
124 classes.append(cls)
125 return classes
125 return classes
126 raise ValueError(
126 raise ValueError(
127 "'%s' does not resolve to a class or module" % name)
127 "'%s' does not resolve to a class or module" % name)
128
128
129 def _import_classes(self, class_names):
129 def _import_classes(self, class_names):
130 """
130 """
131 Import a list of classes.
131 Import a list of classes.
132 """
132 """
133 classes = []
133 classes = []
134 for name in class_names:
134 for name in class_names:
135 classes.extend(self._import_class_or_module(name))
135 classes.extend(self._import_class_or_module(name))
136 return classes
136 return classes
137
137
138 def _all_classes(self, classes):
138 def _all_classes(self, classes):
139 """
139 """
140 Return a list of all classes that are ancestors of *classes*.
140 Return a list of all classes that are ancestors of *classes*.
141 """
141 """
142 all_classes = {}
142 all_classes = {}
143
143
144 def recurse(cls):
144 def recurse(cls):
145 all_classes[cls] = None
145 all_classes[cls] = None
146 for c in cls.__bases__:
146 for c in cls.__bases__:
147 if c not in all_classes:
147 if c not in all_classes:
148 recurse(c)
148 recurse(c)
149
149
150 for cls in classes:
150 for cls in classes:
151 recurse(cls)
151 recurse(cls)
152
152
153 return all_classes.keys()
153 return all_classes.keys()
154
154
155 def class_name(self, cls, parts=0):
155 def class_name(self, cls, parts=0):
156 """
156 """
157 Given a class object, return a fully-qualified name. This
157 Given a class object, return a fully-qualified name. This
158 works for things I've tested in matplotlib so far, but may not
158 works for things I've tested in matplotlib so far, but may not
159 be completely general.
159 be completely general.
160 """
160 """
161 module = cls.__module__
161 module = cls.__module__
162 if module == '__builtin__':
162 if module == '__builtin__':
163 fullname = cls.__name__
163 fullname = cls.__name__
164 else:
164 else:
165 fullname = "%s.%s" % (module, cls.__name__)
165 fullname = "%s.%s" % (module, cls.__name__)
166 if parts == 0:
166 if parts == 0:
167 return fullname
167 return fullname
168 name_parts = fullname.split('.')
168 name_parts = fullname.split('.')
169 return '.'.join(name_parts[-parts:])
169 return '.'.join(name_parts[-parts:])
170
170
171 def get_all_class_names(self):
171 def get_all_class_names(self):
172 """
172 """
173 Get all of the class names involved in the graph.
173 Get all of the class names involved in the graph.
174 """
174 """
175 return [self.class_name(x) for x in self.all_classes]
175 return [self.class_name(x) for x in self.all_classes]
176
176
177 # These are the default options for graphviz
177 # These are the default options for graphviz
178 default_graph_options = {
178 default_graph_options = {
179 "rankdir": "LR",
179 "rankdir": "LR",
180 "size": '"8.0, 12.0"'
180 "size": '"8.0, 12.0"'
181 }
181 }
182 default_node_options = {
182 default_node_options = {
183 "shape": "box",
183 "shape": "box",
184 "fontsize": 10,
184 "fontsize": 10,
185 "height": 0.25,
185 "height": 0.25,
186 "fontname": "Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",
186 "fontname": "Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",
187 "style": '"setlinewidth(0.5)"'
187 "style": '"setlinewidth(0.5)"'
188 }
188 }
189 default_edge_options = {
189 default_edge_options = {
190 "arrowsize": 0.5,
190 "arrowsize": 0.5,
191 "style": '"setlinewidth(0.5)"'
191 "style": '"setlinewidth(0.5)"'
192 }
192 }
193
193
194 def _format_node_options(self, options):
194 def _format_node_options(self, options):
195 return ','.join(["%s=%s" % x for x in options.items()])
195 return ','.join(["%s=%s" % x for x in options.items()])
196 def _format_graph_options(self, options):
196 def _format_graph_options(self, options):
197 return ''.join(["%s=%s;\n" % x for x in options.items()])
197 return ''.join(["%s=%s;\n" % x for x in options.items()])
198
198
199 def generate_dot(self, fd, name, parts=0, urls={},
199 def generate_dot(self, fd, name, parts=0, urls={},
200 graph_options={}, node_options={},
200 graph_options={}, node_options={},
201 edge_options={}):
201 edge_options={}):
202 """
202 """
203 Generate a graphviz dot graph from the classes that
203 Generate a graphviz dot graph from the classes that
204 were passed in to __init__.
204 were passed in to __init__.
205
205
206 *fd* is a Python file-like object to write to.
206 *fd* is a Python file-like object to write to.
207
207
208 *name* is the name of the graph
208 *name* is the name of the graph
209
209
210 *urls* is a dictionary mapping class names to http urls
210 *urls* is a dictionary mapping class names to http urls
211
211
212 *graph_options*, *node_options*, *edge_options* are
212 *graph_options*, *node_options*, *edge_options* are
213 dictionaries containing key/value pairs to pass on as graphviz
213 dictionaries containing key/value pairs to pass on as graphviz
214 properties.
214 properties.
215 """
215 """
216 g_options = self.default_graph_options.copy()
216 g_options = self.default_graph_options.copy()
217 g_options.update(graph_options)
217 g_options.update(graph_options)
218 n_options = self.default_node_options.copy()
218 n_options = self.default_node_options.copy()
219 n_options.update(node_options)
219 n_options.update(node_options)
220 e_options = self.default_edge_options.copy()
220 e_options = self.default_edge_options.copy()
221 e_options.update(edge_options)
221 e_options.update(edge_options)
222
222
223 fd.write('digraph %s {\n' % name)
223 fd.write('digraph %s {\n' % name)
224 fd.write(self._format_graph_options(g_options))
224 fd.write(self._format_graph_options(g_options))
225
225
226 for cls in self.all_classes:
226 for cls in self.all_classes:
227 if not self.show_builtins and cls in __builtins__.values():
227 if not self.show_builtins and cls in __builtins__.values():
228 continue
228 continue
229
229
230 name = self.class_name(cls, parts)
230 name = self.class_name(cls, parts)
231
231
232 # Write the node
232 # Write the node
233 this_node_options = n_options.copy()
233 this_node_options = n_options.copy()
234 url = urls.get(self.class_name(cls))
234 url = urls.get(self.class_name(cls))
235 if url is not None:
235 if url is not None:
236 this_node_options['URL'] = '"%s"' % url
236 this_node_options['URL'] = '"%s"' % url
237 fd.write(' "%s" [%s];\n' %
237 fd.write(' "%s" [%s];\n' %
238 (name, self._format_node_options(this_node_options)))
238 (name, self._format_node_options(this_node_options)))
239
239
240 # Write the edges
240 # Write the edges
241 for base in cls.__bases__:
241 for base in cls.__bases__:
242 if not self.show_builtins and base in __builtins__.values():
242 if not self.show_builtins and base in __builtins__.values():
243 continue
243 continue
244
244
245 base_name = self.class_name(base, parts)
245 base_name = self.class_name(base, parts)
246 fd.write(' "%s" -> "%s" [%s];\n' %
246 fd.write(' "%s" -> "%s" [%s];\n' %
247 (base_name, name,
247 (base_name, name,
248 self._format_node_options(e_options)))
248 self._format_node_options(e_options)))
249 fd.write('}\n')
249 fd.write('}\n')
250
250
251 def run_dot(self, args, name, parts=0, urls={},
251 def run_dot(self, args, name, parts=0, urls={},
252 graph_options={}, node_options={}, edge_options={}):
252 graph_options={}, node_options={}, edge_options={}):
253 """
253 """
254 Run graphviz 'dot' over this graph, returning whatever 'dot'
254 Run graphviz 'dot' over this graph, returning whatever 'dot'
255 writes to stdout.
255 writes to stdout.
256
256
257 *args* will be passed along as commandline arguments.
257 *args* will be passed along as commandline arguments.
258
258
259 *name* is the name of the graph
259 *name* is the name of the graph
260
260
261 *urls* is a dictionary mapping class names to http urls
261 *urls* is a dictionary mapping class names to http urls
262
262
263 Raises DotException for any of the many os and
263 Raises DotException for any of the many os and
264 installation-related errors that may occur.
264 installation-related errors that may occur.
265 """
265 """
266 try:
266 try:
267 dot = subprocess.Popen(['dot'] + list(args),
267 dot = subprocess.Popen(['dot'] + list(args),
268 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
268 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
269 close_fds=True)
269 close_fds=True)
270 except OSError:
270 except OSError:
271 raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?")
271 raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?")
272 except ValueError:
272 except ValueError:
273 raise DotException("'dot' called with invalid arguments")
273 raise DotException("'dot' called with invalid arguments")
274 except:
274 except:
275 raise DotException("Unexpected error calling 'dot'")
275 raise DotException("Unexpected error calling 'dot'")
276
276
277 self.generate_dot(dot.stdin, name, parts, urls, graph_options,
277 self.generate_dot(dot.stdin, name, parts, urls, graph_options,
278 node_options, edge_options)
278 node_options, edge_options)
279 dot.stdin.close()
279 dot.stdin.close()
280 result = dot.stdout.read()
280 result = dot.stdout.read()
281 returncode = dot.wait()
281 returncode = dot.wait()
282 if returncode != 0:
282 if returncode != 0:
283 raise DotException("'dot' returned the errorcode %d" % returncode)
283 raise DotException("'dot' returned the errorcode %d" % returncode)
284 return result
284 return result
285
285
286 class inheritance_diagram(Body, Element):
286 class inheritance_diagram(Body, Element):
287 """
287 """
288 A docutils node to use as a placeholder for the inheritance
288 A docutils node to use as a placeholder for the inheritance
289 diagram.
289 diagram.
290 """
290 """
291 pass
291 pass
292
292
293 def inheritance_diagram_directive(name, arguments, options, content, lineno,
293 def inheritance_diagram_directive(name, arguments, options, content, lineno,
294 content_offset, block_text, state,
294 content_offset, block_text, state,
295 state_machine):
295 state_machine):
296 """
296 """
297 Run when the inheritance_diagram directive is first encountered.
297 Run when the inheritance_diagram directive is first encountered.
298 """
298 """
299 node = inheritance_diagram()
299 node = inheritance_diagram()
300
300
301 class_names = arguments
301 class_names = arguments
302
302
303 # Create a graph starting with the list of classes
303 # Create a graph starting with the list of classes
304 graph = InheritanceGraph(class_names)
304 graph = InheritanceGraph(class_names)
305
305
306 # Create xref nodes for each target of the graph's image map and
306 # Create xref nodes for each target of the graph's image map and
307 # add them to the doc tree so that Sphinx can resolve the
307 # add them to the doc tree so that Sphinx can resolve the
308 # references to real URLs later. These nodes will eventually be
308 # references to real URLs later. These nodes will eventually be
309 # removed from the doctree after we're done with them.
309 # removed from the doctree after we're done with them.
310 for name in graph.get_all_class_names():
310 for name in graph.get_all_class_names():
311 refnodes, x = xfileref_role(
311 refnodes, x = xfileref_role(
312 'class', ':class:`%s`' % name, name, 0, state)
312 'class', ':class:`%s`' % name, name, 0, state)
313 node.extend(refnodes)
313 node.extend(refnodes)
314 # Store the graph object so we can use it to generate the
314 # Store the graph object so we can use it to generate the
315 # dot file later
315 # dot file later
316 node['graph'] = graph
316 node['graph'] = graph
317 # Store the original content for use as a hash
317 # Store the original content for use as a hash
318 node['parts'] = options.get('parts', 0)
318 node['parts'] = options.get('parts', 0)
319 node['content'] = " ".join(class_names)
319 node['content'] = " ".join(class_names)
320 return [node]
320 return [node]
321
321
322 def get_graph_hash(node):
322 def get_graph_hash(node):
323 return md5(node['content'] + str(node['parts'])).hexdigest()[-10:]
323 return md5(node['content'] + str(node['parts'])).hexdigest()[-10:]
324
324
325 def html_output_graph(self, node):
325 def html_output_graph(self, node):
326 """
326 """
327 Output the graph for HTML. This will insert a PNG with clickable
327 Output the graph for HTML. This will insert a PNG with clickable
328 image map.
328 image map.
329 """
329 """
330 graph = node['graph']
330 graph = node['graph']
331 parts = node['parts']
331 parts = node['parts']
332
332
333 graph_hash = get_graph_hash(node)
333 graph_hash = get_graph_hash(node)
334 name = "inheritance%s" % graph_hash
334 name = "inheritance%s" % graph_hash
335 path = '_images'
335 path = '_images'
336 dest_path = os.path.join(setup.app.builder.outdir, path)
336 dest_path = os.path.join(setup.app.builder.outdir, path)
337 if not os.path.exists(dest_path):
337 if not os.path.exists(dest_path):
338 os.makedirs(dest_path)
338 os.makedirs(dest_path)
339 png_path = os.path.join(dest_path, name + ".png")
339 png_path = os.path.join(dest_path, name + ".png")
340 path = setup.app.builder.imgpath
340 path = setup.app.builder.imgpath
341
341
342 # Create a mapping from fully-qualified class names to URLs.
342 # Create a mapping from fully-qualified class names to URLs.
343 urls = {}
343 urls = {}
344 for child in node:
344 for child in node:
345 if child.get('refuri') is not None:
345 if child.get('refuri') is not None:
346 urls[child['reftitle']] = child.get('refuri')
346 urls[child['reftitle']] = child.get('refuri')
347 elif child.get('refid') is not None:
347 elif child.get('refid') is not None:
348 urls[child['reftitle']] = '#' + child.get('refid')
348 urls[child['reftitle']] = '#' + child.get('refid')
349
349
350 # These arguments to dot will save a PNG file to disk and write
350 # These arguments to dot will save a PNG file to disk and write
351 # an HTML image map to stdout.
351 # an HTML image map to stdout.
352 image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'],
352 image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'],
353 name, parts, urls)
353 name, parts, urls)
354 return ('<img src="%s/%s.png" usemap="#%s" class="inheritance"/>%s' %
354 return ('<img src="%s/%s.png" usemap="#%s" class="inheritance"/>%s' %
355 (path, name, name, image_map))
355 (path, name, name, image_map))
356
356
357 def latex_output_graph(self, node):
357 def latex_output_graph(self, node):
358 """
358 """
359 Output the graph for LaTeX. This will insert a PDF.
359 Output the graph for LaTeX. This will insert a PDF.
360 """
360 """
361 graph = node['graph']
361 graph = node['graph']
362 parts = node['parts']
362 parts = node['parts']
363
363
364 graph_hash = get_graph_hash(node)
364 graph_hash = get_graph_hash(node)
365 name = "inheritance%s" % graph_hash
365 name = "inheritance%s" % graph_hash
366 dest_path = os.path.abspath(os.path.join(setup.app.builder.outdir, '_images'))
366 dest_path = os.path.abspath(os.path.join(setup.app.builder.outdir, '_images'))
367 if not os.path.exists(dest_path):
367 if not os.path.exists(dest_path):
368 os.makedirs(dest_path)
368 os.makedirs(dest_path)
369 pdf_path = os.path.abspath(os.path.join(dest_path, name + ".pdf"))
369 pdf_path = os.path.abspath(os.path.join(dest_path, name + ".pdf"))
370
370
371 graph.run_dot(['-Tpdf', '-o%s' % pdf_path],
371 graph.run_dot(['-Tpdf', '-o%s' % pdf_path],
372 name, parts, graph_options={'size': '"6.0,6.0"'})
372 name, parts, graph_options={'size': '"6.0,6.0"'})
373 return '\n\\includegraphics{%s}\n\n' % pdf_path
373 return '\n\\includegraphics{%s}\n\n' % pdf_path
374
374
375 def visit_inheritance_diagram(inner_func):
375 def visit_inheritance_diagram(inner_func):
376 """
376 """
377 This is just a wrapper around html/latex_output_graph to make it
377 This is just a wrapper around html/latex_output_graph to make it
378 easier to handle errors and insert warnings.
378 easier to handle errors and insert warnings.
379 """
379 """
380 def visitor(self, node):
380 def visitor(self, node):
381 try:
381 try:
382 content = inner_func(self, node)
382 content = inner_func(self, node)
383 except DotException, e:
383 except DotException as e:
384 # Insert the exception as a warning in the document
384 # Insert the exception as a warning in the document
385 warning = self.document.reporter.warning(str(e), line=node.line)
385 warning = self.document.reporter.warning(str(e), line=node.line)
386 warning.parent = node
386 warning.parent = node
387 node.children = [warning]
387 node.children = [warning]
388 else:
388 else:
389 source = self.document.attributes['source']
389 source = self.document.attributes['source']
390 self.body.append(content)
390 self.body.append(content)
391 node.children = []
391 node.children = []
392 return visitor
392 return visitor
393
393
394 def do_nothing(self, node):
394 def do_nothing(self, node):
395 pass
395 pass
396
396
397 def setup(app):
397 def setup(app):
398 setup.app = app
398 setup.app = app
399 setup.confdir = app.confdir
399 setup.confdir = app.confdir
400
400
401 app.add_node(
401 app.add_node(
402 inheritance_diagram,
402 inheritance_diagram,
403 latex=(visit_inheritance_diagram(latex_output_graph), do_nothing),
403 latex=(visit_inheritance_diagram(latex_output_graph), do_nothing),
404 html=(visit_inheritance_diagram(html_output_graph), do_nothing))
404 html=(visit_inheritance_diagram(html_output_graph), do_nothing))
405 app.add_directive(
405 app.add_directive(
406 'inheritance-diagram', inheritance_diagram_directive,
406 'inheritance-diagram', inheritance_diagram_directive,
407 False, (1, 100, 0), parts = directives.nonnegative_int)
407 False, (1, 100, 0), parts = directives.nonnegative_int)
General Comments 0
You need to be logged in to leave comments. Login now