Commits

Georg Brandl  committed a62e95f

New ``inheritance_diagram`` extension to embed... inheritance diagrams!

  • Participants
  • Parent commits 8d0ee9b

Comments (0)

Files changed (4)

 Other contributors, listed alphabetically, are:
 
 * Daniel Bültmann -- todo extension
+* Michael Droettboom -- inheritance_diagram extension
 * Charles Duffy -- original graphviz extension
 * Josip Dzolonga -- coverage builder
 * Horst Gutmann -- internationalization support
 
   - New ``graphviz`` extension to embed graphviz graphs.
 
+  - New ``inheritance_diagram`` extension to embed... inheritance
+    diagrams!
+
   - Autodoc now has a reusable Python API, which can be used to
     create custom types of objects to auto-document (e.g. Zope
     interfaces).  See also ``Sphinx.add_autodocumenter()``.

File doc/ext/inheritance.rst

+.. highlight:: rest
+
+The inheritance diagram extension
+=================================
+
+.. module:: sphinx.ext.inheritance_diagram
+   :synopsis: Support for displaying inheritance diagrams via graphviz.
+
+.. versionadded:: 0.6
+
+This extension allows you to include inheritance diagrams, rendered via the
+:mod:`Graphviz extension <sphinx.ext.graphviz>`.
+
+It adds this directive:
+
+.. directive:: inheritance-diagram
+
+   This directive has one or more arguments, each giving a module or class
+   name.  Class names can be unqualified; in that case they are taken to exist
+   in the currently described module (see :dir:`module`).
+
+   For each given class, and each class in each given module, the base classes
+   are determined.  Then, from all classes and their base classes, a graph is
+   generated which is then rendered via the graphviz extension to a directed
+   graph.
+
+   This directive supports an option called ``parts`` that, if given, must be an
+   integer, advising the directive to remove that many parts of module names
+   from the displayed names.  (For example, if all your class names start with
+   ``lib.``, you can give ``:parts: 1`` to remove that prefix from the displayed
+   node names.)
+
+
+New config values are:
+
+.. confval:: inheritance_graph_attrs
+
+   A dictionary of graphviz graph attributes for inheritance diagrams.
+
+.. confval:: inheritance_node_attrs
+
+   A dictionary of graphviz node attributes for inheritance diagrams.
+
+.. confval:: inheritance_edge_attrs
+
+   A dictionary of graphviz edge attributes for inheritance diagrams.

File sphinx/ext/inheritance_diagram.py

