File: rendering.py

package info (click to toggle)
python-graphviz 0.20.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,188 kB
  • sloc: python: 4,098; makefile: 13
file content (331 lines) | stat: -rw-r--r-- 13,409 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
"""Render DOT source files with Graphviz ``dot``."""

import os
import pathlib
import typing
import warnings

from .._defaults import DEFAULT_SOURCE_EXTENSION
from .. import _tools
from .. import exceptions
from .. import parameters

from . import dot_command
from . import execute

__all__ = ['get_format', 'get_filepath', 'render']


def get_format(outfile: pathlib.Path, *, format: typing.Optional[str]) -> str:
    """Return format inferred from outfile suffix and/or given ``format``.

    Args:
        outfile: Path for the rendered output file.
        format: Output format for rendering (``'pdf'``, ``'png'``, ...).

    Returns:
        The given ``format`` falling back to the inferred format.

    Warns:
        graphviz.UnknownSuffixWarning: If the suffix of ``outfile``
            is empty/unknown.
        graphviz.FormatSuffixMismatchWarning: If the suffix of ``outfile``
            does not match the given ``format``.
    """
    try:
        inferred_format = infer_format(outfile)
    except ValueError:
        if format is None:
            msg = ('cannot infer rendering format'
                   f' from suffix {outfile.suffix!r}'
                   f' of outfile: {os.fspath(outfile)!r}'
                   ' (provide format or outfile with a suffix'
                   f' from {get_supported_suffixes()!r})')
            raise exceptions.RequiredArgumentError(msg)

        warnings.warn(f'unknown outfile suffix {outfile.suffix!r}'
                      f' (expected: {"." + format!r})',
                      category=exceptions.UnknownSuffixWarning)
        return format
    else:
        assert inferred_format is not None
        if format is not None and format.lower() != inferred_format:
            warnings.warn(f'expected format {inferred_format!r} from outfile'
                          f' differs from given format: {format!r}',
                          category=exceptions.FormatSuffixMismatchWarning)
            return format

        return inferred_format


def get_supported_suffixes() -> typing.List[str]:
    """Return a sorted list of supported outfile suffixes for exception/warning messages.

    >>> get_supported_suffixes()  # doctest: +ELLIPSIS
    ['.bmp', ...]
    """
    return [f'.{format}' for format in get_supported_formats()]


def get_supported_formats() -> typing.List[str]:
    """Return a sorted list of supported formats for exception/warning messages.

    >>> get_supported_formats()  # doctest: +ELLIPSIS
    ['bmp', ...]
    """
    return sorted(parameters.FORMATS)


def infer_format(outfile: pathlib.Path) -> str:
    """Return format inferred from outfile suffix.

    Args:
        outfile: Path for the rendered output file.

    Returns:
        The inferred format.

    Raises:
        ValueError: If the suffix of ``outfile`` is empty/unknown.

    >>> infer_format(pathlib.Path('spam.pdf'))  # doctest: +NO_EXE
    'pdf'

    >>> infer_format(pathlib.Path('spam.gv.svg'))
    'svg'

    >>> infer_format(pathlib.Path('spam.PNG'))
    'png'

    >>> infer_format(pathlib.Path('spam'))
    Traceback (most recent call last):
        ...
    ValueError: cannot infer rendering format from outfile: 'spam' (missing suffix)

    >>> infer_format(pathlib.Path('spam.wav'))  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
    Traceback (most recent call last):
        ...
    ValueError: cannot infer rendering format from suffix '.wav' of outfile: 'spam.wav'
    (unknown format: 'wav', provide outfile with a suffix from ['.bmp', ...])
    """
    if not outfile.suffix:
        raise ValueError('cannot infer rendering format from outfile:'
                         f' {os.fspath(outfile)!r} (missing suffix)')

    start, sep, format_ = outfile.suffix.partition('.')
    assert sep and not start, f"{outfile.suffix!r}.startswith('.')"
    format_ = format_.lower()

    try:
        parameters.verify_format(format_)
    except ValueError:
        raise ValueError('cannot infer rendering format'
                         f' from suffix {outfile.suffix!r}'
                         f' of outfile: {os.fspath(outfile)!r}'
                         f' (unknown format: {format_!r},'
                         ' provide outfile with a suffix'
                         f' from {get_supported_suffixes()!r})')
    return format_


