|
From: <md...@us...> - 2008-06-19 19:56:26
|
Revision: 5601
http://matplotlib.svn.sourceforge.net/matplotlib/?rev=5601&view=rev
Author: mdboom
Date: 2008-06-19 12:55:41 -0700 (Thu, 19 Jun 2008)
Log Message:
-----------
Add initial support for inheritance diagrams.
Modified Paths:
--------------
trunk/matplotlib/doc/api/artist_api.rst
trunk/matplotlib/doc/api/collections_api.rst
trunk/matplotlib/doc/conf.py
trunk/matplotlib/doc/devel/transformations.rst
Added Paths:
-----------
trunk/matplotlib/doc/sphinxext/inheritance_diagram.py
Modified: trunk/matplotlib/doc/api/artist_api.rst
===================================================================
--- trunk/matplotlib/doc/api/artist_api.rst 2008-06-19 19:53:12 UTC (rev 5600)
+++ trunk/matplotlib/doc/api/artist_api.rst 2008-06-19 19:55:41 UTC (rev 5601)
@@ -2,6 +2,8 @@
matplotlib artists
*******************
+.. inheritance-diagram:: matplotlib.patches matplotlib.lines matplotlib.text
+
:mod:`matplotlib.artist`
=============================
Modified: trunk/matplotlib/doc/api/collections_api.rst
===================================================================
--- trunk/matplotlib/doc/api/collections_api.rst 2008-06-19 19:53:12 UTC (rev 5600)
+++ trunk/matplotlib/doc/api/collections_api.rst 2008-06-19 19:55:41 UTC (rev 5601)
@@ -2,6 +2,7 @@
matplotlib collections
**********************
+.. inheritance-diagram:: matplotlib.collections
:mod:`matplotlib.collections`
=============================
Modified: trunk/matplotlib/doc/conf.py
===================================================================
--- trunk/matplotlib/doc/conf.py 2008-06-19 19:53:12 UTC (rev 5600)
+++ trunk/matplotlib/doc/conf.py 2008-06-19 19:55:41 UTC (rev 5601)
@@ -28,7 +28,7 @@
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['mathpng', 'math_symbol_table', 'sphinx.ext.autodoc',
- 'only_directives', 'plot_directive']
+ 'only_directives', 'plot_directive', 'inheritance_diagram']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
Modified: trunk/matplotlib/doc/devel/transformations.rst
===================================================================
--- trunk/matplotlib/doc/devel/transformations.rst 2008-06-19 19:53:12 UTC (rev 5600)
+++ trunk/matplotlib/doc/devel/transformations.rst 2008-06-19 19:55:41 UTC (rev 5601)
@@ -2,6 +2,8 @@
Working with transformations
==============================
+.. inheritance-diagram:: matplotlib.transforms matplotlib.path
+
:mod:`matplotlib.transforms`
=============================
Added: trunk/matplotlib/doc/sphinxext/inheritance_diagram.py
===================================================================
--- trunk/matplotlib/doc/sphinxext/inheritance_diagram.py (rev 0)
+++ trunk/matplotlib/doc/sphinxext/inheritance_diagram.py 2008-06-19 19:55:41 UTC (rev 5601)
@@ -0,0 +1,414 @@
+"""
+Defines a docutils directive for inserting inheritance diagrams.
+
+Provide the directive with one or more classes or modules (separated
+by whitespace). For modules, all of the classes in that module will
+be used.
+
+Example::
+
+ Given the following classes:
+
+ class A: pass
+ class B(A): pass
+ class C(A): pass
+ class D(B, C): pass
+ class E(B): pass
+
+ .. inheritance-diagram: D E
+
+ Produces a graph like the following:
+
+ A
+ / \
+ B C
+ / \ /
+ E D
+
+The graph is inserted as a PNG+image map into HTML and a PDF in
+LaTeX.
+"""
+
+import inspect
+import os
+import subprocess
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+
+from docutils.nodes import Body, Element
+from docutils.writers.html4css1 import HTMLTranslator
+from sphinx.latexwriter import LaTeXTranslator
+from docutils.parsers.rst import directives
+from sphinx.roles import xfileref_role
+from sphinx.directives.desc import py_sig_re
+
+class DotException(Exception):
+ pass
+
+class InheritanceGraph(object):
+ """
+ Given a list of classes, determines the set of classes that
+ they inherit from all the way to the root "object", and then
+ is able to generate a graphviz dot graph from them.
+ """
+ def __init__(self, class_names, show_builtins=False):
+ """
+ *class_names* is a list of child classes to show bases from.
+
+ If *show_builtins* is True, then Python builtins will be shown
+ in the graph.
+ """
+ self.class_names = class_names
+ self.classes = self._import_classes(class_names)
+ self.all_classes = self._all_classes(self.classes)
+ if len(self.all_classes) == 0:
+ raise ValueError("No classes found for inheritance diagram")
+ self.show_builtins = show_builtins
+
+ def _import_class_or_module(self, name):
+ """
+ Import a class using its fully-qualified *name*.
+ """
+ try:
+ path, base, signature = py_sig_re.match(name).groups()
+ except:
+ raise ValueError(
+ "Invalid class '%s' specified for inheritance diagram" % name)
+ fullname = (path or '') + base
+ path = path and path.rstrip('.')
+ if not path:
+ raise ValueError(
+ "Invalid class '%s' specified for inheritance diagram" % name)
+ try:
+ module = __import__(path, None, None, [])
+ except ImportError:
+ raise ValueError(
+ "Could not import class '%s' specified for inheritance diagram" % name)
+
+ try:
+ todoc = module
+ for comp in fullname.split('.')[1:]:
+ todoc = getattr(todoc, comp)
+ except AttributeError:
+ raise ValueError(
+ "Could not find class '%s' specified for inheritance diagram" % name)
+
+ # If a class, just return it
+ if inspect.isclass(todoc):
+ return [todoc]
+ elif inspect.ismodule(todoc):
+ classes = []
+ for cls in todoc.__dict__.values():
+ if inspect.isclass(cls) and cls.__module__ == todoc.__name__:
+ classes.append(cls)
+ return classes
+ raise ValueError(
+ "'%s' does not resolve to a class or module" % name)
+
+ def _import_classes(self, class_names):
+ """
+ Import a list of classes.
+ """
+ classes = []
+ for name in class_names:
+ classes.extend(self._import_class_or_module(name))
+ return classes
+
+ def _all_classes(self, classes):
+ """
+ Return a list of all classes that are ancestors of *classes*.
+ """
+ all_classes = {}
+
+ def recurse(cls):
+ all_classes[cls] = None
+ for c in cls.__bases__:
+ if c not in all_classes:
+ recurse(c)
+
+ for cls in classes:
+ recurse(cls)
+
+ return all_classes.keys()
+
+ def class_name(self, cls):
+ """
+ Given a class object, return a fully-qualified name. This
+ works for things I've tested in matplotlib so far, but may
+ not be completely general.
+ """
+ module = cls.__module__
+ if module == '__builtin__':
+ return cls.__name__
+ return '.'.join([module, cls.__name__])
+
+ def get_all_class_names(self):
+ """
+ Get all of the class names involved in the graph.
+ """
+ return [self.class_name(x) for x in self.all_classes]
+
+ # These are the default options for
+ default_graph_options = {
+ "rankdir": "LR",
+ "size": '"11.0, 11.0"'
+ }
+ default_node_options = {
+ "shape": "box",
+ "fontsize": 10,
+ "height": 0.25,
+ "fontname": "sans",
+ "style": '"setlinewidth(0.5)"'
+ }
+ default_edge_options = {
+ "arrowsize": 0.5,
+ "style": '"setlinewidth(0.5)"'
+ }
+
+ def _format_node_options(self, options):
+ return ','.join(["%s=%s" % x for x in options.items()])
+ def _format_graph_options(self, options):
+ return ''.join(["%s=%s;\n" % x for x in options.items()])
+
+ def generate_dot(self, fd, name, urls={},
+ graph_options={}, node_options={},
+ edge_options={}):
+ """
+ Generate a graphviz dot graph from the classes that
+ were passed in to __init__.
+
+ *fd* is a Python file-like object to write to.
+
+ *name* is the name of the graph
+
+ *urls* is a dictionary mapping class names to http urls
+
+ *graph_options*, *node_options*, *edge_options* are
+ dictionaries containing key/value pairs to pass on as graphviz
+ properties.
+ """
+ g_options = self.default_graph_options.copy()
+ g_options.update(graph_options)
+ n_options = self.default_node_options.copy()
+ n_options.update(node_options)
+ e_options = self.default_edge_options.copy()
+ e_options.update(edge_options)
+
+ fd.write('digraph %s {\n' % name)
+ fd.write(self._format_graph_options(g_options))
+
+ for cls in self.all_classes:
+ if not self.show_builtins and cls in __builtins__.values():
+ continue
+
+ name = self.class_name(cls)
+
+ # Write the node
+ this_node_options = n_options.copy()
+ url = urls.get(name)
+ if url is not None:
+ this_node_options['URL'] = '"%s"' % url
+ fd.write(' "%s" [%s];\n' %
+ (name, self._format_node_options(this_node_options)))
+
+ # Write the edges
+ for base in cls.__bases__:
+ if not self.show_builtins and base in __builtins__.values():
+ continue
+
+ base_name = self.class_name(base)
+ fd.write(' "%s" -> "%s" [%s];\n' %
+ (self.class_name(base), name,
+ self._format_node_options(e_options)))
+ fd.write('}\n')
+
+ def run_dot(self, args, name, urls={},
+ graph_options={}, node_options={}, edge_options={}):
+ """
+ Run graphviz 'dot' over this graph, returning whatever 'dot'
+ writes to stdout.
+
+ *args* will be passed along as commandline arguments.
+
+ *name* is the name of the graph
+
+ *urls* is a dictionary mapping class names to http urls
+
+ Raises DotException for any of the many os and
+ installation-related errors that may occur.
+ """
+ try:
+ dot = subprocess.Popen(['dot'] + list(args),
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ close_fds=True)
+ except OSError:
+ raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?")
+ except ValueError:
+ raise DotException("'dot' called with invalid arguments")
+ except:
+ raise DotException("Unexpected error calling 'dot'")
+
+ self.generate_dot(dot.stdin, name, urls, graph_options, node_options,
+ edge_options)
+ dot.stdin.close()
+ result = dot.stdout.read()
+ returncode = dot.wait()
+ if returncode != 0:
+ raise DotException("'dot' returned the errorcode %d" % returncode)
+ return result
+
+class inheritance_diagram(Body, Element):
+ """
+ A docutils node to use as a placeholder for the inheritance
+ diagram.
+ """
+ pass
+
+def inheritance_diagram_directive_run(clstexts, state):
+ """
+ Run when the inheritance_diagram directive is first encountered.
+ """
+ node = inheritance_diagram()
+
+ # Create a graph starting with the list of classes
+ graph = InheritanceGraph(clstexts)
+
+ # Create xref nodes for each target of the graph's image map and
+ # add them to the doc tree so that Sphinx can resolve the
+ # references to real URLs later. These nodes will eventually be
+ # removed from the doctree after we're done with them.
+ for name in graph.get_all_class_names():
+ refnodes, x = xfileref_role(
+ 'class', ':class:`%s`' % name, name, 0, state)
+ node.extend(refnodes)
+ # Store the graph object so we can use it to generate the
+ # dot file later
+ node['graph'] = graph
+ # Store the original content for use as a hash
+ node['content'] = " ".join(clstexts)
+ return [node]
+
+def html_output_graph(self, node):
+ """
+ Output the graph for HTML. This will insert a PNG with clickable
+ image map.
+ """
+ graph = node['graph']
+
+ # Determine where to write the PNG to. This follows
+ # the same procedure as mathpng.py
+ name = 'inheritance%s' % md5(node['content']).hexdigest()[-10:]
+ png_path = '_static/%s.png' % name
+
+ path = '_static'
+ source = self.document.attributes['source']
+ count = source.split('/doc/')[-1].count('/')
+ for i in range(count):
+ if os.path.exists(path): break
+ path = '../'+path
+ path = '../'+path #specifically added for matplotlib
+
+ # Create a mapping from fully-qualified class names to URLs.
+ urls = {}
+ for child in node:
+ try:
+ urls[child['reftitle']] = child['refuri']
+ except KeyError:
+ try:
+ urls[child['reftitle']] = '#' + child['refid']
+ except KeyError:
+ pass
+
+ # These arguments to dot will save a PNG file to disk and write
+ # an HTML image map to stdout.
+ image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'],
+ name, urls)
+ return ('<img src="%s/%s.png" usemap="#%s"/>%s' %
+ (path, name, name, image_map))
+
+def latex_output_graph(self, node):
+ """
+ Output the graph for LaTeX. This will insert a PDF.
+ """
+ graph = node['graph']
+
+ # Determine where to write the PNG to. This follows
+ # the same procedure as mathpng.py
+ name = 'inheritance%s' % md5(node['content']).hexdigest()[-10:]
+ pdf_path = '_static/%s.pdf' % name
+
+ path = '_static'
+ source = self.document.attributes['source']
+ count = source.split('/doc/')[-1].count('/')
+ for i in range(count):
+ if os.path.exists(path): break
+ path = '../'+path
+ path = '../'+path #specifically added for matplotlib
+
+ graph.run_dot(['-Tpdf', '-o%s' % pdf_path], name,
+ graph_options={'size': '"6.0,6.0"'})
+ return '\\includegraphics{../../_static/%s.pdf}' % name
+
+def visit_inheritance_diagram(inner_func):
+ """
+ This is just a wrapper around html/latex_output_graph to make it
+ easier to handle errors and insert warnings.
+ """
+ def visitor(self, node):
+ try:
+ content = inner_func(self, node)
+ except DotException, e:
+ # Insert the exception as a warning in the document
+ warning = self.document.reporter.warning(str(e), line=node.line)
+ warning.parent = node
+ node.children = [warning]
+ else:
+ source = self.document.attributes['source']
+ self.body.append(content)
+ node.children = []
+ return visitor
+
+def do_nothing(self, node):
+ pass
+
+# Deal with the old and new way of registering directives
+try:
+ from docutils.parsers.rst import Directive
+except ImportError:
+ from docutils.parsers.rst.directives import _directives
+ def inheritance_diagram_directive(name, arguments, options, content, lineno,
+ content_offset, block_text, state,
+ state_machine):
+ return inheritance_diagram_directive_run(arguments, state)
+ inheritance_diagram_directive.__doc__ = __doc__
+ inheritance_diagram_directive.arguments = (1, 100, 0)
+ inheritance_diagram_directive.options = {}
+ inheritance_diagram_directive.content = 0
+ _directives['inheritance-diagram'] = inheritance_diagram_directive
+else:
+ class inheritance_diagram_directive(Directive):
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 100
+ final_argument_whitespace = False
+ option_spec = {}
+
+ def run(self):
+ return inheritance_diagram_directive_run(self.arguments, self.state)
+ inheritance_diagram_directive.__doc__ = __doc__
+
+ directives.register_directive('inheritance-diagram',
+ inheritance_diagram_directive)
+
+def setup(app):
+ app.add_node(inheritance_diagram)
+
+ HTMLTranslator.visit_inheritance_diagram = \
+ visit_inheritance_diagram(html_output_graph)
+ HTMLTranslator.depart_inheritance_diagram = do_nothing
+
+ LaTeXTranslator.visit_inheritance_diagram = \
+ visit_inheritance_diagram(latex_output_graph)
+ LaTeXTranslator.depart_inheritance_diagram = do_nothing
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|