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)
|