structure saas with tools

This commit is contained in:
Davidson Gomes
2025-04-25 15:30:54 -03:00
commit 1aef473937
16434 changed files with 6584257 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
"""Execute rendering and unflattening subprocesses, open files in viewer."""
from .dot_command import DOT_BINARY
from .execute import ExecutableNotFound, CalledProcessError
from .mixins import Render, Pipe, Unflatten, View
from .piping import pipe, pipe_string, pipe_lines, pipe_lines_string
from .rendering import render
from .unflattening import UNFLATTEN_BINARY, unflatten
from .upstream_version import version
from .viewing import view
__all__ = ['DOT_BINARY', 'UNFLATTEN_BINARY',
'render',
'pipe', 'pipe_string',
'pipe_lines', 'pipe_lines_string',
'unflatten',
'version',
'view',
'ExecutableNotFound', 'CalledProcessError',
'Render', 'Pipe', 'Unflatten', 'View']

View File

@@ -0,0 +1,44 @@
"""Check and assemble commands for running Graphviz ``dot``."""
import os
import pathlib
import typing
from .. import exceptions
from .. import parameters
__all__ = ['DOT_BINARY', 'command']
DOT_BINARY = pathlib.Path('dot')
def command(engine: str, format_: str, *,
renderer: typing.Optional[str] = None,
formatter: typing.Optional[str] = None,
neato_no_op: typing.Union[bool, int, None] = None
) -> typing.List[typing.Union[os.PathLike, str]]:
"""Return ``subprocess.Popen`` argument list for rendering.
See also:
Upstream documentation:
- https://www.graphviz.org/doc/info/command.html#-K
- https://www.graphviz.org/doc/info/command.html#-T
- https://www.graphviz.org/doc/info/command.html#-n
"""
if formatter is not None and renderer is None:
raise exceptions.RequiredArgumentError('formatter given without renderer')
parameters.verify_engine(engine, required=True)
parameters.verify_format(format_, required=True)
parameters.verify_renderer(renderer, required=False)
parameters.verify_formatter(formatter, required=False)
output_format = [f for f in (format_, renderer, formatter) if f is not None]
output_format_flag = ':'.join(output_format)
cmd = [DOT_BINARY, f'-K{engine}', f'-T{output_format_flag}']
if neato_no_op:
cmd.append(f'-n{neato_no_op:d}')
return cmd

View File

