1# backend.py - execute rendering, open files in viewer 2 3import os 4import re 5import errno 6import platform 7import subprocess 8 9from ._compat import CalledProcessError, stderr_write_bytes 10 11from . import tools 12 13__all__ = [ 14 'render', 'pipe', 'version', 'view', 15 'ENGINES', 'FORMATS', 'RENDERERS', 'FORMATTERS', 16 'ExecutableNotFound', 'RequiredArgumentError', 17] 18 19ENGINES = { # http://www.graphviz.org/pdf/dot.1.pdf 20 'dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage', 21} 22 23FORMATS = { # http://www.graphviz.org/doc/info/output.html 24 'bmp', 25 'canon', 'dot', 'gv', 'xdot', 'xdot1.2', 'xdot1.4', 26 'cgimage', 27 'cmap', 28 'eps', 29 'exr', 30 'fig', 31 'gd', 'gd2', 32 'gif', 33 'gtk', 34 'ico', 35 'imap', 'cmapx', 36 'imap_np', 'cmapx_np', 37 'ismap', 38 'jp2', 39 'jpg', 'jpeg', 'jpe', 40 'json', 'json0', 'dot_json', 'xdot_json', # Graphviz 2.40 41 'pct', 'pict', 42 'pdf', 43 'pic', 44 'plain', 'plain-ext', 45 'png', 46 'pov', 47 'ps', 48 'ps2', 49 'psd', 50 'sgi', 51 'svg', 'svgz', 52 'tga', 53 'tif', 'tiff', 54 'tk', 55 'vml', 'vmlz', 56 'vrml', 57 'wbmp', 58 'webp', 59 'xlib', 60 'x11', 61} 62 63RENDERERS = { # $ dot -T: 64 'cairo', 65 'dot', 66 'fig', 67 'gd', 68 'gdiplus', 69 'map', 70 'pic', 71 'pov', 72 'ps', 73 'svg', 74 'tk', 75 'vml', 76 'vrml', 77 'xdot', 78} 79 80FORMATTERS = {'cairo', 'core', 'gd', 'gdiplus', 'gdwbmp', 'xlib'} 81 82PLATFORM = platform.system().lower() 83 84 85class ExecutableNotFound(RuntimeError): 86 """Exception raised if the Graphviz executable is not found.""" 87 88 _msg = ('failed to execute %r, ' 89 'make sure the Graphviz executables are on your systems\' PATH') 90 91 def __init__(self, args): 92 super(ExecutableNotFound, self).__init__(self._msg % args) 93 94 95class RequiredArgumentError(Exception): 96 """Exception raised if a required argument is missing.""" 97 98 99def command(engine, format, filepath=None, renderer=None, formatter=None): 100 """Return args list for ``subprocess.Popen`` and name of the rendered file.""" 101 if formatter is not None and renderer is None: 102 raise RequiredArgumentError('formatter given without renderer') 103 104 if engine not in ENGINES: 105 raise ValueError('unknown engine: %r' % engine) 106 if format not in FORMATS: 107 raise ValueError('unknown format: %r' % format) 108 if renderer is not None and renderer not in RENDERERS: 109 raise ValueError('unknown renderer: %r' % renderer) 110 if formatter is not None and formatter not in FORMATTERS: 111 raise ValueError('unknown formatter: %r' % formatter) 112 113 format_arg = [s for s in (format, renderer, formatter) if s is not None] 114 suffix = '.'.join(reversed(format_arg)) 115 format_arg = ':'.join(format_arg) 116 117 cmd = [engine, '-T%s' % format_arg] 118 rendered = None 119 if filepath is not None: 120 cmd.extend(['-O', filepath]) 121 rendered = '%s.%s' % (filepath, suffix) 122 123 return cmd, rendered 124 125 126if PLATFORM == 'windows': # pragma: no cover 127 def get_startupinfo(): 128 """Return subprocess.STARTUPINFO instance hiding the console window.""" 129 startupinfo = subprocess.STARTUPINFO() 130 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 131 startupinfo.wShowWindow = subprocess.SW_HIDE 132 return startupinfo 133else: 134 def get_startupinfo(): 135 """Return None for startupinfo argument of ``subprocess.Popen``.""" 136 return None 137 138 139def run(cmd, input=None, capture_output=False, check=False, quiet=False, **kwargs): 140 """Run the command described by cmd and return its (stdout, stderr) tuple.""" 141 if input is not None: 142 kwargs['stdin'] = subprocess.PIPE 143 if capture_output: 144 kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE 145 146 try: 147 proc = subprocess.Popen(cmd, startupinfo=get_startupinfo(), **kwargs) 148 except OSError as e: 149 if e.errno == errno.ENOENT: 150 raise ExecutableNotFound(cmd) 151 else: # pragma: no cover 152 raise 153 154 out, err = proc.communicate(input) 155 156 if not quiet and err: 157 stderr_write_bytes(err, flush=True) 158 if check and proc.returncode: 159 raise CalledProcessError(proc.returncode, cmd, output=out, stderr=err) 160 161 return out, err 162 163 164def render(engine, format, filepath, renderer=None, formatter=None, quiet=False): 165 """Render file with Graphviz ``engine`` into ``format``, return result filename. 166 167 Args: 168 engine: The layout commmand used for rendering (``'dot'``, ``'neato'``, ...). 169 format: The output format used for rendering (``'pdf'``, ``'png'``, ...). 170 filepath: Path to the DOT source file to render. 171 renderer: The output renderer used for rendering (``'cairo'``, ``'gd'``, ...). 172 formatter: The output formatter used for rendering (``'cairo'``, ``'gd'``, ...). 173 quiet (bool): Suppress ``stderr`` output. 174 Returns: 175 The (possibly relative) path of the rendered file. 176 Raises: 177 ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are not known. 178 graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. 179 graphviz.ExecutableNotFound: If the Graphviz executable is not found. 180 subprocess.CalledProcessError: If the exit status is non-zero. 181 """ 182 cmd, rendered = command(engine, format, filepath, renderer, formatter) 183 run(cmd, capture_output=True, check=True, quiet=quiet) 184 return rendered 185 186 187def pipe(engine, format, data, renderer=None, formatter=None, quiet=False): 188 """Return ``data`` piped through Graphviz ``engine`` into ``format``. 189 190 Args: 191 engine: The layout commmand used for rendering (``'dot'``, ``'neato'``, ...). 192 format: The output format used for rendering (``'pdf'``, ``'png'``, ...). 193 data: The binary (encoded) DOT source string to render. 194 renderer: The output renderer used for rendering (``'cairo'``, ``'gd'``, ...). 195 formatter: The output formatter used for rendering (``'cairo'``, ``'gd'``, ...). 196 quiet (bool): Suppress ``stderr`` output. 197 Returns: 198 Binary (encoded) stdout of the layout command. 199 Raises: 200 ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are not known. 201 graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. 202 graphviz.ExecutableNotFound: If the Graphviz executable is not found. 203 subprocess.CalledProcessError: If the exit status is non-zero. 204 """ 205 cmd, _ = command(engine, format, None, renderer, formatter) 206 out, _ = run(cmd, input=data, capture_output=True, check=True, quiet=quiet) 207 return out 208 209 210def version(): 211 """Return the version number tuple from the ``stderr`` output of ``dot -V``. 212 213 Returns: 214 Two or three ``int`` version ``tuple``. 215 Raises: 216 graphviz.ExecutableNotFound: If the Graphviz executable is not found. 217 subprocess.CalledProcessError: If the exit status is non-zero. 218 RuntimmeError: If the output cannot be parsed into a version number. 219 """ 220 cmd = ['dot', '-V'] 221 out, _ = run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 222 223 info = out.decode('ascii') 224 ma = re.search(r'graphviz version (\d+\.\d+(?:\.\d+)?) ', info) 225 if ma is None: 226 raise RuntimeError 227 return tuple(int(d) for d in ma.group(1).split('.')) 228 229 230def view(filepath): 231 """Open filepath with its default viewing application (platform-specific). 232 233 Args: 234 filepath: Path to the file to open in viewer. 235 Raises: 236 RuntimeError: If the current platform is not supported. 237 """ 238 try: 239 view_func = getattr(view, PLATFORM) 240 except AttributeError: 241 raise RuntimeError('platform %r not supported' % PLATFORM) 242 view_func(filepath) 243 244 245@tools.attach(view, 'darwin') 246def view_darwin(filepath): 247 """Open filepath with its default application (mac).""" 248 subprocess.Popen(['open', filepath]) 249 250 251@tools.attach(view, 'linux') 252@tools.attach(view, 'freebsd') 253def view_unixoid(filepath): 254 """Open filepath in the user's preferred application (linux, freebsd).""" 255 subprocess.Popen(['xdg-open', filepath]) 256 257 258@tools.attach(view, 'windows') 259def view_windows(filepath): 260 """Start filepath with its associated application (windows).""" 261 os.startfile(os.path.normpath(filepath)) 262