structure saas with tools
This commit is contained in:
@@ -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']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
132
.venv/lib/python3.10/site-packages/graphviz/backend/execute.py
Normal file
132
.venv/lib/python3.10/site-packages/graphviz/backend/execute.py
Normal 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}]'
|
||||
@@ -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)
|
||||
213
.venv/lib/python3.10/site-packages/graphviz/backend/piping.py
Normal file
213
.venv/lib/python3.10/site-packages/graphviz/backend/piping.py
Normal 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
|
||||
331
.venv/lib/python3.10/site-packages/graphviz/backend/rendering.py
Normal file
331
.venv/lib/python3.10/site-packages/graphviz/backend/rendering.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user