@@ -0,0 +1,132 @@
"""Run subprocesses with ``subprocess.run()`` and ``subprocess.Popen()``."""
import errno
import logging
import os
import subprocess
import sys
import typing
from .. import _compat
__all__ = ['run_check', 'ExecutableNotFound', 'CalledProcessError']
log = logging.getLogger(__name__)
BytesOrStrIterator = typing.Union[typing.Iterator[bytes],
typing.Iterator[str]]
@typing.overload
def run_check(cmd: typing.Sequence[typing.Union[os.PathLike, str]], *,
input_lines: typing.Optional[typing.Iterator[bytes]] = ...,
encoding: None = ...,
quiet: bool = ...,
**kwargs) -> subprocess.CompletedProcess:
"""Accept bytes input_lines with default ``encoding=None```."""
@typing.overload
def run_check(cmd: typing.Sequence[typing.Union[os.PathLike, str]], *,
input_lines: typing.Optional[typing.Iterator[str]] = ...,
encoding: str,
quiet: bool = ...,
**kwargs) -> subprocess.CompletedProcess:
"""Accept string input_lines when given ``encoding``."""
@typing.overload
def run_check(cmd: typing.Sequence[typing.Union[os.PathLike, str]], *,
input_lines: typing.Optional[BytesOrStrIterator] = ...,
encoding: typing.Optional[str] = ...,
capture_output: bool = ...,
quiet: bool = ...,
**kwargs) -> subprocess.CompletedProcess:
"""Accept bytes or string input_lines depending on ``encoding``."""
def run_check(cmd: typing.Sequence[typing.Union[os.PathLike, str]], *,
input_lines: typing.Optional[BytesOrStrIterator] = None,
encoding: typing.Optional[str] = None,
quiet: bool = False,
**kwargs) -> subprocess.CompletedProcess:
"""Run the command described by ``cmd``
with ``check=True`` and return its completed process.
Raises:
CalledProcessError: if the returncode of the subprocess is non-zero.
"""
log.debug('run %r', cmd)
if not kwargs.pop('check', True): # pragma: no cover
raise NotImplementedError('check must be True or omited')
if encoding is not None:
kwargs['encoding'] = encoding
kwargs.setdefault('startupinfo', _compat.get_startupinfo())
try:
if input_lines is not None:
assert kwargs.get('input') is None
assert iter(input_lines) is input_lines
if kwargs.pop('capture_output'):
kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE
proc = _run_input_lines(cmd, input_lines, kwargs=kwargs)
else:
proc = subprocess.run(cmd, **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
raise ExecutableNotFound(cmd) from e
raise
if not quiet and proc.stderr:
_write_stderr(proc.stderr)
try:
proc.check_returncode()
except subprocess.CalledProcessError as e:
raise CalledProcessError(*e.args)
return proc
def _run_input_lines(cmd, input_lines, *, kwargs):
popen = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs)
stdin_write = popen.stdin.write
for line in input_lines:
stdin_write(line)
stdout, stderr = popen.communicate()
return subprocess.CompletedProcess(popen.args, popen.returncode,
stdout=stdout, stderr=stderr)
def _write_stderr(stderr) -> None:
if isinstance(stderr, bytes):
stderr_encoding = (getattr(sys.stderr, 'encoding', None)
or sys.getdefaultencoding())
stderr = stderr.decode(stderr_encoding)
sys.stderr.write(stderr)
sys.stderr.flush()
return None
class ExecutableNotFound(RuntimeError):
""":exc:`RuntimeError` raised if the Graphviz executable is not found."""
_msg = ('failed to execute {!r}, '
'make sure the Graphviz executables are on your systems\' PATH')
def __init__(self, args) -> None:
super().__init__(self._msg.format(*args))
class CalledProcessError(subprocess.CalledProcessError):
""":exc:`~subprocess.CalledProcessError` raised if a subprocess ``returncode`` is not ``0``.""" # noqa: E501
def __str__(self) -> 'str':
return f'{super().__str__()} [stderr: {self.stderr!r}]'

View File

@@ -0,0 +1,76 @@
"""Mixin classes used by Base subclasses to inherit backend functionality."""
import os
import typing
from .. import parameters
from . import piping
from . import rendering
from . import unflattening
from . import viewing
__all__ = ['Render', 'Pipe', 'Unflatten', 'View']
class Render(parameters.Parameters):
"""Parameters for calling and calling ``graphviz.render()``."""
def _get_render_parameters(self,
outfile: typing.Union[os.PathLike, str, None] = None,
raise_if_result_exists: bool = False,
overwrite_source: bool = False,
**kwargs):
kwargs = self._get_parameters(**kwargs)
kwargs.update(outfile=outfile,
raise_if_result_exists=raise_if_result_exists,
overwrite_filepath=overwrite_source)
return [kwargs.pop('engine'), kwargs.pop('format')], kwargs
@property
def _render(_): # noqa: N805
"""Simplify ``._render()`` mocking."""
return rendering.render
class Pipe(parameters.Parameters):
"""Parameters for calling and calling ``graphviz.pipe()``."""
_get_format = staticmethod(rendering.get_format)
_get_filepath = staticmethod(rendering.get_filepath)
def _get_pipe_parameters(self, **kwargs):
kwargs = self._get_parameters(**kwargs)
return [kwargs.pop('engine'), kwargs.pop('format')], kwargs
@property
def _pipe_lines(_): # noqa: N805
"""Simplify ``._pipe_lines()`` mocking."""
return piping.pipe_lines
@property
def _pipe_lines_string(_): # noqa: N805
"""Simplify ``._pipe_lines_string()`` mocking."""
return piping.pipe_lines_string
class Unflatten:
@property
def _unflatten(_): # noqa: N805
"""Simplify ``._unflatten mocking."""
return unflattening.unflatten
class View:
"""Open filepath with its default viewing application
(platform-specific)."""
_view_darwin = staticmethod(viewing.view_darwin)
_view_freebsd = staticmethod(viewing.view_unixoid)
_view_linux = staticmethod(viewing.view_unixoid)
_view_windows = staticmethod(viewing.view_windows)