+"""
+    sphinx.ext.inheritance_diagram
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    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.
+
+    :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import os
+import re
+import sys
+import inspect
+import subprocess
+try:
+    from hashlib import md5
+except ImportError:
+    from md5 import md5
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+
+from sphinx.roles import xfileref_role
+from sphinx.ext.graphviz import render_dot_html, render_dot_latex
+from sphinx.util.compat import Directive
+
+
+class_sig_re = re.compile(r'''^([\w.]*\.)?    # module names
+                          (\w+)  \s* $        # class/final module name
+                          ''', re.VERBOSE)
+
+
+class InheritanceException(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, currmodule, 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, currmodule)
+        self.all_classes = self._all_classes(self.classes)
+        if len(self.all_classes) == 0:
+            raise InheritanceException('No classes found for '
+                                       'inheritance diagram')
+        self.show_builtins = show_builtins
+
+    def _import_class_or_module(self, name, currmodule):
+        """
+        Import a class using its fully-qualified *name*.
+        """
+        try:
+            path, base = class_sig_re.match(name).groups()
+        except ValueError:
+            raise InheritanceException('Invalid class or module %r specified '
+                                       'for inheritance diagram' % name)
+
+        fullname = (path or '') + base
+        path = (path and path.rstrip('.') or '')
+
+        # two possibilities: either it is a module, then import it
+        try:
+            module = __import__(fullname)
+            todoc = sys.modules[fullname]
+        except ImportError:
+            # else it is a class, then import the module
+            if not path:
+                if currmodule:
+                    # try the current module
+                    path = currmodule
+                else:
+                    raise InheritanceException(
+                        'Could not import class %r specified for '
+                        'inheritance diagram' % base)
+            try:
+                module = __import__(path)
+                todoc = getattr(sys.modules[path], base)
+            except (ImportError, AttributeError):
+                raise InheritanceException(
+                    'Could not import class or module %r specified for '
+                    'inheritance diagram' % (path + '.' + base))
+
+        # 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 InheritanceException('%r specified for inheritance diagram is '
+                                   'not a class or module' % name)
+
+    def _import_classes(self, class_names, currmodule):
+        """
+        Import a list of classes.
+        """
+        classes = []
+        for name in class_names:
+            classes.extend(self._import_class_or_module(name, currmodule))
+        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, parts=0):
+        """
+        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__':
+            fullname = cls.__name__
+        else:
+            fullname = '%s.%s' % (module, cls.__name__)
+        if parts == 0:
+            return fullname
+        name_parts = fullname.split('.')
+        return '.'.join(name_parts[-parts:])
+
+    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 attrs for graphviz
+    default_graph_attrs = {
+        'rankdir': 'LR',
+        'size': '"8.0, 12.0"',
+    }
+    default_node_attrs = {
+        'shape': 'box',
+        'fontsize': 10,
+        'height': 0.25,
+        'fontname': 'Vera Sans, DejaVu Sans, Liberation Sans, '
+                    'Arial, Helvetica, sans',
+        'style': '"setlinewidth(0.5)"',
+    }
+    default_edge_attrs = {
+        'arrowsize': 0.5,
+        'style': '"setlinewidth(0.5)"',
+    }
+
+    def _format_node_attrs(self, attrs):
+        return ','.join(['%s=%s' % x for x in attrs.items()])
+
+    def _format_graph_attrs(self, attrs):
+        return ''.join(['%s=%s;\n' % x for x in attrs.items()])
+
+    def generate_dot(self, name, parts=0, urls={}, env=None,
+                     graph_attrs={}, node_attrs={}, edge_attrs={}):
+        """
+        Generate a graphviz dot graph from the classes that
+        were passed in to __init__.
+
+        *name* is the name of the graph.
+
+        *urls* is a dictionary mapping class names to HTTP URLs.
+
+        *graph_attrs*, *node_attrs*, *edge_attrs* are dictionaries containing
+        key/value pairs to pass on as graphviz properties.
+        """
+        g_attrs = self.default_graph_attrs.copy()
+        n_attrs = self.default_node_attrs.copy()
+        e_attrs = self.default_edge_attrs.copy()
+        g_attrs.update(graph_attrs)
+        n_attrs.update(node_attrs)
+        e_attrs.update(edge_attrs)
+        if env:
+            g_attrs.update(env.config.inheritance_graph_attrs)
+            n_attrs.update(env.config.inheritance_node_attrs)
+            e_attrs.update(env.config.inheritance_edge_attrs)
+
+        res = []
+        res.append('digraph %s {\n' % name)
+        res.append(self._format_graph_attrs(g_attrs))
+
+        for cls in self.all_classes:
+            if not self.show_builtins and cls in __builtins__.values():
+                continue
+
+            name = self.class_name(cls, parts)
+
+            # Write the node
+            this_node_attrs = n_attrs.copy()
+            url = urls.get(self.class_name(cls))
+            if url is not None:
+                this_node_attrs['URL'] = '"%s"' % url
+            res.append('  "%s" [%s];\n' %
+                       (name, self._format_node_attrs(this_node_attrs)))
+
+            # 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, parts)
+                res.append('  "%s" -> "%s" [%s];\n' %
+                           (base_name, name,
+                            self._format_node_attrs(e_attrs)))
+        res.append('}\n')
+        return ''.join(res)
+
+
+class inheritance_diagram(nodes.General, nodes.Element):
+    """
+    A docutils node to use as a placeholder for the inheritance diagram.
+    """
+    pass
+
+
+class InheritanceDiagram(Directive):
+    """
+    Run when the inheritance_diagram directive is first encountered.
+    """
+    has_content = False
+    required_arguments = 1
+    optional_arguments = 0
+    final_argument_whitespace = True
+    option_spec = {
+        'parts': directives.nonnegative_int,
+    }
+
+    def run(self):
+        node = inheritance_diagram()
+        node.document = self.state.document
+        env = self.state.document.settings.env
+        class_names = self.arguments[0].split()
+
+        # Create a graph starting with the list of classes
+        try:
+            graph = InheritanceGraph(class_names, env.currmodule)
+        except InheritanceException, err:
+            return [node.document.reporter.warning(err.args[0],
+                                                   line=self.lineno)]
+
+        # 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, self.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['parts'] = self.options.get('parts', 0)
+        node['content'] = ' '.join(class_names)
+        return [node]
+
+
+def get_graph_hash(node):
+    return md5(node['content'] + str(node['parts'])).hexdigest()[-10:]
+
+
+def html_visit_inheritance_diagram(self, node):
+    """
+    Output the graph for HTML.  This will insert a PNG with clickable
+    image map.
+    """
+    graph = node['graph']
+    parts = node['parts']
+
+    graph_hash = get_graph_hash(node)
+    name = 'inheritance%s' % graph_hash
+
+    # Create a mapping from fully-qualified class names to URLs.
+    urls = {}
+    for child in node:
+        if child.get('refuri') is not None:
+            urls[child['reftitle']] = child.get('refuri')
+        elif child.get('refid') is not None:
+            urls[child['reftitle']] = '#' + child.get('refid')
+
+    dotcode = graph.generate_dot(name, parts, urls, env=self.builder.env)
+    render_dot_html(self, node, dotcode, [], 'inheritance', 'inheritance')
+    raise nodes.SkipNode
+
+
+def latex_visit_inheritance_diagram(self, node):
+    """
+    Output the graph for LaTeX.  This will insert a PDF.
+    """
+    graph = node['graph']
+    parts = node['parts']
+
+    graph_hash = get_graph_hash(node)
+    name = 'inheritance%s' % graph_hash
+
+    dotcode = graph.generate_dot(name, parts, urls, env=self.builder.env,
+                                 graph_attrs={'size': '"6.0,6.0"'})
+    render_dot_latex(self, node, dotcode, [], 'inheritance')
+    raise nodes.SkipNode
+
+
+def skip(self, node):
+    raise nodes.SkipNode
+
+
+def setup(app):
+    app.setup_extension('sphinx.ext.graphviz')
+    app.add_node(
+        inheritance_diagram,
+        latex=(latex_visit_inheritance_diagram, None),
+        html=(html_visit_inheritance_diagram, None),
+        text=(skip, None))
+    app.add_directive('inheritance-diagram', InheritanceDiagram)
+    app.add_config_value('inheritance_graph_attrs', {}, False),
+    app.add_config_value('inheritance_node_attrs', {}, False),
+    app.add_config_value('inheritance_edge_attrs', {}, False),