Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/graphviz/backend/rendering.py: 52%
82 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:43 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:43 +0000
1"""Render DOT source files with Graphviz ``dot``."""
3import os
4import pathlib
5import typing
6import warnings
8from .._defaults import DEFAULT_SOURCE_EXTENSION
9from .. import _tools
10from .. import exceptions
11from .. import parameters
13from . import dot_command
14from . import execute
16__all__ = ['get_format', 'get_filepath', 'render']
19def get_format(outfile: pathlib.Path, *, format: typing.Optional[str]) -> str:
20 """Return format inferred from outfile suffix and/or given ``format``.
22 Args:
23 outfile: Path for the rendered output file.
24 format: Output format for rendering (``'pdf'``, ``'png'``, ...).
26 Returns:
27 The given ``format`` falling back to the inferred format.
29 Warns:
30 graphviz.UnknownSuffixWarning: If the suffix of ``outfile``
31 is empty/unknown.
32 graphviz.FormatSuffixMismatchWarning: If the suffix of ``outfile``
33 does not match the given ``format``.
34 """
35 try:
36 inferred_format = infer_format(outfile)
37 except ValueError:
38 if format is None:
39 msg = ('cannot infer rendering format'
40 f' from suffix {outfile.suffix!r}'
41 f' of outfile: {os.fspath(outfile)!r}'
42 ' (provide format or outfile with a suffix'
43 f' from {get_supported_suffixes()!r})')
44 raise exceptions.RequiredArgumentError(msg)
46 warnings.warn(f'unknown outfile suffix {outfile.suffix!r}'
47 f' (expected: {"." + format!r})',
48 category=exceptions.UnknownSuffixWarning)
49 return format
50 else:
51 assert inferred_format is not None
52 if format is not None and format.lower() != inferred_format:
53 warnings.warn(f'expected format {inferred_format!r} from outfile'
54 f' differs from given format: {format!r}',
55 category=exceptions.FormatSuffixMismatchWarning)
56 return format
58 return inferred_format
61def get_supported_suffixes() -> typing.List[str]:
62 """Return a sorted list of supported outfile suffixes for exception/warning messages.
64 >>> get_supported_suffixes() # doctest: +ELLIPSIS
65 ['.bmp', ...]
66 """
67 return [f'.{format}' for format in get_supported_formats()]
70def get_supported_formats() -> typing.List[str]:
71 """Return a sorted list of supported formats for exception/warning messages.
73 >>> get_supported_formats() # doctest: +ELLIPSIS
74 ['bmp', ...]
75 """
76 return sorted(parameters.FORMATS)
79def infer_format(outfile: pathlib.Path) -> str:
80 """Return format inferred from outfile suffix.
82 Args:
83 outfile: Path for the rendered output file.
85 Returns:
86 The inferred format.
88 Raises:
89 ValueError: If the suffix of ``outfile`` is empty/unknown.
91 >>> infer_format(pathlib.Path('spam.pdf')) # doctest: +NO_EXE
92 'pdf'
94 >>> infer_format(pathlib.Path('spam.gv.svg'))
95 'svg'
97 >>> infer_format(pathlib.Path('spam.PNG'))
98 'png'
100 >>> infer_format(pathlib.Path('spam'))
101 Traceback (most recent call last):
102 ...
103 ValueError: cannot infer rendering format from outfile: 'spam' (missing suffix)
105 >>> infer_format(pathlib.Path('spam.wav')) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
106 Traceback (most recent call last):
107 ...
108 ValueError: cannot infer rendering format from suffix '.wav' of outfile: 'spam.wav'
109 (unknown format: 'wav', provide outfile with a suffix from ['.bmp', ...])
110 """
111 if not outfile.suffix:
112 raise ValueError('cannot infer rendering format from outfile:'
113 f' {os.fspath(outfile)!r} (missing suffix)')
115 start, sep, format_ = outfile.suffix.partition('.')
116 assert sep and not start, f"{outfile.suffix!r}.startswith('.')"
117 format_ = format_.lower()
119 try:
120 parameters.verify_format(format_)
121 except ValueError:
122 raise ValueError('cannot infer rendering format'
123 f' from suffix {outfile.suffix!r}'
124 f' of outfile: {os.fspath(outfile)!r}'
125 f' (unknown format: {format_!r},'
126 ' provide outfile with a suffix'
127 f' from {get_supported_suffixes()!r})')
128 return format_
131def get_outfile(filepath: typing.Union[os.PathLike, str], *,
132 format: str,
133 renderer: typing.Optional[str] = None,
134 formatter: typing.Optional[str] = None) -> pathlib.Path:
135 """Return ``filepath`` + ``[[.formatter].renderer].format``.
137 See also:
138 https://www.graphviz.org/doc/info/command.html#-O
139 """
140 filepath = _tools.promote_pathlike(filepath)
142 parameters.verify_format(format, required=True)
143 parameters.verify_renderer(renderer, required=False)
144 parameters.verify_formatter(formatter, required=False)
146 suffix_args = (formatter, renderer, format)
147 suffix = '.'.join(a for a in suffix_args if a is not None)
148 return filepath.with_suffix(f'{filepath.suffix}.{suffix}')
151def get_filepath(outfile: typing.Union[os.PathLike, str]) -> pathlib.Path:
152 """Return ``outfile.with_suffix('.gv')``."""
153 outfile = _tools.promote_pathlike(outfile)
154 return outfile.with_suffix(f'.{DEFAULT_SOURCE_EXTENSION}')
157@typing.overload
158def render(engine: str,
159 format: str,
160 filepath: typing.Union[os.PathLike, str],
161 renderer: typing.Optional[str] = ...,
162 formatter: typing.Optional[str] = ...,
163 neato_no_op: typing.Union[bool, int, None] = ...,
164 quiet: bool = ..., *,
165 outfile: typing.Union[os.PathLike, str, None] = ...,
166 raise_if_result_exists: bool = ...,
167 overwrite_filepath: bool = ...) -> str:
168 """Require ``format`` and ``filepath`` with default ``outfile=None``."""
171@typing.overload
172def render(engine: str,
173 format: typing.Optional[str] = ...,
174 filepath: typing.Union[os.PathLike, str, None] = ...,
175 renderer: typing.Optional[str] = ...,
176 formatter: typing.Optional[str] = ...,
177 neato_no_op: typing.Union[bool, int, None] = ...,
178 quiet: bool = False, *,
179 outfile: typing.Union[os.PathLike, str, None] = ...,
180 raise_if_result_exists: bool = ...,
181 overwrite_filepath: bool = ...) -> str:
182 """Optional ``format`` and ``filepath`` with given ``outfile``."""
185@typing.overload
186def render(engine: str,
187 format: typing.Optional[str] = ...,
188 filepath: typing.Union[os.PathLike, str, None] = ...,
189 renderer: typing.Optional[str] = ...,
190 formatter: typing.Optional[str] = ...,
191 neato_no_op: typing.Union[bool, int, None] = ...,
192 quiet: bool = False, *,
193 outfile: typing.Union[os.PathLike, str, None] = ...,
194 raise_if_result_exists: bool = ...,
195 overwrite_filepath: bool = ...) -> str:
196 """Required/optional ``format`` and ``filepath`` depending on ``outfile``."""
199@_tools.deprecate_positional_args(supported_number=3)
200def render(engine: str,
201 format: typing.Optional[str] = None,
202 filepath: typing.Union[os.PathLike, str, None] = None,
203 renderer: typing.Optional[str] = None,
204 formatter: typing.Optional[str] = None,
205 neato_no_op: typing.Union[bool, int, None] = None,
206 quiet: bool = False, *,
207 outfile: typing.Union[os.PathLike, str, None] = None,
208 raise_if_result_exists: bool = False,
209 overwrite_filepath: bool = False) -> str:
210 r"""Render file with ``engine`` into ``format`` and return result filename.
212 Args:
213 engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...).
214 format: Output format for rendering (``'pdf'``, ``'png'``, ...).
215 Can be omitted if an ``outfile`` with a known ``format`` is given,
216 i.e. if ``outfile`` ends with a known ``.{format}`` suffix.
217 filepath: Path to the DOT source file to render.
218 Can be omitted if ``outfile`` is given,
219 in which case it defaults to ``outfile.with_suffix('.gv')``.
220 renderer: Output renderer (``'cairo'``, ``'gd'``, ...).
221 formatter: Output formatter (``'cairo'``, ``'gd'``, ...).
222 neato_no_op: Neato layout engine no-op flag.
223 quiet: Suppress ``stderr`` output from the layout subprocess.
224 outfile: Path for the rendered output file.
225 raise_if_result_exits: Raise :exc:`graphviz.FileExistsError`
226 if the result file exists.
227 overwrite_filepath: Allow ``dot`` to write to the file it reads from.
228 Incompatible with ``raise_if_result_exists``.
230 Returns:
231 The (possibly relative) path of the rendered file.
233 Raises:
234 ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter``
235 are unknown.
236 graphviz.RequiredArgumentError: If ``format`` or ``filepath`` are None
237 unless ``outfile`` is given.
238 graphviz.RequiredArgumentError: If ``formatter`` is given
239 but ``renderer`` is None.
240 ValueError: If ``outfile`` and ``filename`` are the same file
241 unless ``overwite_filepath=True``.
242 graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable
243 is not found.
244 graphviz.CalledProcessError: If the returncode (exit status)
245 of the rendering ``dot`` subprocess is non-zero.
246 graphviz.FileExistsError: If ``raise_if_exists``
247 and the result file exists.
249 Warns:
250 graphviz.UnknownSuffixWarning: If the suffix of ``outfile``
251 is empty or unknown.
252 graphviz.FormatSuffixMismatchWarning: If the suffix of ``outfile``
253 does not match the given ``format``.
255 Example:
256 >>> doctest_mark_exe()
257 >>> import pathlib
258 >>> import graphviz
259 >>> assert pathlib.Path('doctest-output/spam.gv').write_text('graph { spam }') == 14
260 >>> graphviz.render('dot', 'png', 'doctest-output/spam.gv').replace('\\', '/')
261 'doctest-output/spam.gv.png'
262 >>> graphviz.render('dot', filepath='doctest-output/spam.gv',
263 ... outfile='doctest-output/spam.png').replace('\\', '/')
264 'doctest-output/spam.png'
265 >>> graphviz.render('dot', outfile='doctest-output/spam.pdf').replace('\\', '/')
266 'doctest-output/spam.pdf'
268 Note:
269 The layout command is started from the directory of ``filepath``,
270 so that references to external files
271 (e.g. ``[image=images/camelot.png]``)
272 can be given as paths relative to the DOT source file.
274 See also:
275 Upstream docs: https://www.graphviz.org/doc/info/command.html
276 """
277 if raise_if_result_exists and overwrite_filepath:
278 raise ValueError('overwrite_filepath cannot be combined'
279 ' with raise_if_result_exists')
281 filepath, outfile = map(_tools.promote_pathlike, (filepath, outfile))
283 if outfile is not None:
284 format = get_format(outfile, format=format)
286 if filepath is None:
287 filepath = get_filepath(outfile)
289 if (not overwrite_filepath and outfile.name == filepath.name
290 and outfile.resolve() == filepath.resolve()): # noqa: E129
291 raise ValueError(f'outfile {outfile.name!r} must be different'
292 f' from input file {filepath.name!r}'
293 ' (pass overwrite_filepath=True to override)')
295 outfile_arg = (outfile.resolve() if outfile.parent != filepath.parent
296 else outfile.name)
298 # https://www.graphviz.org/doc/info/command.html#-o
299 args = ['-o', outfile_arg, filepath.name]
300 elif filepath is None:
301 raise exceptions.RequiredArgumentError('filepath: (required if outfile is not given,'
302 f' got {filepath!r})')
303 elif format is None:
304 raise exceptions.RequiredArgumentError('format: (required if outfile is not given,'
305 f' got {format!r})')
306 else:
307 outfile = get_outfile(filepath,
308 format=format,
309 renderer=renderer,
310 formatter=formatter)
311 # https://www.graphviz.org/doc/info/command.html#-O
312 args = ['-O', filepath.name]
314 cmd = dot_command.command(engine, format,
315 renderer=renderer,
316 formatter=formatter,
317 neato_no_op=neato_no_op)
319 if raise_if_result_exists and os.path.exists(outfile):
320 raise exceptions.FileExistsError(f'output file exists: {os.fspath(outfile)!r}')
322 cmd += args
324 execute.run_check(cmd,
325 cwd=filepath.parent if filepath.parent.parts else None,
326 quiet=quiet,
327 capture_output=True)
329 return os.fspath(outfile)