View File

@@ -0,0 +1,213 @@
"""Pipe bytes, strings, or string iterators through Graphviz ``dot``."""
import typing
from .. import _tools
from . import dot_command
from . import execute
__all__ = ['pipe', 'pipe_string',
'pipe_lines', 'pipe_lines_string']
@_tools.deprecate_positional_args(supported_number=3)
def pipe(engine: str, format: str, data: bytes,
renderer: typing.Optional[str] = None,
formatter: typing.Optional[str] = None,
neato_no_op: typing.Union[bool, int, None] = None,
quiet: bool = False) -> bytes:
"""Return ``data`` (``bytes``) piped through ``engine`` into ``format`` as ``bytes``.
Args:
engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...).
format: Output format for rendering (``'pdf'``, ``'png'``, ...).
data: Binary (encoded) DOT source bytes to render.
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.
Returns:
Binary (encoded) stdout of the layout command.
Raises:
ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter``
are unknown.
graphviz.RequiredArgumentError: If ``formatter`` is given
but ``renderer`` is None.
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.
Example:
>>> doctest_mark_exe()
>>> import graphviz
>>> graphviz.pipe('dot', 'svg', b'graph { hello -- world }')[:14]
b'<?xml version='
Note:
The layout command is started from the current directory.
"""
cmd = dot_command.command(engine, format,
renderer=renderer,
formatter=formatter,
neato_no_op=neato_no_op)
kwargs = {'input': data}
proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
return proc.stdout
def pipe_string(engine: str, format: str, input_string: str, *,
encoding: str,
renderer: typing.Optional[str] = None,
formatter: typing.Optional[str] = None,
neato_no_op: typing.Union[bool, int, None] = None,
quiet: bool = False) -> str:
"""Return ``input_string`` piped through ``engine`` into ``format`` as string.
Args:
engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...).
format: Output format for rendering (``'pdf'``, ``'png'``, ...).
input_string: Binary (encoded) DOT source bytes to render.
encoding: Encoding to en/decode subprocess stdin and stdout (required).
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.
Returns:
Decoded stdout of the layout command.
Raises:
ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter``
are unknown.
graphviz.RequiredArgumentError: If ``formatter`` is given
but ``renderer`` is None.
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.
Example:
>>> doctest_mark_exe()
>>> import graphviz
>>> graphviz.pipe_string('dot', 'svg', 'graph { spam }',
... encoding='ascii')[:14]
'<?xml version='
Note:
The layout command is started from the current directory.
"""
cmd = dot_command.command(engine, format,
renderer=renderer,
formatter=formatter,
neato_no_op=neato_no_op)
kwargs = {'input': input_string, 'encoding': encoding}
proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
return proc.stdout
def pipe_lines(engine: str, format: str, input_lines: typing.Iterator[str], *,
input_encoding: str,
renderer: typing.Optional[str] = None,
formatter: typing.Optional[str] = None,
neato_no_op: typing.Union[bool, int, None] = None,
quiet: bool = False) -> bytes:
r"""Return ``input_lines`` piped through ``engine`` into ``format`` as ``bytes``.
Args:
engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...).
format: Output format for rendering (``'pdf'``, ``'png'``, ...).
input_lines: DOT source lines to render (including final newline).
input_encoding: Encode input_lines for subprocess stdin (required).
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.
Returns:
Binary stdout of the layout command.
Raises:
ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter``
are unknown.
graphviz.RequiredArgumentError: If ``formatter`` is given
but ``renderer`` is None.
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.
Example:
>>> doctest_mark_exe()
>>> import graphviz
>>> graphviz.pipe_lines('dot', 'svg', iter(['graph { spam }\n']),
... input_encoding='ascii')[:14]
b'<?xml version='
Note:
The layout command is started from the current directory.
"""
cmd = dot_command.command(engine, format,
renderer=renderer,
formatter=formatter,
neato_no_op=neato_no_op)
kwargs = {'input_lines': (line.encode(input_encoding) for line in input_lines)}
proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
return proc.stdout
def pipe_lines_string(engine: str, format: str, input_lines: typing.Iterator[str], *,
encoding: str,
renderer: typing.Optional[str] = None,
formatter: typing.Optional[str] = None,
neato_no_op: typing.Union[bool, int, None] = None,
quiet: bool = False) -> str:
r"""Return ``input_lines`` piped through ``engine`` into ``format`` as string.
Args:
engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...).
format: Output format for rendering (``'pdf'``, ``'png'``, ...).
input_lines: DOT source lines to render (including final newline).
encoding: Encoding to en/decode subprocess stdin and stdout (required).
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.
Returns:
Decoded stdout of the layout command.
Raises:
ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter``
are unknown.
graphviz.RequiredArgumentError: If ``formatter`` is given
but ``renderer`` is None.
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.
Example:
>>> doctest_mark_exe()
>>> import graphviz
>>> graphviz.pipe_lines_string('dot', 'svg', iter(['graph { spam }\n']),
... encoding='ascii')[:14]
'<?xml version='
Note:
The layout command is started from the current directory.
"""
cmd = dot_command.command(engine, format,
renderer=renderer,
formatter=formatter,
neato_no_op=neato_no_op)
kwargs = {'input_lines': input_lines, 'encoding': encoding}
proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
return proc.stdout

