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