def get_outfile(filepath: typing.Union[os.PathLike, str], *,
                format: str,
                renderer: typing.Optional[str] = None,
                formatter: typing.Optional[str] = None) -> pathlib.Path:
    """Return ``filepath`` + ``[[.formatter].renderer].format``.

    See also:
        https://www.graphviz.org/doc/info/command.html#-O
    """
    filepath = _tools.promote_pathlike(filepath)

    parameters.verify_format(format, required=True)
    parameters.verify_renderer(renderer, required=False)
    parameters.verify_formatter(formatter, required=False)

    suffix_args = (formatter, renderer, format)
    suffix = '.'.join(a for a in suffix_args if a is not None)
    return filepath.with_suffix(f'{filepath.suffix}.{suffix}')


def get_filepath(outfile: typing.Union[os.PathLike, str]) -> pathlib.Path:
    """Return ``outfile.with_suffix('.gv')``."""
    outfile = _tools.promote_pathlike(outfile)
    return outfile.with_suffix(f'.{DEFAULT_SOURCE_EXTENSION}')


@typing.overload
def render(engine: str,
           format: str,
           filepath: typing.Union[os.PathLike, str],
           renderer: typing.Optional[str] = ...,
           formatter: typing.Optional[str] = ...,
           neato_no_op: typing.Union[bool, int, None] = ...,
           quiet: bool = ..., *,
           outfile: typing.Union[os.PathLike, str, None] = ...,
           raise_if_result_exists: bool = ...,
           overwrite_filepath: bool = ...) -> str:
    """Require ``format`` and ``filepath`` with default ``outfile=None``."""


@typing.overload
def render(engine: str,
           format: typing.Optional[str] = ...,
           filepath: typing.Union[os.PathLike, str, None] = ...,
           renderer: typing.Optional[str] = ...,
           formatter: typing.Optional[str] = ...,
           neato_no_op: typing.Union[bool, int, None] = ...,
           quiet: bool = False, *,
           outfile: typing.Union[os.PathLike, str, None] = ...,
           raise_if_result_exists: bool = ...,
           overwrite_filepath: bool = ...) -> str:
    """Optional ``format`` and ``filepath`` with given ``outfile``."""


@typing.overload
def render(engine: str,
           format: typing.Optional[str] = ...,
           filepath: typing.Union[os.PathLike, str, None] = ...,
           renderer: typing.Optional[str] = ...,
           formatter: typing.Optional[str] = ...,
           neato_no_op: typing.Union[bool, int, None] = ...,
           quiet: bool = False, *,
           outfile: typing.Union[os.PathLike, str, None] = ...,
           raise_if_result_exists: bool = ...,
           overwrite_filepath: bool = ...) -> str:
    """Required/optional ``format`` and ``filepath`` depending on ``outfile``."""


