##// END OF EJS Templates
fix dot syntax error in inheritance diagram
MinRK -
Show More
@@ -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 "fontname": "Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",
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 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