Source

sphinx / sphinx / ext / graphviz.py

Full commit
Georg Brandl 8d0ee9b 







Georg Brandl 3a0071d 
Georg Brandl 8d0ee9b 



Georg Brandl 52837ea 
Georg Brandl 8d0ee9b 

Georg Brandl 9826ac3 
Georg Brandl 8d0ee9b 






Georg Brandl 82cd821 
Georg Brandl 8d0ee9b 

Georg Brandl 4ff56bf 
Benoît Allard c61d128 
Georg Brandl 8d0ee9b 



















Georg Brandl 52837ea 
Georg Brandl 8d0ee9b 
Georg Brandl 82cd821 

Doug Hellmann 9c4c1d6 
Doug Hellmann 651fa18 
Georg Brandl 82cd821 
Georg Brandl 8d0ee9b 

Georg Brandl 52837ea 
























Georg Brandl 8d0ee9b 
Georg Brandl 801f913 
Georg Brandl 8d0ee9b 
Georg Brandl 82cd821 

Doug Hellmann 651fa18 

Doug Hellmann 9c4c1d6 
Georg Brandl 8d0ee9b 










Georg Brandl 82cd821 

Doug Hellmann 9c4c1d6 
Doug Hellmann 651fa18 
Georg Brandl 82cd821 
Georg Brandl 8d0ee9b 





Georg Brandl 82cd821 

Doug Hellmann 651fa18 

Doug Hellmann 9c4c1d6 
Georg Brandl 8d0ee9b 



Georg Brandl d3456c0 
rreilink 8aba132 
Ali Afshar 447b5e1 
Georg Brandl 8d0ee9b 
rreilink 8aba132 

Georg Brandl 8d0ee9b 










Georg Brandl 7073334 
Georg Brandl 8d0ee9b 


Georg Brandl 1b41300 
Georg Brandl 8d0ee9b 


Georg Brandl d6faac3 



Georg Brandl 8d0ee9b 








Georg Brandl b53767c 
Georg Brandl 8d0ee9b 




Georg Brandl 1b41300 
Benoît Allard c61d128 
Georg Brandl d6faac3 



Georg Brandl 4856395 
Georg Brandl d6faac3 

Benoît Allard c61d128 





Georg Brandl d6faac3 



Georg Brandl 8d0ee9b 


Georg Brandl 7073334 
Georg Brandl 8d0ee9b 

Georg Brandl cd67597 

Georg Brandl 9826ac3 
Georg Brandl 8d0ee9b 
Georg Brandl 9826ac3 



Georg Brandl 8d0ee9b 



Georg Brandl 345d65d 

Doug Hellmann 9c4c1d6 




Georg Brandl 8d0ee9b 


Georg Brandl cd67597 
Georg Brandl 82cd821 
Benoît Allard 66fa8ee 
Georg Brandl 9826ac3 
Benoît Allard 66fa8ee 
Georg Brandl 9826ac3 
Georg Brandl 8d0ee9b 
Georg Brandl 9826ac3 







Georg Brandl 7e80409 
Georg Brandl 9826ac3 

Kacper Kowalik 5920013 
Georg Brandl 9826ac3 
Georg Brandl 7e80409 
Kacper Kowalik 5920013 
Georg Brandl 345d65d 


Georg Brandl 9826ac3 
Doug Hellmann 9c4c1d6 
Georg Brandl 8d0ee9b 








Bernhard Leiner fd52839 
Georg Brandl 8d0ee9b 



Doug Hellmann 651fa18 

Doug Hellmann 9c4c1d6 



Georg Brandl 8d0ee9b 
Doug Hellmann 651fa18 
Georg Brandl 97f8346 
Georg Brandl 345d65d 
Doug Hellmann 651fa18 

Georg Brandl 345d65d 
Doug Hellmann 651fa18 



Georg Brandl 345d65d 
Georg Brandl b5440a7 
Georg Brandl 8d0ee9b 





Georg Brandl 4ff56bf 
Jon Waltman a1da402 







Jon Waltman 9ee4af8 

Jon Waltman a1da402 








Georg Brandl 4ff56bf 


Robert Lehmann 46bdc37 

Timo Schmid 3ccae6c 
Georg Brandl 4ff56bf 



Robert Lehmann 46bdc37 