@_tools.deprecate_positional_args(supported_number=3)
def render(engine: str,
           format: typing.Optional[str] = None,
           filepath: typing.Union[os.PathLike, str, None] = None,
           renderer: typing.Optional[str] = None,
           formatter: typing.Optional[str] = None,
           neato_no_op: typing.Union[bool, int, None] = None,
           quiet: bool = False, *,
           outfile: typing.Union[os.PathLike, str, None] = None,
           raise_if_result_exists: bool = False,
           overwrite_filepath: bool = False) -> str:
    r"""Render file with ``engine`` into ``format`` and return result filename.

    Args:
        engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...).
        format: Output format for rendering (``'pdf'``, ``'png'``, ...).
            Can be omitted if an ``outfile`` with a known ``format`` is given,
            i.e. if ``outfile`` ends  with a known ``.{format}`` suffix.
        filepath: Path to the DOT source file to render.
            Can be omitted if ``outfile`` is given,
            in which case it defaults to ``outfile.with_suffix('.gv')``.
        renderer: Output renderer (``'cairo'``, ``'gd'``, ...).
        formatter: Output formatter (``'cairo'``, ``'gd'``, ...).
        neato_no_op: Neato layout engine no-op flag.
        quiet: Suppress ``stderr`` output from the layout subprocess.
        outfile: Path for the rendered output file.
        raise_if_result_exists: Raise :exc:`graphviz.FileExistsError`
            if the result file exists.
        overwrite_filepath: Allow ``dot`` to write to the file it reads from.
            Incompatible with ``raise_if_result_exists``.

    Returns:
        The (possibly relative) path of the rendered file.

    Raises:
        ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter``
            are unknown.
        graphviz.RequiredArgumentError: If ``format`` or ``filepath`` are None
            unless ``outfile`` is given.
        graphviz.RequiredArgumentError: If ``formatter`` is given
            but ``renderer`` is None.
        ValueError: If ``outfile`` and ``filename`` are the same file
            unless ``overwite_filepath=True``.
        graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable
            is not found.
        graphviz.CalledProcessError: If the returncode (exit status)
            of the rendering ``dot`` subprocess is non-zero.
        graphviz.FileExistsError: If ``raise_if_exists``
            and the result file exists.

    Warns:
        graphviz.UnknownSuffixWarning: If the suffix of ``outfile``
            is empty or unknown.
        graphviz.FormatSuffixMismatchWarning: If the suffix of ``outfile``
            does not match the given ``format``.

    Example:
        >>> doctest_mark_exe()
        >>> import pathlib
        >>> import graphviz
        >>> assert pathlib.Path('doctest-output/spam.gv').write_text('graph { spam }') == 14
        >>> graphviz.render('dot', 'png', 'doctest-output/spam.gv').replace('\\', '/')
        'doctest-output/spam.gv.png'
        >>> graphviz.render('dot', filepath='doctest-output/spam.gv',
        ...                 outfile='doctest-output/spam.png').replace('\\', '/')
        'doctest-output/spam.png'
        >>> graphviz.render('dot', outfile='doctest-output/spam.pdf').replace('\\', '/')
        'doctest-output/spam.pdf'

    Note:
        The layout command is started from the directory of ``filepath``,
        so that references to external files
        (e.g. ``[image=images/camelot.png]``)
        can be given as paths relative to the DOT source file.

    See also:
        Upstream docs: https://www.graphviz.org/doc/info/command.html
    """
    if raise_if_result_exists and overwrite_filepath:
        raise ValueError('overwrite_filepath cannot be combined'
                         ' with raise_if_result_exists')

    filepath, outfile = map(_tools.promote_pathlike, (filepath, outfile))

    if outfile is not None:
        format = get_format(outfile, format=format)

        if filepath is None:
            filepath = get_filepath(outfile)

        if (not overwrite_filepath and outfile.name == filepath.name
            and outfile.resolve() == filepath.resolve()):  # noqa: E129
            raise ValueError(f'outfile {outfile.name!r} must be different'
                             f' from input file {filepath.name!r}'
                             ' (pass overwrite_filepath=True to override)')

        outfile_arg = (outfile.resolve() if outfile.parent != filepath.parent
                       else outfile.name)

        # https://www.graphviz.org/doc/info/command.html#-o
        args = ['-o', outfile_arg, filepath.name]
    elif filepath is None:
        raise exceptions.RequiredArgumentError('filepath: (required if outfile is not given,'
                                               f' got {filepath!r})')
    elif format is None:
        raise exceptions.RequiredArgumentError('format: (required if outfile is not given,'
                                               f' got {format!r})')
    else:
        outfile = get_outfile(filepath,
                              format=format,
                              renderer=renderer,
                              formatter=formatter)
        # https://www.graphviz.org/doc/info/command.html#-O
        args = ['-O', filepath.name]

    cmd = dot_command.command(engine, format,
                              renderer=renderer,
                              formatter=formatter,
                              neato_no_op=neato_no_op)

    if raise_if_result_exists and os.path.exists(outfile):
        raise exceptions.FileExistsError(f'output file exists: {os.fspath(outfile)!r}')

    cmd += args

    assert filepath is not None, 'work around pytype false alarm'

    execute.run_check(cmd,
                      cwd=filepath.parent if filepath.parent.parts else None,
                      quiet=quiet,
                      capture_output=True)

    return os.fspath(outfile)