##// END OF EJS Templates
fix dot syntax error in inheritance diagram
MinRK -
Show More
@@ -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 as 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