Commits

Georg Brandl committed 8d0ee9b

New ``graphviz`` extension to embed graphviz graphs.

Comments (0)

Files changed (5)

 Other contributors, listed alphabetically, are:
 
 * Daniel Bültmann -- todo extension
+* Charles Duffy -- original graphviz extension
 * Josip Dzolonga -- coverage builder
 * Horst Gutmann -- internationalization support
 * Martin Hans -- autodoc improvements
 
 * Extensions and API:
 
+  - New ``graphviz`` extension to embed graphviz graphs.
+
   - 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()``.

doc/ext/graphviz.rst

+.. highlight:: rest
+
+The Graphviz extension
+======================
+
+.. module:: sphinx.ext.graphviz
+   :synopsis: Support for Graphviz graphs.
+
+.. versionadded:: 0.6
+
+This extension allows you to embed `Graphviz <http://graphviz.org/>`_ graphs in
+your documents.
+
+It adds these directives:
+
+
+.. directive:: graphviz
+
+   Directive to embed graphviz code.  The input code for ``dot`` is given as the
+   content.  For example::
+
+      .. graphviz::
+
+         digraph foo {
+            "bar" -> "baz";
+         }
+
+   In HTML output, the code will be rendered to a PNG image.  In LaTeX output,
+   the code will be rendered to an embeddable PDF file.
+
+
+.. directive:: graph
+
+   Directive for embedding a single undirected graph.  The name is given as a
+   directive argument, the contents of the graph are the directive content.
+   This is a convenience directive to generate ``graph <name> { <content> }``.
+
+   For example::
+
+      .. graph:: foo
+
+         "bar" -- "baz";
+
+
+.. directive:: digraph
+
+   Directive for embedding a single directed graph.  The name is given as a
+   directive argument, the contents of the graph are the directive content.
+   This is a convenience directive to generate ``digraph <name> { <content> }``.
+
+   For example::
+
+      .. digraph:: foo
+
+         "bar" -> "baz" -> "quux";
+
+
+There are also these new config values:
+
+.. confval:: graphviz_dot
+
+   The command name with which to invoke ``dot``.  The default is ``'dot'``; you
+   may need to set this to a full path if ``dot`` is not in the executable
+   search path.
+
+   Since this setting is not portable from system to system, it is normally not
+   useful to set it in ``conf.py``; rather, giving it on the
+   :program:`sphinx-build` command line via the :option:`-D` option should be
+   preferable, like this::
+
+      sphinx-build -b html -D graphviz_dot=C:\graphviz\bin\dot.exe . _build/html
+
+.. confval:: graphviz_dot_args
+
+   Additional command-line arguments to give to dot, as a list.  The default is
+   an empty list.  This is the right place to set global graph, node or edge
+   attributes via dot's ``-G``, ``-N`` and ``-E`` options.

doc/extensions.rst

    ext/doctest
    ext/intersphinx
    ext/math
+   ext/graphviz
+   ext/inheritance
    ext/refcounting
    ext/ifconfig
    ext/coverage

sphinx/ext/graphviz.py