View File

@@ -0,0 +1,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)

View File

@@ -0,0 +1,63 @@
"""Pipe DOT source code through ``unflatten``."""
import pathlib
import typing
from ..encoding import DEFAULT_ENCODING
from .. import _tools
from .. import exceptions
from . import execute
__all__ = ['UNFLATTEN_BINARY', 'unflatten']
UNFLATTEN_BINARY = pathlib.Path('unflatten')
@_tools.deprecate_positional_args(supported_number=1)
def unflatten(source: str,
stagger: typing.Optional[int] = None,
fanout: bool = False,
chain: typing.Optional[int] = None,
encoding: str = DEFAULT_ENCODING) -> str:
"""Return DOT ``source`` piped through ``unflatten`` preprocessor as string.
Args:
source: DOT source to process
(improve layout aspect ratio).
stagger: Stagger the minimum length of leaf edges
between 1 and this small integer.
fanout: Fanout nodes with indegree = outdegree = 1
when staggering (requires ``stagger``).
chain: Form disconnected nodes into chains of up to this many nodes.
encoding: Encoding to encode unflatten stdin and decode its stdout.
Returns:
Decoded stdout of the Graphviz unflatten command.
Raises:
graphviz.RequiredArgumentError: If ``fanout`` is given
but no ``stagger``.
graphviz.ExecutableNotFound: If the Graphviz 'unflatten' executable
is not found.
graphviz.CalledProcessError: If the returncode (exit status)
of the unflattening 'unflatten' subprocess is non-zero.
See also:
Upstream documentation:
https://www.graphviz.org/pdf/unflatten.1.pdf
"""
if fanout and stagger is None:
raise exceptions.RequiredArgumentError('fanout given without stagger')
cmd = [UNFLATTEN_BINARY]
if stagger is not None:
cmd += ['-l', str(stagger)]
if fanout:
cmd.append('-f')
if chain is not None:
cmd += ['-c', str(chain)]
proc = execute.run_check(cmd, input=source, encoding=encoding,
capture_output=True)
return proc.stdout

