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