# files.py - save, render, view """Save DOT code objects, render with Graphviz dot, and open in viewer.""" import codecs import io import locale import logging import os from ._compat import text_type from . import backend from . import tools __all__ = ['File', 'Source'] log = logging.getLogger(__name__) class Base(object): _engine = 'dot' _format = 'pdf' _encoding = backend.ENCODING @property def engine(self): """The layout commmand used for rendering (``'dot'``, ``'neato'``, ...).""" return self._engine @engine.setter def engine(self, engine): engine = engine.lower() if engine not in backend.ENGINES: raise ValueError('unknown engine: %r' % engine) self._engine = engine @property def format(self): """The output format used for rendering (``'pdf'``, ``'png'``, ...).""" return self._format @format.setter def format(self, format): format = format.lower() if format not in backend.FORMATS: raise ValueError('unknown format: %r' % format) self._format = format @property def encoding(self): """The encoding for the saved source file.""" return self._encoding @encoding.setter def encoding(self, encoding): if encoding is None: encoding = locale.getpreferredencoding() codecs.lookup(encoding) # raise early self._encoding = encoding def copy(self): """Return a copied instance of the object. Returns: An independent copy of the current object. """ kwargs = self._kwargs() return self.__class__(**kwargs) def _kwargs(self): ns = self.__dict__ return {a[1:]: ns[a] for a in ('_format', '_engine', '_encoding') if a in ns} class File(Base): directory = '' _default_extension = 'gv' def __init__(self, filename=None, directory=None, format=None, engine=None, encoding=backend.ENCODING): if filename is None: name = getattr(self, 'name', None) or self.__class__.__name__ filename = '%s.%s' % (name, self._default_extension) self.filename = filename if directory is not None: self.directory = directory if format is not None: self.format = format if engine is not None: self.engine = engine self.encoding = encoding def _kwargs(self): result = super(File, self)._kwargs() result['filename'] = self.filename if 'directory' in self.__dict__: result['directory'] = self.directory return result def __str__(self): """The DOT source code as string.""" return self.source def unflatten(self, stagger=None, fanout=False, chain=None): """Return a new :class:`.Source` instance with the source piped through the Graphviz *unflatten* preprocessor. Args: stagger (int): Stagger the minimum length of leaf edges between 1 and this small integer. fanout (bool): Fanout nodes with indegree = outdegree = 1 when staggering (requires ``stagger``). chain (int): Form disconnected nodes into chains of up to this many nodes. Returns: Source: Prepocessed DOT source code (improved layout aspect ratio). Raises: graphviz.RequiredArgumentError: If ``fanout`` is given but ``stagger`` is None. graphviz.ExecutableNotFound: If the Graphviz unflatten executable is not found. subprocess.CalledProcessError: If the exit status is non-zero. See also: https://www.graphviz.org/pdf/unflatten.1.pdf """ out = backend.unflatten(self.source, stagger=stagger, fanout=fanout, chain=chain, encoding=self._encoding) return Source(out, filename=self.filename, directory=self.directory, format=self._format, engine=self._engine, encoding=self._encoding) def _repr_svg_(self): return self.pipe(format='svg').decode(self._encoding) def pipe(self, format=None, renderer=None, formatter=None, quiet=False): """Return the source piped through the Graphviz layout command. Args: format: The output format used for rendering (``'pdf'``, ``'png'``, etc.). renderer: The output renderer used for rendering (``'cairo'``, ``'gd'``, ...). formatter: The output formatter used for rendering (``'cairo'``, ``'gd'``, ...). quiet (bool): Suppress ``stderr`` output from the layout subprocess. Returns: Binary (encoded) stdout of the layout command. Raises: ValueError: If ``format``, ``renderer``, or ``formatter`` are not known. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. graphviz.ExecutableNotFound: If the Graphviz executable is not found. subprocess.CalledProcessError: If the exit status is non-zero. """ if format is None: format = self._format data = text_type(self.source).encode(self._encoding) out = backend.pipe(self._engine, format, data, renderer=renderer, formatter=formatter, quiet=quiet) return out @property def filepath(self): return os.path.join(self.directory, self.filename) def save(self, filename=None, directory=None): """Save the DOT source to file. Ensure the file ends with a newline. Args: filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) directory: (Sub)directory for source saving and rendering. Returns: The (possibly relative) path of the saved source file. """ if filename is not None: self.filename = filename if directory is not None: self.directory = directory filepath = self.filepath tools.mkdirs(filepath) data = text_type(self.source) log.debug('write %d bytes to %r', len(data), filepath) with io.open(filepath, 'w', encoding=self.encoding) as fd: fd.write(data) if not data.endswith(u'\n'): fd.write(u'\n') return filepath def render(self, filename=None, directory=None, view=False, cleanup=False, format=None, renderer=None, formatter=None, quiet=False, quiet_view=False): """Save the source to file and render with the Graphviz engine. Args: filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) directory: (Sub)directory for source saving and rendering. view (bool): Open the rendered result with the default application. cleanup (bool): Delete the source file after rendering. format: The output format used for rendering (``'pdf'``, ``'png'``, etc.). renderer: The output renderer used for rendering (``'cairo'``, ``'gd'``, ...). formatter: The output formatter used for rendering (``'cairo'``, ``'gd'``, ...). quiet (bool): Suppress ``stderr`` output from the layout subprocess. quiet_view (bool): Suppress ``stderr`` output from the viewer process (implies ``view=True``, ineffective on Windows). Returns: The (possibly relative) path of the rendered file. Raises: ValueError: If ``format``, ``renderer``, or ``formatter`` are not known. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. graphviz.ExecutableNotFound: If the Graphviz executable is not found. subprocess.CalledProcessError: If the exit status is non-zero. RuntimeError: If viewer opening is requested but not supported. The layout command is started from the directory of ``filepath``, so that references to external files (e.g. ``[image=...]``) can be given as paths relative to the DOT source file. """ filepath = self.save(filename, directory) if format is None: format = self._format rendered = backend.render(self._engine, format, filepath, renderer=renderer, formatter=formatter, quiet=quiet) if cleanup: log.debug('delete %r', filepath) os.remove(filepath) if quiet_view or view: self._view(rendered, self._format, quiet_view) return rendered def view(self, filename=None, directory=None, cleanup=False, quiet=False, quiet_view=False): """Save the source to file, open the rendered result in a viewer. Args: filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) directory: (Sub)directory for source saving and rendering. cleanup (bool): Delete the source file after rendering. quiet (bool): Suppress ``stderr`` output from the layout subprocess. quiet_view (bool): Suppress ``stderr`` output from the viewer process (ineffective on Windows). Returns: The (possibly relative) path of the rendered file. Raises: graphviz.ExecutableNotFound: If the Graphviz executable is not found. subprocess.CalledProcessError: If the exit status is non-zero. RuntimeError: If opening the viewer is not supported. Short-cut method for calling :meth:`.render` with ``view=True``. Note: There is no option to wait for the application to close, and no way to retrieve the application's exit status. """ return self.render(filename=filename, directory=directory, view=True, cleanup=cleanup, quiet=quiet, quiet_view=quiet_view) def _view(self, filepath, format, quiet): """Start the right viewer based on file format and platform.""" methodnames = [ '_view_%s_%s' % (format, backend.PLATFORM), '_view_%s' % backend.PLATFORM, ] for name in methodnames: view_method = getattr(self, name, None) if view_method is not None: break else: raise RuntimeError('%r has no built-in viewer support for %r' ' on %r platform' % (self.__class__, format, backend.PLATFORM)) view_method(filepath, quiet) _view_darwin = staticmethod(backend.view.darwin) _view_freebsd = staticmethod(backend.view.freebsd) _view_linux = staticmethod(backend.view.linux) _view_windows = staticmethod(backend.view.windows) class Source(File): """Verbatim DOT source code string to be rendered by Graphviz. Args: source: The verbatim DOT source code string. filename: Filename for saving the source (defaults to ``'Source.gv'``). directory: (Sub)directory for source saving and rendering. format: Rendering output format (``'pdf'``, ``'png'``, ...). engine: Layout command used (``'dot'``, ``'neato'``, ...). encoding: Encoding for saving the source. Note: All parameters except ``source`` are optional. All of them can be changed under their corresponding attribute name after instance creation. """ @classmethod def from_file(cls, filename, directory=None, format=None, engine=None, encoding=backend.ENCODING): """Return an instance with the source string read from the given file. Args: filename: Filename for loading/saving the source. directory: (Sub)directory for source loading/saving and rendering. format: Rendering output format (``'pdf'``, ``'png'``, ...). engine: Layout command used (``'dot'``, ``'neato'``, ...). encoding: Encoding for loading/saving the source. """ filepath = os.path.join(directory or '', filename) if encoding is None: encoding = locale.getpreferredencoding() log.debug('read %r with encoding %r', filepath, encoding) with io.open(filepath, encoding=encoding) as fd: source = fd.read() return cls(source, filename, directory, format, engine, encoding) def __init__(self, source, filename=None, directory=None, format=None, engine=None, encoding=backend.ENCODING): super(Source, self).__init__(filename, directory, format, engine, encoding) self.source = source #: The verbatim DOT source code string. def _kwargs(self): result = super(Source, self)._kwargs() result['source'] = self.source return result