View File

@@ -0,0 +1,62 @@
"""Return the version number from running ``dot -V``."""
import logging
import re
import subprocess
import typing
from . import dot_command
from . import execute
VERSION_PATTERN = re.compile(r'''
graphviz[ ]
version[ ]
(\d+)\.(\d+)
(?:\.(\d+)
(?:
~dev\.\d{8}\.\d{4}
|
\.(\d+)
)?
)?
[ ]
''', re.VERBOSE)
log = logging.getLogger(__name__)
def version() -> typing.Tuple[int, ...]:
"""Return the upstream version number tuple from ``stderr`` of ``dot -V``.
Returns:
Two, three, or four ``int`` version ``tuple``.
Raises:
graphviz.ExecutableNotFound: If the Graphviz executable is not found.
graphviz.CalledProcessError: If the exit status is non-zero.
RuntimeError: If the output cannot be parsed into a version number.
Example:
>>> doctest_mark_exe()
>>> import graphviz
>>> graphviz.version() # doctest: +ELLIPSIS
(...)
Note:
Ignores the ``~dev.<YYYYmmdd.HHMM>`` portion of development versions.
See also:
Upstream release version entry format:
https://gitlab.com/graphviz/graphviz/-/blob/f94e91ba819cef51a4b9dcb2d76153684d06a913/gen_version.py#L17-20
"""
cmd = [dot_command.DOT_BINARY, '-V']
proc = execute.run_check(cmd,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
encoding='ascii')
ma = VERSION_PATTERN.search(proc.stdout)
if ma is None:
raise RuntimeError(f'cannot parse {cmd!r} output: {proc.stdout!r}')
return tuple(int(d) for d in ma.groups() if d is not None)

View File

@@ -0,0 +1,71 @@
"""Open files in platform-specific default viewing application."""
import logging
import os
import platform
import subprocess
import typing
from .. import _tools
__all__ = ['view']
PLATFORM = platform.system().lower()
log = logging.getLogger(__name__)
@_tools.deprecate_positional_args(supported_number=1)
def view(filepath: typing.Union[os.PathLike, str],
quiet: bool = False) -> None:
"""Open filepath with its default viewing application (platform-specific).
Args:
filepath: Path to the file to open in viewer.
quiet: Suppress ``stderr`` output
from the viewer process (ineffective on Windows).
Raises:
RuntimeError: If the current platform is not supported.
Note:
There is no option to wait for the application to close,
and no way to retrieve the application's exit status.
"""
try:
view_func = getattr(view, PLATFORM)
except AttributeError:
raise RuntimeError(f'platform {PLATFORM!r} not supported')
view_func(filepath, quiet=quiet)
@_tools.attach(view, 'darwin')
def view_darwin(filepath: typing.Union[os.PathLike, str], *,
quiet: bool) -> None:
"""Open filepath with its default application (mac)."""
cmd = ['open', filepath]
log.debug('view: %r', cmd)
kwargs = {'stderr': subprocess.DEVNULL} if quiet else {}
subprocess.Popen(cmd, **kwargs)
@_tools.attach(view, 'linux')
@_tools.attach(view, 'freebsd')
def view_unixoid(filepath: typing.Union[os.PathLike, str], *,
quiet: bool) -> None:
"""Open filepath in the user's preferred application (linux, freebsd)."""
cmd = ['xdg-open', filepath]
log.debug('view: %r', cmd)
kwargs = {'stderr': subprocess.DEVNULL} if quiet else {}
subprocess.Popen(cmd, **kwargs)
@_tools.attach(view, 'windows')
def view_windows(filepath: typing.Union[os.PathLike, str], *,
quiet: bool) -> None:
"""Start filepath with its associated application (windows)."""
# TODO: implement quiet=True
filepath = os.path.normpath(filepath)
log.debug('view: %r', filepath)
os.startfile(filepath) # pytype: disable=module-attr