##// END OF EJS Templates
Fix inheritance_diagram Sphinx extension for Sphinx 1.2
Thomas Kluyver -
Show More
@@ -1,407 +1,409 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 from sphinx.roles import xfileref_role
43 from sphinx.roles import XRefRole
44
45 xfileref_role = XRefRole()
44 46
45 47 def my_import(name):
46 48 """Module importer - taken from the python documentation.
47 49
48 50 This function allows importing names with dots in them."""
49 51
50 52 mod = __import__(name)
51 53 components = name.split('.')
52 54 for comp in components[1:]:
53 55 mod = getattr(mod, comp)
54 56 return mod
55 57
56 58 class DotException(Exception):
57 59 pass
58 60
59 61 class InheritanceGraph(object):
60 62 """
61 63 Given a list of classes, determines the set of classes that
62 64 they inherit from all the way to the root "object", and then
63 65 is able to generate a graphviz dot graph from them.
64 66 """
65 67 def __init__(self, class_names, show_builtins=False):
66 68 """
67 69 *class_names* is a list of child classes to show bases from.
68 70
69 71 If *show_builtins* is True, then Python builtins will be shown
70 72 in the graph.
71 73 """
72 74 self.class_names = class_names
73 75 self.classes = self._import_classes(class_names)
74 76 self.all_classes = self._all_classes(self.classes)
75 77 if len(self.all_classes) == 0:
76 78 raise ValueError("No classes found for inheritance diagram")
77 79 self.show_builtins = show_builtins
78 80
79 81 py_sig_re = re.compile(r'''^([\w.]*\.)? # class names
80 82 (\w+) \s* $ # optionally arguments
81 83 ''', re.VERBOSE)
82 84
83 85 def _import_class_or_module(self, name):
84 86 """
85 87 Import a class using its fully-qualified *name*.
86 88 """
87 89 try:
88 90 path, base = self.py_sig_re.match(name).groups()
89 91 except:
90 92 raise ValueError(
91 93 "Invalid class or module '%s' specified for inheritance diagram" % name)
92 94 fullname = (path or '') + base
93 95 path = (path and path.rstrip('.'))
94 96 if not path:
95 97 path = base
96 98 try:
97 99 module = __import__(path, None, None, [])
98 100 # We must do an import of the fully qualified name. Otherwise if a
99 101 # subpackage 'a.b' is requested where 'import a' does NOT provide
100 102 # 'a.b' automatically, then 'a.b' will not be found below. This
101 103 # second call will force the equivalent of 'import a.b' to happen
102 104 # after the top-level import above.
103 105 my_import(fullname)
104 106
105 107 except ImportError:
106 108 raise ValueError(
107 109 "Could not import class or module '%s' specified for inheritance diagram" % name)
108 110
109 111 try:
110 112 todoc = module
111 113 for comp in fullname.split('.')[1:]:
112 114 todoc = getattr(todoc, comp)
113 115 except AttributeError:
114 116 raise ValueError(
115 117 "Could not find class or module '%s' specified for inheritance diagram" % name)
116 118
117 119 # If a class, just return it
118 120 if inspect.isclass(todoc):
119 121 return [todoc]
120 122 elif inspect.ismodule(todoc):
121 123 classes = []
122 124 for cls in todoc.__dict__.values():
123 125 if inspect.isclass(cls) and cls.__module__ == todoc.__name__:
124 126 classes.append(cls)
125 127 return classes
126 128 raise ValueError(
127 129 "'%s' does not resolve to a class or module" % name)
128 130
129 131 def _import_classes(self, class_names):
130 132 """
131 133 Import a list of classes.
132 134 """
133 135 classes = []
134 136 for name in class_names:
135 137 classes.extend(self._import_class_or_module(name))
136 138 return classes
137 139
138 140 def _all_classes(self, classes):
139 141 """
140 142 Return a list of all classes that are ancestors of *classes*.
141 143 """
142 144 all_classes = {}
143 145
144 146 def recurse(cls):
145 147 all_classes[cls] = None
146 148 for c in cls.__bases__:
147 149 if c not in all_classes:
148 150 recurse(c)
149 151
150 152 for cls in classes:
151 153 recurse(cls)
152 154
153 155 return all_classes.keys()
154 156
155 157 def class_name(self, cls, parts=0):
156 158 """
157 159 Given a class object, return a fully-qualified name. This
158 160 works for things I've tested in matplotlib so far, but may not
159 161 be completely general.
160 162 """
161 163 module = cls.__module__
162 164 if module == '__builtin__':
163 165 fullname = cls.__name__
164 166 else:
165 167 fullname = "%s.%s" % (module, cls.__name__)
166 168 if parts == 0:
167 169 return fullname
168 170 name_parts = fullname.split('.')
169 171 return '.'.join(name_parts[-parts:])
170 172
171 173 def get_all_class_names(self):
172 174 """
173 175 Get all of the class names involved in the graph.
174 176 """
175 177 return [self.class_name(x) for x in self.all_classes]
176 178
177 179 # These are the default options for graphviz
178 180 default_graph_options = {
179 181 "rankdir": "LR",
180 182 "size": '"8.0, 12.0"'
181 183 }
182 184 default_node_options = {
183 185 "shape": "box",
184 186 "fontsize": 10,
185 187 "height": 0.25,
186 188 "fontname": '"Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans"',
187 189 "style": '"setlinewidth(0.5)"'
188 190 }
189 191 default_edge_options = {
190 192 "arrowsize": 0.5,
191 193 "style": '"setlinewidth(0.5)"'
192 194 }
193 195
194 196 def _format_node_options(self, options):
195 197 return ','.join(["%s=%s" % x for x in options.items()])
196 198 def _format_graph_options(self, options):
197 199 return ''.join(["%s=%s;\n" % x for x in options.items()])
198 200
199 201 def generate_dot(self, fd, name, parts=0, urls={},
200 202 graph_options={}, node_options={},
201 203 edge_options={}):
202 204 """
203 205 Generate a graphviz dot graph from the classes that
204 206 were passed in to __init__.
205 207
206 208 *fd* is a Python file-like object to write to.
207 209
208 210 *name* is the name of the graph
209 211
210 212 *urls* is a dictionary mapping class names to http urls
211 213
212 214 *graph_options*, *node_options*, *edge_options* are
213 215 dictionaries containing key/value pairs to pass on as graphviz
214 216 properties.
215 217 """
216 218 g_options = self.default_graph_options.copy()
217 219 g_options.update(graph_options)
218 220 n_options = self.default_node_options.copy()
219 221 n_options.update(node_options)
220 222 e_options = self.default_edge_options.copy()
221 223 e_options.update(edge_options)
222 224
223 225 fd.write('digraph %s {\n' % name)
224 226 fd.write(self._format_graph_options(g_options))
225 227
226 228 for cls in self.all_classes:
227 229 if not self.show_builtins and cls in __builtins__.values():
228 230 continue
229 231
230 232 name = self.class_name(cls, parts)
231 233
232 234 # Write the node
233 235 this_node_options = n_options.copy()
234 236 url = urls.get(self.class_name(cls))
235 237 if url is not None:
236 238 this_node_options['URL'] = '"%s"' % url
237 239 fd.write(' "%s" [%s];\n' %
238 240 (name, self._format_node_options(this_node_options)))
239 241
240 242 # Write the edges
241 243 for base in cls.__bases__:
242 244 if not self.show_builtins and base in __builtins__.values():
243 245 continue
244 246
245 247 base_name = self.class_name(base, parts)
246 248 fd.write(' "%s" -> "%s" [%s];\n' %
247 249 (base_name, name,
248 250 self._format_node_options(e_options)))
249 251 fd.write('}\n')
250 252
251 253 def run_dot(self, args, name, parts=0, urls={},
252 254 graph_options={}, node_options={}, edge_options={}):
253 255 """
254 256 Run graphviz 'dot' over this graph, returning whatever 'dot'
255 257 writes to stdout.
256 258
257 259 *args* will be passed along as commandline arguments.
258 260
259 261 *name* is the name of the graph
260 262
261 263 *urls* is a dictionary mapping class names to http urls
262 264
263 265 Raises DotException for any of the many os and
264 266 installation-related errors that may occur.
265 267 """
266 268 try:
267 269 dot = subprocess.Popen(['dot'] + list(args),
268 270 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
269 271 close_fds=True)
270 272 except OSError:
271 273 raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?")
272 274 except ValueError:
273 275 raise DotException("'dot' called with invalid arguments")
274 276 except:
275 277 raise DotException("Unexpected error calling 'dot'")
276 278
277 279 self.generate_dot(dot.stdin, name, parts, urls, graph_options,
278 280 node_options, edge_options)
279 281 dot.stdin.close()
280 282 result = dot.stdout.read()
281 283 returncode = dot.wait()
282 284 if returncode != 0:
283 285 raise DotException("'dot' returned the errorcode %d" % returncode)
284 286 return result
285 287
286 288 class inheritance_diagram(Body, Element):
287 289 """
288 290 A docutils node to use as a placeholder for the inheritance
289 291 diagram.
290 292 """
291 293 pass
292 294
293 295 def inheritance_diagram_directive(name, arguments, options, content, lineno,
294 296 content_offset, block_text, state,
295 297 state_machine):
296 298 """
297 299 Run when the inheritance_diagram directive is first encountered.
298 300 """
299 301 node = inheritance_diagram()
300 302
301 303 class_names = arguments
302 304
303 305 # Create a graph starting with the list of classes
304 306 graph = InheritanceGraph(class_names)
305 307
306 308 # Create xref nodes for each target of the graph's image map and
307 309 # add them to the doc tree so that Sphinx can resolve the
308 310 # references to real URLs later. These nodes will eventually be
309 311 # removed from the doctree after we're done with them.
310 312 for name in graph.get_all_class_names():
311 313 refnodes, x = xfileref_role(
312 314 'class', ':class:`%s`' % name, name, 0, state)
313 315 node.extend(refnodes)
314 316 # Store the graph object so we can use it to generate the
315 317 # dot file later
316 318 node['graph'] = graph
317 319 # Store the original content for use as a hash
318 320 node['parts'] = options.get('parts', 0)
319 321 node['content'] = " ".join(class_names)
320 322 return [node]
321 323
322 324 def get_graph_hash(node):
323 325 return md5(node['content'] + str(node['parts'])).hexdigest()[-10:]
324 326
325 327 def html_output_graph(self, node):
326 328 """
327 329 Output the graph for HTML. This will insert a PNG with clickable
328 330 image map.
329 331 """
330 332 graph = node['graph']
331 333 parts = node['parts']
332 334
333 335 graph_hash = get_graph_hash(node)
334 336 name = "inheritance%s" % graph_hash
335 337 path = '_images'
336 338 dest_path = os.path.join(setup.app.builder.outdir, path)
337 339 if not os.path.exists(dest_path):
338 340 os.makedirs(dest_path)
339 341 png_path = os.path.join(dest_path, name + ".png")
340 342 path = setup.app.builder.imgpath
341 343
342 344 # Create a mapping from fully-qualified class names to URLs.
343 345 urls = {}
344 346 for child in node:
345 347 if child.get('refuri') is not None:
346 348 urls[child['reftitle']] = child.get('refuri')
347 349 elif child.get('refid') is not None:
348 350 urls[child['reftitle']] = '#' + child.get('refid')
349 351
350 352 # These arguments to dot will save a PNG file to disk and write
351 353 # an HTML image map to stdout.
352 354 image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'],
353 355 name, parts, urls)
354 356 return ('<img src="%s/%s.png" usemap="#%s" class="inheritance"/>%s' %
355 357 (path, name, name, image_map))
356 358
357 359 def latex_output_graph(self, node):
358 360 """
359 361 Output the graph for LaTeX. This will insert a PDF.
360 362 """
361 363 graph = node['graph']
362 364 parts = node['parts']
363 365
364 366 graph_hash = get_graph_hash(node)
365 367 name = "inheritance%s" % graph_hash
366 368 dest_path = os.path.abspath(os.path.join(setup.app.builder.outdir, '_images'))
367 369 if not os.path.exists(dest_path):
368 370 os.makedirs(dest_path)
369 371 pdf_path = os.path.abspath(os.path.join(dest_path, name + ".pdf"))
370 372
371 373 graph.run_dot(['-Tpdf', '-o%s' % pdf_path],
372 374 name, parts, graph_options={'size': '"6.0,6.0"'})
373 375 return '\n\\includegraphics{%s}\n\n' % pdf_path
374 376
375 377 def visit_inheritance_diagram(inner_func):
376 378 """
377 379 This is just a wrapper around html/latex_output_graph to make it
378 380 easier to handle errors and insert warnings.
379 381 """
380 382 def visitor(self, node):
381 383 try:
382 384 content = inner_func(self, node)
383 385 except DotException as e:
384 386 # Insert the exception as a warning in the document
385 387 warning = self.document.reporter.warning(str(e), line=node.line)
386 388 warning.parent = node
387 389 node.children = [warning]
388 390 else:
389 391 source = self.document.attributes['source']
390 392 self.body.append(content)
391 393 node.children = []
392 394 return visitor
393 395
394 396 def do_nothing(self, node):
395 397 pass
396 398
397 399 def setup(app):
398 400 setup.app = app
399 401 setup.confdir = app.confdir
400 402
401 403 app.add_node(
402 404 inheritance_diagram,
403 405 latex=(visit_inheritance_diagram(latex_output_graph), do_nothing),
404 406 html=(visit_inheritance_diagram(html_output_graph), do_nothing))
405 407 app.add_directive(
406 408 'inheritance-diagram', inheritance_diagram_directive,
407 409 False, (1, 100, 0), parts = directives.nonnegative_int)
General Comments 0
You need to be logged in to leave comments. Login now