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