1"""Save DOT code objects, render with Graphviz ``dot``, and open in viewer."""
2
3import logging
4import os
5import pathlib
6import typing
7
8from . import _tools
9from . import backend
10from . import saving
11
12__all__ = ['Render']
13
14
15log = logging.getLogger(__name__)
16
17
18class Render(saving.Save, backend.Render, backend.View):
19 """Write source lines to file and render with Graphviz."""
20
21 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self')
22 def render(self,
23 filename: typing.Union[os.PathLike, str, None] = None,
24 directory: typing.Union[os.PathLike, str, None] = None,
25 view: bool = False,
26 cleanup: bool = False,
27 format: typing.Optional[str] = None,
28 renderer: typing.Optional[str] = None,
29 formatter: typing.Optional[str] = None,
30 neato_no_op: typing.Union[bool, int, None] = None,
31 quiet: bool = False,
32 quiet_view: bool = False, *,
33 outfile: typing.Union[os.PathLike, str, None] = None,
34 engine: typing.Optional[str] = None,
35 raise_if_result_exists: bool = False,
36 overwrite_source: bool = False) -> str:
37 r"""Save the source to file and render with the Graphviz engine.
38
39 Args:
40 filename: Filename for saving the source
41 (defaults to ``name`` + ``'.gv'``).s
42 directory: (Sub)directory for source saving and rendering.
43 view (bool): Open the rendered result
44 with the default application.
45 cleanup (bool): Delete the source file
46 after successful rendering.
47 format: The output format used for rendering
48 (``'pdf'``, ``'png'``, etc.).
49 renderer: The output renderer used for rendering
50 (``'cairo'``, ``'gd'``, ...).
51 formatter: The output formatter used for rendering
52 (``'cairo'``, ``'gd'``, ...).
53 neato_no_op: Neato layout engine no-op flag.
54 quiet (bool): Suppress ``stderr`` output
55 from the layout subprocess.
56 quiet_view (bool): Suppress ``stderr`` output
57 from the viewer process
58 (implies ``view=True``, ineffective on Windows platform).
59 outfile: Path for the rendered output file.
60 engine: Layout engine for rendering
61 (``'dot'``, ``'neato'``, ...).
62 raise_if_result_exists: Raise :exc:`graphviz.FileExistsError`
63 if the result file exists.
64 overwrite_source: Allow ``dot`` to write to the file it reads from.
65 Incompatible with ``raise_if_result_exists``.
66
67 Returns:
68 The (possibly relative) path of the rendered file.
69
70 Raises:
71 ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter``
72 are unknown.
73 graphviz.RequiredArgumentError: If ``formatter`` is given
74 but ``renderer`` is None.
75 ValueError: If ``outfile`` is the same file as the source file
76 unless ``overwite_source=True``.
77 graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable
78 is not found.
79 graphviz.CalledProcessError: If the returncode (exit status)
80 of the rendering ``dot`` subprocess is non-zero.
81 RuntimeError: If viewer opening is requested but not supported.
82
83 Example:
84 >>> doctest_mark_exe()
85 >>> import graphviz
86 >>> dot = graphviz.Graph(name='spam', directory='doctest-output')
87 >>> dot.render(format='png').replace('\\', '/')
88 'doctest-output/spam.gv.png'
89 >>> dot.render(outfile='spam.svg').replace('\\', '/')
90 'doctest-output/spam.svg'
91
92 Note:
93 The layout command is started from the directory of ``filepath``,
94 so that references to external files
95 (e.g. ``[image=images/camelot.png]``)
96 can be given as paths relative to the DOT source file.
97 """
98 outfile = _tools.promote_pathlike(outfile)
99 if outfile is not None:
100 format = self._get_format(outfile, format=format)
101 if directory is None:
102 outfile = pathlib.Path(self.directory, outfile)
103
104 args, kwargs = self._get_render_parameters(engine=engine,
105 format=format,
106 renderer=renderer,
107 formatter=formatter,
108 neato_no_op=neato_no_op,
109 quiet=quiet,
110 outfile=outfile,
111 raise_if_result_exists=raise_if_result_exists,
112 overwrite_source=overwrite_source,
113 verify=True)
114
115 if outfile is not None and filename is None:
116 filename = self._get_filepath(outfile)
117
118 filepath = self.save(filename, directory=directory, skip_existing=None)
119
120 args.append(filepath)
121
122 rendered = self._render(*args, **kwargs)
123
124 if cleanup:
125 log.debug('delete %r', filepath)
126 os.remove(filepath)
127
128 if quiet_view or view:
129 self._view(rendered, format=self._format, quiet=quiet_view)
130
131 return rendered
132
133 def _view(self, filepath: typing.Union[os.PathLike, str], *,
134 format: str, quiet: bool) -> None:
135 """Start the right viewer based on file format and platform."""
136 methodnames = [
137 f'_view_{format}_{backend.viewing.PLATFORM}',
138 f'_view_{backend.viewing.PLATFORM}',
139 ]
140 for name in methodnames:
141 view_method = getattr(self, name, None)
142 if view_method is not None:
143 break
144 else:
145 raise RuntimeError(f'{self.__class__!r} has no built-in viewer'
146 f' support for {format!r}'
147 f' on {backend.viewing.PLATFORM!r} platform')
148 view_method(filepath, quiet=quiet)
149
150 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self')
151 def view(self,
152 filename: typing.Union[os.PathLike, str, None] = None,
153 directory: typing.Union[os.PathLike, str, None] = None,
154 cleanup: bool = False,
155 quiet: bool = False,
156 quiet_view: bool = False) -> str:
157 """Save the source to file, open the rendered result in a viewer.
158
159 Convenience short-cut for running ``.render(view=True)``.
160
161 Args:
162 filename: Filename for saving the source
163 (defaults to ``name`` + ``'.gv'``).
164 directory: (Sub)directory for source saving and rendering.
165 cleanup (bool): Delete the source file after successful rendering.
166 quiet (bool): Suppress ``stderr`` output from the layout subprocess.
167 quiet_view (bool): Suppress ``stderr`` output
168 from the viewer process (ineffective on Windows).
169
170 Returns:
171 The (possibly relative) path of the rendered file.
172
173 Raises:
174 graphviz.ExecutableNotFound: If the Graphviz executable
175 is not found.
176 graphviz.CalledProcessError: If the exit status is non-zero.
177 RuntimeError: If opening the viewer is not supported.
178
179 Short-cut method for calling :meth:`.render` with ``view=True``.
180
181 Note:
182 There is no option to wait for the application to close,
183 and no way to retrieve the application's exit status.
184 """
185 return self.render(filename=filename, directory=directory, view=True,
186 cleanup=cleanup, quiet=quiet, quiet_view=quiet_view)