Georg Brandl 4ff56bf 


Georg Brandl 8d0ee9b 


Georg Brandl 4ff56bf 
Georg Brandl 5a52427 
Georg Brandl 4ff56bf 

Georg Brandl 8d0ee9b 




Georg Brandl 9826ac3 
# -*- coding: utf-8 -*-
"""
    sphinx.ext.graphviz
    ~~~~~~~~~~~~~~~~~~~

    Allow graphviz-formatted graphs to be included in Sphinx-generated
    documents inline.

    :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import re
import codecs
import posixpath
from os import path
from math import ceil
from subprocess import Popen, PIPE
try:
    from hashlib import sha1 as sha
except ImportError:
    from sha import sha

from docutils import nodes
from docutils.parsers.rst import directives

from sphinx.errors import SphinxError
from sphinx.locale import _
from sphinx.util.osutil import ensuredir, ENOENT, EPIPE, EINVAL
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 = 1
    final_argument_whitespace = False
    option_spec = {
        'alt': directives.unchanged,
        'inline': directives.flag,
        'caption': directives.unchanged,
    }

    def run(self):
        if self.arguments:
            document = self.state.document
            if self.content:
                return [document.reporter.warning(
                    'Graphviz directive cannot have both content and '
                    'a filename argument', line=self.lineno)]
            env = self.state.document.settings.env
            rel_filename, filename = env.relfn2path(self.arguments[0])
            env.note_dependency(rel_filename)
            try:
                fp = codecs.open(filename, 'r', 'utf-8')
                try:
                    dotcode = fp.read()
                finally:
                    fp.close()
            except (IOError, OSError):
                return [document.reporter.warning(
                    'External Graphviz file %r not found or reading '
                    'it failed' % filename, line=self.lineno)]
        else:
            dotcode = '\n'.join(self.content)
            if not dotcode.strip():
                return [self.state_machine.reporter.warning(
                    'Ignoring "graphviz" directive without content.',
                    line=self.lineno)]
        node = graphviz()
        node['code'] = dotcode
        node['options'] = []
        if 'alt' in self.options:
            node['alt'] = self.options['alt']
        if 'caption' in self.options:
            node['caption'] = self.options['caption']
        node['inline'] = 'inline' in self.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 = {
        'alt': directives.unchanged,
        'inline': directives.flag,
        'caption': directives.unchanged,
    }

    def run(self):
        node = graphviz()
        node['code'] = '%s %s {\n%s\n}\n' % \
                       (self.name, self.arguments[0], '\n'.join(self.content))
        node['options'] = []
        if 'alt' in self.options:
            node['alt'] = self.options['alt']
        if 'caption' in self.options:
            node['caption'] = self.options['caption']
        node['inline'] = 'inline' in self.options
        return [node]


def render_dot(self, code, options, format, prefix='graphviz'):
    """Render graphviz code into a PNG or PDF output file."""
    hashkey = (code + str(options) + \
              str(self.builder.config.graphviz_dot) + \
              str(self.builder.config.graphviz_dot_args)
              ).encode('utf-8')
              
    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, outfn

    if hasattr(self.builder, '_graphviz_warned_dot') or \
       hasattr(self.builder, '_graphviz_warned_ps2pdf'):
        return None, None

    ensuredir(path.dirname(outfn))

    # graphviz expects UTF-8 by default
    if isinstance(code, unicode):
        code = code.encode('utf-8')

    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 != ENOENT:   # 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, None
    wentWrong = False
    try:
        # Graphviz may close standard input when an error occurs,
        # resulting in a broken pipe on communicate()
        stdout, stderr = p.communicate(code)
    except (OSError, IOError), err:
        if err.errno != EPIPE:
            raise
        wentWrong = True
    except IOError, err:
        if err.errno != EINVAL:
            raise
        wentWrong = True
    if wentWrong:
        # in this case, read the standard output and standard error streams
        # directly, to get the error message(s)
        stdout, stderr = p.stdout.read(), p.stderr.read()
        p.wait()
    if p.returncode != 0:
        raise GraphvizError('dot exited with error:\n[stderr]\n%s\n'
                            '[stdout]\n%s' % (stderr, stdout))
    return relfn, outfn


def render_dot_html(self, node, code, options, prefix='graphviz',
                    imgcls=None, alt=None):
    format = self.builder.config.graphviz_output_format
    try:
        if format not in ('png', 'svg'):
            raise GraphvizError("graphviz_output_format must be one of 'png', "
                                "'svg', but is %r" % format)
        fname, outfn = render_dot(self, code, options, format, prefix)
    except GraphvizError, exc:
        self.builder.warn('dot code %r: ' % code + str(exc))
        raise nodes.SkipNode

    inline = node.get('inline', False)
    if inline:
        wrapper = 'span'
    else:
        wrapper = 'p'

    self.body.append(self.starttag(node, wrapper, CLASS='graphviz'))
    if fname is None:
        self.body.append(self.encode(code))
    else:
        if alt is None:
            alt = node.get('alt', self.encode(code).strip())
        imgcss = imgcls and 'class="%s"' % imgcls or ''
        if format == 'svg':
            svgtag = '<img src="%s" alt="%s" %s/>\n' % (fname, alt, imgcss)
            self.body.append(svgtag)
        else:
            mapfile = open(outfn + '.map', 'rb')
            try:
                imgmap = mapfile.readlines()
            finally:
                mapfile.close()
            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, alt, imgcss))
            else:
                # has a map: get the name of the map and connect the parts
                mapname = mapname_re.match(imgmap[0].decode('utf-8')).group(1)
                self.body.append('<img src="%s" alt="%s" usemap="#%s" %s/>\n' %
                                 (fname, alt, mapname, imgcss))
                self.body.extend([item.decode('utf-8') for item in imgmap])
        if node.get('caption') and not inline:
            self.body.append('</p>\n<p class="caption">')
            self.body.append(self.encode(node['caption']))

    self.body.append('</%s>\n' % wrapper)
    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, outfn = render_dot(self, code, options, 'pdf', prefix)
    except GraphvizError, exc:
        self.builder.warn('dot code %r: ' % code + str(exc))
        raise nodes.SkipNode

    inline = node.get('inline', False)
    if inline:
        para_separator = ''
    else:
        para_separator = '\n'

    if fname is not None:
        caption = node.get('caption')
        # XXX add ids from previous target node
        if caption and not inline:
            self.body.append('\n\\begin{figure}[h!]')
            self.body.append('\n\\begin{center}')
            self.body.append('\n\\caption{%s}' % self.encode(caption))
            self.body.append('\n\\includegraphics{%s}' % fname)
            self.body.append('\n\\end{center}')
            self.body.append('\n\\end{figure}\n')
        else:
            self.body.append('%s\\includegraphics{%s}%s' %
                             (para_separator, fname, para_separator))
    raise nodes.SkipNode


def latex_visit_graphviz(self, node):
    render_dot_latex(self, node, node['code'], node['options'])


def render_dot_texinfo(self, node, code, options, prefix='graphviz'):
    try:
        fname, outfn = render_dot(self, code, options, 'png', prefix)
    except GraphvizError, exc:
        self.builder.warn('dot code %r: ' % code + str(exc))
        raise nodes.SkipNode
    if fname is not None:
        self.body.append('\n\n@float\n')
        caption = node.get('caption')
        if caption:
            self.body.append('@caption{%s}\n' % self.escape_arg(caption))
        self.body.append('@image{%s,,,[graphviz],png}\n'
                         '@end float\n\n' % fname[:-4])
    raise nodes.SkipNode

def texinfo_visit_graphviz(self, node):
    render_dot_texinfo(self, node, node['code'], node['options'])


def text_visit_graphviz(self, node):
    if 'alt' in node.attributes:
        self.add_text(_('[graph: %s]') % node['alt'])
    else:
        self.add_text(_('[graph]'))
    raise nodes.SkipNode


def man_visit_graphviz(self, node):
    if 'alt' in node.attributes:
        self.body.append(_('[graph: %s]') % node['alt'])
    else:
        self.body.append(_('[graph]'))
    raise nodes.SkipNode


def setup(app):
    app.add_node(graphviz,
                 html=(html_visit_graphviz, None),
                 latex=(latex_visit_graphviz, None),
                 texinfo=(texinfo_visit_graphviz, None),
                 text=(text_visit_graphviz, None),
                 man=(man_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')
    app.add_config_value('graphviz_output_format', 'png', 'html')