https://github.com/xflr6/graphviz
Tip revision: 418c18edd9ec92f19aacbbf32c7c134fb08713ea authored by Sebastian Bank on 18 August 2018, 18:13:24 UTC
release 0.9
release 0.9
Tip revision: 418c18e
backend.py
# backend.py - execute rendering, open files in viewer
import os
import io
import re
import sys
import errno
import platform
import subprocess
import contextlib
from ._compat import CalledProcessError, stderr_write_bytes
from . import tools
__all__ = ['render', 'pipe', 'version', 'view']
ENGINES = { # http://www.graphviz.org/pdf/dot.1.pdf
'dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage',
}
FORMATS = { # http://www.graphviz.org/doc/info/output.html
'bmp',
'canon', 'dot', 'gv', 'xdot', 'xdot1.2', 'xdot1.4',
'cgimage',
'cmap',
'eps',
'exr',
'fig',
'gd', 'gd2',
'gif',
'gtk',
'ico',
'imap', 'cmapx',
'imap_np', 'cmapx_np',
'ismap',
'jp2',
'jpg', 'jpeg', 'jpe',
'json', 'json0', 'dot_json', 'xdot_json', # Graphviz 2.40
'pct', 'pict',
'pdf',
'pic',
'plain', 'plain-ext',
'png',
'pov',
'ps',
'ps2',
'psd',
'sgi',
'svg', 'svgz',
'tga',
'tif', 'tiff',
'tk',
'vml', 'vmlz',
'vrml',
'wbmp',
'webp',
'xlib',
'x11',
}
PLATFORM = platform.system().lower()
POPEN_KWARGS = {}
if PLATFORM == 'windows': # pragma: no cover
POPEN_KWARGS['startupinfo'] = subprocess.STARTUPINFO()
POPEN_KWARGS['startupinfo'].dwFlags |= subprocess.STARTF_USESHOWWINDOW
POPEN_KWARGS['startupinfo'].wShowWindow = subprocess.SW_HIDE
# work around WinError 87 from https://bugs.python.org/issue19764
# https://github.com/python/cpython/commit/b2a6083eb0384f38839d3f1ed32262a3852026fa
# TODO: consider not reusing the instance instead (adapt test code for this)
if sys.version_info >= (3, 7):
POPEN_KWARGS['close_fds'] = False
class ExecutableNotFound(RuntimeError):
"""Exception raised if the Graphviz executable is not found."""
_msg = ('failed to execute %r, '
'make sure the Graphviz executables are on your systems\' PATH')
def __init__(self, args):
super(ExecutableNotFound, self).__init__(self._msg % args)
def command(engine, format, filepath=None):
"""Return args list for ``subprocess.Popen`` and name of the rendered file."""
if engine not in ENGINES:
raise ValueError('unknown engine: %r' % engine)
if format not in FORMATS:
raise ValueError('unknown format: %r' % format)
cmd = [engine, '-T%s' % format]
rendered = None
if filepath is not None:
cmd.extend(['-O', filepath])
rendered = '%s.%s' % (filepath, format)
return cmd, rendered
def run(cmd, input=None, capture_output=False, check=False, quiet=False, **kwargs):
if input is not None:
kwargs['stdin'] = subprocess.PIPE
if capture_output:
kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE
kwargs.update(POPEN_KWARGS)
try:
proc = subprocess.Popen(cmd, **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
raise ExecutableNotFound(cmd)
else: # pragma: no cover
raise
out, err = proc.communicate(input)
if not quiet and err:
stderr_write_bytes(err, flush=True)
if check and proc.returncode:
raise CalledProcessError(proc.returncode, cmd, output=out, stderr=err)
return out, err
def render(engine, format, filepath, quiet=False):
"""Render file with Graphviz ``engine`` into ``format``, return result filename.
Args:
engine: The layout commmand used for rendering (``'dot'``, ``'neato'``, ...).
format: The output format used for rendering (``'pdf'``, ``'png'``, ...).
filepath: Path to the DOT source file to render.
quiet (bool): Suppress ``stderr`` output.
Returns:
The (possibly relative) path of the rendered file.
Raises:
ValueError: If ``engine`` or ``format`` are not known.
graphviz.ExecutableNotFound: If the Graphviz executable is not found.
subprocess.CalledProcessError: If the exit status is non-zero.
"""
cmd, rendered = command(engine, format, filepath)
run(cmd, capture_output=True, check=True, quiet=quiet)
return rendered
def pipe(engine, format, data, quiet=False):
"""Return ``data`` piped through Graphviz ``engine`` into ``format``.
Args:
engine: The layout commmand used for rendering (``'dot'``, ``'neato'``, ...).
format: The output format used for rendering (``'pdf'``, ``'png'``, ...).
data: The binary (encoded) DOT source string to render.
quiet (bool): Suppress ``stderr`` output.
Returns:
Binary (encoded) stdout of the layout command.
Raises:
ValueError: If ``engine`` or ``format`` are not known.
graphviz.ExecutableNotFound: If the Graphviz executable is not found.
subprocess.CalledProcessError: If the exit status is non-zero.
"""
cmd, _ = command(engine, format)
out, _ = run(cmd, input=data, capture_output=True, check=True, quiet=quiet)
return out
def version():
"""Return the version number tuple from the ``stderr`` output of ``dot -V``.
Returns:
Two or three ``int`` version ``tuple``.
Raises:
graphviz.ExecutableNotFound: If the Graphviz executable is not found.
subprocess.CalledProcessError: If the exit status is non-zero.
RuntimmeError: If the output cannot be parsed into a version number.
"""
cmd = ['dot', '-V']
out, _ = run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
info = out.decode('ascii')
ma = re.search(r'graphviz version (\d+\.\d+(?:\.\d+)?) ', info)
if ma is None:
raise RuntimeError
return tuple(int(d) for d in ma.group(1).split('.'))
def view(filepath):
"""Open filepath with its default viewing application (platform-specific).
Args:
filepath: Path to the file to open in viewer.
Raises:
RuntimeError: If the current platform is not supported.
"""
try:
view_func = getattr(view, PLATFORM)
except AttributeError:
raise RuntimeError('platform %r not supported' % PLATFORM)
view_func(filepath)
@tools.attach(view, 'darwin')
def view_darwin(filepath):
"""Open filepath with its default application (mac)."""
subprocess.Popen(['open', filepath])
@tools.attach(view, 'linux')
@tools.attach(view, 'freebsd')
def view_unixoid(filepath):
"""Open filepath in the user's preferred application (linux, freebsd)."""
subprocess.Popen(['xdg-open', filepath])
@tools.attach(view, 'windows')
def view_windows(filepath):
"""Start filepath with its associated application (windows)."""
os.startfile(os.path.normpath(filepath))