+# -*- coding: utf-8 -*-
+"""
+    sphinx.ext.graphviz
+    ~~~~~~~~~~~~~~~~~~~
+
+    Allow graphviz-formatted graphs to be included in Sphinx-generated
+    documents inline.
+
+    :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import os
+import re
+import sys
+import posixpath
+from os import path
+from subprocess import Popen, PIPE
+try:
+    from hashlib import sha1 as sha
+except ImportError:
+    from sha import sha
+
+from docutils import nodes
+
+from sphinx.errors import SphinxError
+from sphinx.util import ensuredir
+from sphinx.util.compat import Directive
+
+
+mapname_re = re.compile(r'<map id="(.*?)"')
+
+
+class GraphvizError(SphinxError):
+    category = 'Graphviz error'
+
+
+class graphviz(nodes.General, nodes.Element):
+    pass
+
+
+class Graphviz(Directive):
+    """
+    Directive to insert arbitrary dot markup.
+    """
+    has_content = True
+    required_arguments = 0
+    optional_arguments = 0
+    final_argument_whitespace = False
+    option_spec = {}
+
+    def run(self):
+        node = graphviz()
+        node['code'] = '\n'.join(self.content)
+        node['options'] = []
+        return [node]
+
+
+class GraphvizSimple(Directive):
+    """
+    Directive to insert arbitrary dot markup.
+    """
+    has_content = True
+    required_arguments = 1
+    optional_arguments = 0
+    final_argument_whitespace = False
+    option_spec = {}
+
+    def run(self):
+        node = graphviz()
+        node['code'] = '%s %s {\n%s\n}\n' % \
+                       (self.name, self.arguments[0], '\n'.join(self.content))
+        node['options'] = []
+        return [node]
+
+
+def render_dot(self, code, options, format, prefix='graphviz'):
+    """
+    Render graphviz code into a PNG or PDF output file.
+    """
+    hashkey = code.encode('utf-8') + str(options) + \
+              str(self.builder.config.graphviz_dot_args)
+    fname = '%s-%s.%s' % (prefix, sha(hashkey).hexdigest(), format)
+    if hasattr(self.builder, 'imgpath'):
+        # HTML
+        relfn = posixpath.join(self.builder.imgpath, fname)
+        outfn = path.join(self.builder.outdir, '_images', fname)
+    else:
+        # LaTeX
+        relfn = fname
+        outfn = path.join(self.builder.outdir, fname)
+
+    if path.isfile(outfn):
+        return relfn
+
+    if hasattr(self.builder, '_graphviz_warned_dot') or \
+       hasattr(self.builder, '_graphviz_warned_ps2pdf'):
+        return None
+
+    ensuredir(path.dirname(outfn))
+
+    dot_args = [self.builder.config.graphviz_dot]
+    dot_args.extend(self.builder.config.graphviz_dot_args)
+    dot_args.extend(options)
+    dot_args.extend(['-T' + format, '-o' + outfn])
+    if format == 'png':
+        dot_args.extend(['-Tcmapx', '-o%s.map' % outfn])
+    try:
+        p = Popen(dot_args, stdout=PIPE, stdin=PIPE, stderr=PIPE)
+    except OSError, err:
+        if err.errno != 2:   # No such file or directory
+            raise
+        self.builder.warn('dot command %r cannot be run (needed for graphviz '
+                          'output), check the graphviz_dot setting' %
+                          self.builder.config.graphviz_dot)
+        self.builder._graphviz_warned_dot = True
+        return None
+    stdout, stderr = p.communicate(code)
+    if p.returncode != 0:
+        raise GraphvizError('dot exited with error:\n[stderr]\n%s\n'
+                            '[stdout]\n%s' % (stderr, stdout))
+    return relfn
+
+
+def render_dot_html(self, node, code, options, prefix='graphviz', imgcls=None):
+    try:
+        fname = render_dot(self, code, options, 'png', prefix)
+    except GraphvizError, exc:
+        self.builder.warn('dot code %r: ' % code + str(exc))
+        raise nodes.SkipNode
+
+    self.body.append(self.starttag(node, 'p', CLASS='graphviz'))
+    if fname is None:
+        self.body.append(self.encode(code))
+    else:
+        mapfile = open(path.join(self.builder.outdir, fname) + '.map', 'rb')
+        imgmap = mapfile.readlines()
+        mapfile.close()
+        imgcss = imgcls and 'class="%s"' % imgcls or ''
+        if len(imgmap) == 2:
+            # nothing in image map (the lines are <map> and </map>)
+            self.body.append('<img src="%s" alt="%s" %s/>\n' %
+                             (fname, self.encode(code).strip(), imgcss))
+        else:
+            # has a map: get the name of the map and connect the parts
+            mapname = mapname_re.match(imgmap[0]).group(1)
+            self.body.append('<img src="%s" alt="%s" usemap="#%s" %s/>\n' %
+                             (fname, self.encode(code).strip(),
+                              mapname, imgcss))
+            self.body.extend(imgmap)
+    self.body.append('</p>\n')
+    raise nodes.SkipNode
+
+
+def html_visit_graphviz(self, node):
+    render_dot_html(self, node, node['code'], node['options'])
+
+
+def render_dot_latex(self, node, code, options, prefix='graphviz'):
+    try:
+        fname = render_dot(self, code, options, 'pdf', prefix)
+    except GraphvizError, exc:
+        self.builder.warn('dot code %r: ' % code + str(exc))
+        raise nodes.SkipNode
+
+    if fname is not None:
+        self.body.append('\\includegraphics{%s}' % fname)
+    raise nodes.SkipNode
+
+
+def latex_visit_graphviz(self, node):
+    render_dot_latex(self, node, node['code'], node['options'])
+
+def setup(app):
+    app.add_node(graphviz,
+                 html=(html_visit_graphviz, None),
+                 latex=(latex_visit_graphviz, None))
+    app.add_directive('graphviz', Graphviz)
+    app.add_directive('graph', GraphvizSimple)
+    app.add_directive('digraph', GraphvizSimple)
+    app.add_config_value('graphviz_dot', 'dot', 'html')
+    app.add_config_value('graphviz_dot_args', [], 'html')