structure saas with tools
This commit is contained in:
109
.venv/lib/python3.10/site-packages/graphviz/__init__.py
Normal file
109
.venv/lib/python3.10/site-packages/graphviz/__init__.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# graphviz - create dot, save, render, view
|
||||
|
||||
"""Assemble DOT source code and render it with Graphviz.
|
||||
|
||||
Example:
|
||||
>>> import graphviz # doctest: +NO_EXE
|
||||
>>> dot = graphviz.Digraph(comment='The Round Table')
|
||||
|
||||
>>> dot.node('A', 'King Arthur')
|
||||
>>> dot.node('B', 'Sir Bedevere the Wise')
|
||||
>>> dot.node('L', 'Sir Lancelot the Brave')
|
||||
|
||||
>>> dot.edges(['AB', 'AL'])
|
||||
|
||||
>>> dot.edge('B', 'L', constraint='false')
|
||||
|
||||
>>> print(dot) #doctest: +NORMALIZE_WHITESPACE
|
||||
// The Round Table
|
||||
digraph {
|
||||
A [label="King Arthur"]
|
||||
B [label="Sir Bedevere the Wise"]
|
||||
L [label="Sir Lancelot the Brave"]
|
||||
A -> B
|
||||
A -> L
|
||||
B -> L [constraint=false]
|
||||
}
|
||||
"""
|
||||
|
||||
from ._defaults import set_default_engine, set_default_format, set_jupyter_format
|
||||
|
||||
from .backend import (DOT_BINARY, UNFLATTEN_BINARY,
|
||||
render, pipe, pipe_string, pipe_lines, pipe_lines_string,
|
||||
unflatten, version, view)
|
||||
from .exceptions import (ExecutableNotFound, CalledProcessError,
|
||||
RequiredArgumentError, FileExistsError,
|
||||
UnknownSuffixWarning, FormatSuffixMismatchWarning,
|
||||
DotSyntaxWarning)
|
||||
from .graphs import Graph, Digraph
|
||||
from .jupyter_integration import SUPPORTED_JUPYTER_FORMATS
|
||||
from .parameters import ENGINES, FORMATS, RENDERERS, FORMATTERS
|
||||
from .quoting import escape, nohtml
|
||||
from .sources import Source
|
||||
|
||||
__all__ = ['ENGINES', 'FORMATS', 'RENDERERS', 'FORMATTERS',
|
||||
'DOT_BINARY', 'UNFLATTEN_BINARY',
|
||||
'SUPPORTED_JUPYTER_FORMATS',
|
||||
'Graph', 'Digraph',
|
||||
'Source',
|
||||
'escape', 'nohtml',
|
||||
'render', 'pipe', 'pipe_string', 'pipe_lines', 'pipe_lines_string',
|
||||
'unflatten', 'version', 'view',
|
||||
'ExecutableNotFound', 'CalledProcessError',
|
||||
'RequiredArgumentError', 'FileExistsError',
|
||||
'UnknownSuffixWarning', 'FormatSuffixMismatchWarning',
|
||||
'DotSyntaxWarning',
|
||||
'set_default_engine', 'set_default_format', 'set_jupyter_format']
|
||||
|
||||
__title__ = 'graphviz'
|
||||
__version__ = '0.20.3'
|
||||
__author__ = 'Sebastian Bank <sebastian.bank@uni-leipzig.de>'
|
||||
__license__ = 'MIT, see LICENSE.txt'
|
||||
__copyright__ = 'Copyright (c) 2013-2024 Sebastian Bank'
|
||||
|
||||
ENGINES = ENGINES
|
||||
""":class:`set` of known layout commands used for rendering
|
||||
(``'dot'``, ``'neato'``, ...)."""
|
||||
|
||||
FORMATS = FORMATS
|
||||
""":class:`set` of known output formats for rendering
|
||||
(``'pdf'``, ``'png'``, ...)."""
|
||||
|
||||
RENDERERS = RENDERERS
|
||||
""":class:`set` of known output renderers for rendering
|
||||
(``'cairo'``, ``'gd'``, ...)."""
|
||||
|
||||
FORMATTERS = FORMATTERS
|
||||
""":class:`set` of known output formatters for rendering
|
||||
(``'cairo'``, ``'gd'``, ...)."""
|
||||
|
||||
SUPPORTED_JUPYTER_FORMATS = SUPPORTED_JUPYTER_FORMATS
|
||||
""":class:`set` of supported formats for ``_repr_mimebundle_()``
|
||||
(``'svg'``, ``'png'``, ...)."""
|
||||
|
||||
DOT_BINARY = DOT_BINARY
|
||||
""":class:`pathlib.Path` of rendering command (``Path('dot')``)."""
|
||||
|
||||
UNFLATTEN_BINARY = UNFLATTEN_BINARY
|
||||
""":class:`pathlib.Path` of unflatten command (``Path('unflatten')``)."""
|
||||
|
||||
|
||||
ExecutableNotFound = ExecutableNotFound
|
||||
|
||||
|
||||
CalledProcessError = CalledProcessError
|
||||
|
||||
|
||||
RequiredArgumentError = RequiredArgumentError
|
||||
|
||||
|
||||
FileExistsError = FileExistsError
|
||||
|
||||
|
||||
UnknownSuffixWarning = UnknownSuffixWarning
|
||||
|
||||
|
||||
FormatSuffixMismatchWarning = FormatSuffixMismatchWarning
|
||||
|
||||
|
||||
DotSyntaxWarning = DotSyntaxWarning
|
||||
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.
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.
33
.venv/lib/python3.10/site-packages/graphviz/_compat.py
Normal file
33
.venv/lib/python3.10/site-packages/graphviz/_compat.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Python 3.8 compatibility and platform compatibility."""
|
||||
|
||||
import platform
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 9): # pragma: no cover
|
||||
# pytype not supported
|
||||
import unittest.mock
|
||||
|
||||
Literal = unittest.mock.MagicMock(name='Literal')
|
||||
else: # pragma: no cover
|
||||
from typing import Literal
|
||||
|
||||
Literal = Literal # CAVEAT: use None instead of Literal[None]
|
||||
|
||||
|
||||
def get_startupinfo() -> None:
|
||||
"""Return None for startupinfo argument of ``subprocess.Popen``."""
|
||||
return None
|
||||
|
||||
|
||||
assert get_startupinfo() is None, 'get_startupinfo() defaults to a no-op'
|
||||
|
||||
|
||||
if platform.system() == 'Windows': # pragma: no cover
|
||||
import subprocess
|
||||
|
||||
def get_startupinfo() -> subprocess.STARTUPINFO: # pytype: disable=module-attr
|
||||
"""Return subprocess.STARTUPINFO instance hiding the console window."""
|
||||
startupinfo = subprocess.STARTUPINFO() # pytype: disable=module-attr
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # pytype: disable=module-attr
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE # pytype: disable=module-attr
|
||||
return startupinfo
|
||||
70
.venv/lib/python3.10/site-packages/graphviz/_defaults.py
Normal file
70
.venv/lib/python3.10/site-packages/graphviz/_defaults.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Set package-wide default parameters and IPython/Jupyter display format."""
|
||||
|
||||
__all_ = ['DEFAULT_SOURCE_EXTENSION',
|
||||
'set_default_engine', 'set_default_format', 'set_jupyter_format']
|
||||
|
||||
DEFAULT_SOURCE_EXTENSION = 'gv'
|
||||
|
||||
|
||||
def set_default_engine(engine: str) -> str:
|
||||
"""Change the default ``engine`` and return the old default value.
|
||||
|
||||
Args:
|
||||
engine: new default ``engine``
|
||||
used by all present and newly created instances
|
||||
without explicitly set ``engine``
|
||||
(``'dot'``, ``'neato'``, ...).
|
||||
|
||||
Returns:
|
||||
The old default value used for ``engine``.
|
||||
"""
|
||||
from . import parameters
|
||||
|
||||
parameters.verify_engine(engine)
|
||||
|
||||
old_default_engine = parameters.Parameters._engine
|
||||
parameters.Parameters._engine = engine
|
||||
return old_default_engine
|
||||
|
||||
|
||||
def set_default_format(format: str) -> str:
|
||||
"""Change the default ``format`` and return the old default value.
|
||||
|
||||
Args:
|
||||
format: new default ``format``
|
||||
used by all present and newly created instances
|
||||
without explicitly set ``format``
|
||||
(``'pdf'``, ``'png'``, ...).
|
||||
|
||||
Returns:
|
||||
The old default value used for ``format``.
|
||||
"""
|
||||
from . import parameters
|
||||
|
||||
parameters.verify_format(format)
|
||||
|
||||
old_default_format = parameters.Parameters._format
|
||||
parameters.Parameters._format = format
|
||||
return old_default_format
|
||||
|
||||
|
||||
def set_jupyter_format(jupyter_format: str) -> str:
|
||||
"""Change the default mimetype format for ``_repr_mimebundle_()`` and return the old value.
|
||||
|
||||
Args:
|
||||
jupyter_format: new default IPython/Jupyter display format
|
||||
used by all present and newly created instances
|
||||
(``'svg'``, ``'png'``, ...).
|
||||
|
||||
Returns:
|
||||
The old default value used for IPython/Jupyter display format.
|
||||
"""
|
||||
from . import jupyter_integration
|
||||
|
||||
mimetype = jupyter_integration.get_jupyter_format_mimetype(jupyter_format)
|
||||
|
||||
old_mimetype = jupyter_integration.JupyterIntegration._jupyter_mimetype
|
||||
old_format = jupyter_integration.get_jupyter_mimetype_format(old_mimetype)
|
||||
|
||||
jupyter_integration.JupyterIntegration._jupyter_mimetype = mimetype
|
||||
return old_format
|
||||
175
.venv/lib/python3.10/site-packages/graphviz/_tools.py
Normal file
175
.venv/lib/python3.10/site-packages/graphviz/_tools.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""Generic re-useable self-contained helper functions."""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
__all__ = ['attach',
|
||||
'mkdirs',
|
||||
'mapping_items',
|
||||
'promote_pathlike',
|
||||
'promote_pathlike_directory',
|
||||
'deprecate_positional_args']
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def attach(object: typing.Any, /, name: str) -> typing.Callable:
|
||||
"""Return a decorator doing ``setattr(object, name)`` with its argument.
|
||||
|
||||
>>> spam = type('Spam', (object,), {})() # doctest: +NO_EXE
|
||||
|
||||
>>> @attach(spam, 'eggs')
|
||||
... def func():
|
||||
... pass
|
||||
|
||||
>>> spam.eggs # doctest: +ELLIPSIS
|
||||
<function func at 0x...>
|
||||
"""
|
||||
def decorator(func):
|
||||
setattr(object, name, func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def mkdirs(filename: typing.Union[os.PathLike, str], /, *, mode: int = 0o777) -> None:
|
||||
"""Recursively create directories up to the path of ``filename``
|
||||
as needed."""
|
||||
dirname = os.path.dirname(filename)
|
||||
if not dirname:
|
||||
return
|
||||
log.debug('os.makedirs(%r)', dirname)
|
||||
os.makedirs(dirname, mode=mode, exist_ok=True)
|
||||
|
||||
|
||||
def mapping_items(mapping, /):
|
||||
"""Return an iterator over the ``mapping`` items,
|
||||
sort if it's a plain dict.
|
||||
|
||||
>>> list(mapping_items({'spam': 0, 'ham': 1, 'eggs': 2})) # doctest: +NO_EXE
|
||||
[('eggs', 2), ('ham', 1), ('spam', 0)]
|
||||
|
||||
>>> from collections import OrderedDict
|
||||
>>> list(mapping_items(OrderedDict(enumerate(['spam', 'ham', 'eggs']))))
|
||||
[(0, 'spam'), (1, 'ham'), (2, 'eggs')]
|
||||
"""
|
||||
result = iter(mapping.items())
|
||||
if type(mapping) is dict:
|
||||
result = iter(sorted(result))
|
||||
return result
|
||||
|
||||
|
||||
@typing.overload
|
||||
def promote_pathlike(filepath: typing.Union[os.PathLike, str], /) -> pathlib.Path:
|
||||
"""Return path object for path-like-object."""
|
||||
|
||||
|
||||
@typing.overload
|
||||
def promote_pathlike(filepath: None, /) -> None:
|
||||
"""Return None for None."""
|
||||
|
||||
|
||||
@typing.overload
|
||||
def promote_pathlike(filepath: typing.Union[os.PathLike, str, None], /,
|
||||
) -> typing.Optional[pathlib.Path]:
|
||||
"""Return path object or ``None`` depending on ``filepath``."""
|
||||
|
||||
|
||||
def promote_pathlike(filepath: typing.Union[os.PathLike, str, None]
|
||||
) -> typing.Optional[pathlib.Path]:
|
||||
"""Return path-like object ``filepath`` promoted into a path object.
|
||||
|
||||
See also:
|
||||
https://docs.python.org/3/glossary.html#term-path-like-object
|
||||
"""
|
||||
return pathlib.Path(filepath) if filepath is not None else None
|
||||
|
||||
|
||||
def promote_pathlike_directory(directory: typing.Union[os.PathLike, str, None], /, *,
|
||||
default: typing.Union[os.PathLike, str, None] = None,
|
||||
) -> pathlib.Path:
|
||||
"""Return path-like object ``directory`` promoted into a path object (default to ``os.curdir``).
|
||||
|
||||
See also:
|
||||
https://docs.python.org/3/glossary.html#term-path-like-object
|
||||
"""
|
||||
return pathlib.Path(directory if directory is not None
|
||||
else default or os.curdir)
|
||||
|
||||
|
||||
def deprecate_positional_args(*,
|
||||
supported_number: int,
|
||||
category: typing.Type[Warning] = PendingDeprecationWarning,
|
||||
stacklevel: int = 1):
|
||||
"""Mark supported_number of positional arguments as the maximum.
|
||||
|
||||
Args:
|
||||
supported_number: Number of positional arguments
|
||||
for which no warning is raised.
|
||||
category: Type of Warning to raise
|
||||
or None to return a nulldecorator
|
||||
returning the undecorated function.
|
||||
stacklevel: See :func:`warning.warn`.
|
||||
|
||||
Returns:
|
||||
Return a decorator raising a category warning
|
||||
on more than supported_number positional args.
|
||||
|
||||
See also:
|
||||
https://docs.python.org/3/library/exceptions.html#FutureWarning
|
||||
https://docs.python.org/3/library/exceptions.html#DeprecationWarning
|
||||
https://docs.python.org/3/library/exceptions.html#PendingDeprecationWarning
|
||||
"""
|
||||
assert supported_number > 0, f'supported_number at least one: {supported_number!r}'
|
||||
|
||||
if category is None:
|
||||
def nulldecorator(func):
|
||||
"""Return the undecorated function."""
|
||||
return func
|
||||
|
||||
return nulldecorator
|
||||
|
||||
assert issubclass(category, Warning)
|
||||
|
||||
stacklevel += 1
|
||||
|
||||
def decorator(func):
|
||||
signature = inspect.signature(func)
|
||||
argnames = [name for name, param in signature.parameters.items()
|
||||
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
|
||||
log.debug('deprecate positional args: %s.%s(%r)',
|
||||
func.__module__, func.__qualname__,
|
||||
argnames[supported_number:])
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if len(args) > supported_number:
|
||||
call_args = zip(argnames, args)
|
||||
supported = itertools.islice(call_args, supported_number)
|
||||
supported = dict(supported)
|
||||
deprecated = dict(call_args)
|
||||
assert deprecated
|
||||
func_name = func.__name__.lstrip('_')
|
||||
func_name, sep, rest = func_name.partition('_legacy')
|
||||
assert not set or not rest
|
||||
wanted = ', '.join(f'{name}={value!r}'
|
||||
for name, value in deprecated.items())
|
||||
warnings.warn(f'The signature of {func.__name__} will be reduced'
|
||||
f' to {supported_number} positional args'
|
||||
f' {list(supported)}: pass {wanted}'
|
||||
' as keyword arg(s)',
|
||||
stacklevel=stacklevel,
|
||||
category=category)
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@@ -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
|
||||
32
.venv/lib/python3.10/site-packages/graphviz/base.py
Normal file
32
.venv/lib/python3.10/site-packages/graphviz/base.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Iterables of DOT source code lines (including final newline)."""
|
||||
|
||||
import typing
|
||||
|
||||
from . import copying
|
||||
|
||||
__all__ = ['Base']
|
||||
|
||||
|
||||
class LineIterable:
|
||||
"""Iterable of DOT Source code lines
|
||||
(mimics ``file`` objects in text mode)."""
|
||||
|
||||
def __iter__(self) -> typing.Iterator[str]: # pragma: no cover
|
||||
r"""Yield the generated DOT source line by line.
|
||||
|
||||
Yields: Line ending with a newline (``'\n'``).
|
||||
"""
|
||||
raise NotImplementedError('to be implemented by concrete subclasses')
|
||||
|
||||
|
||||
# Common base interface for all exposed classes
|
||||
class Base(LineIterable, copying.CopyBase):
|
||||
"""LineIterator with ``.source`` attribute, that it returns for ``str()``."""
|
||||
|
||||
@property
|
||||
def source(self) -> str: # pragma: no cover
|
||||
raise NotImplementedError('to be implemented by concrete subclasses')
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""The DOT source code as string."""
|
||||
return self.source
|
||||
20
.venv/lib/python3.10/site-packages/graphviz/copying.py
Normal file
20
.venv/lib/python3.10/site-packages/graphviz/copying.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Create new instance copies with cooperative ``super()`` calls."""
|
||||
|
||||
__all__ = ['CopyBase']
|
||||
|
||||
|
||||
class CopyBase:
|
||||
"""Create new instance copies with cooperative ``super()`` calls."""
|
||||
|
||||
def copy(self):
|
||||
"""Return a copied instance of the object.
|
||||
|
||||
Returns:
|
||||
An independent copy of the current object.
|
||||
"""
|
||||
kwargs = self._copy_kwargs()
|
||||
return self.__class__(**kwargs)
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
return kwargs
|
||||
344
.venv/lib/python3.10/site-packages/graphviz/dot.py
Normal file
344
.venv/lib/python3.10/site-packages/graphviz/dot.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""Create DOT code with method-calls."""
|
||||
|
||||
import contextlib
|
||||
import typing
|
||||
|
||||
from . import _tools
|
||||
from . import base
|
||||
from . import quoting
|
||||
|
||||
__all__ = ['GraphSyntax', 'DigraphSyntax', 'Dot']
|
||||
|
||||
|
||||
def comment(line: str) -> str:
|
||||
"""Return comment header line."""
|
||||
return f'// {line}\n'
|
||||
|
||||
|
||||
def graph_head(name: str) -> str:
|
||||
"""Return DOT graph head line."""
|
||||
return f'graph {name}{{\n'
|
||||
|
||||
|
||||
def digraph_head(name: str) -> str:
|
||||
"""Return DOT digraph head line."""
|
||||
return f'digraph {name}{{\n'
|
||||
|
||||
|
||||
def graph_edge(*, tail: str, head: str, attr: str) -> str:
|
||||
"""Return DOT graph edge statement line."""
|
||||
return f'\t{tail} -- {head}{attr}\n'
|
||||
|
||||
|
||||
def digraph_edge(*, tail: str, head: str, attr: str) -> str:
|
||||
"""Return DOT digraph edge statement line."""
|
||||
return f'\t{tail} -> {head}{attr}\n'
|
||||
|
||||
|
||||
class GraphSyntax:
|
||||
"""DOT graph head and edge syntax."""
|
||||
|
||||
_head = staticmethod(graph_head)
|
||||
|
||||
_edge = staticmethod(graph_edge)
|
||||
|
||||
|
||||
class DigraphSyntax:
|
||||
"""DOT digraph head and edge syntax."""
|
||||
|
||||
_head = staticmethod(digraph_head)
|
||||
|
||||
_edge = staticmethod(digraph_edge)
|
||||
|
||||
|
||||
def subgraph(name: str) -> str:
|
||||
"""Return DOT subgraph head line."""
|
||||
return f'subgraph {name}{{\n'
|
||||
|
||||
|
||||
def subgraph_plain(name: str) -> str:
|
||||
"""Return plain DOT subgraph head line."""
|
||||
return f'{name}{{\n'
|
||||
|
||||
|
||||
def node(left: str, right: str) -> str:
|
||||
"""Return DOT node statement line."""
|
||||
return f'\t{left}{right}\n'
|
||||
|
||||
|
||||
class Dot(quoting.Quote, base.Base):
|
||||
"""Assemble DOT source code."""
|
||||
|
||||
directed: bool
|
||||
|
||||
_comment = staticmethod(comment)
|
||||
|
||||
@staticmethod
|
||||
def _head(name: str) -> str: # pragma: no cover
|
||||
"""Return DOT head line."""
|
||||
raise NotImplementedError('must be implemented by concrete subclasses')
|
||||
|
||||
@classmethod
|
||||
def _head_strict(cls, name: str) -> str:
|
||||
"""Return DOT strict head line."""
|
||||
return f'strict {cls._head(name)}'
|
||||
|
||||
_tail = '}\n'
|
||||
|
||||
_subgraph = staticmethod(subgraph)
|
||||
|
||||
_subgraph_plain = staticmethod(subgraph_plain)
|
||||
|
||||
_node = _attr = staticmethod(node)
|
||||
|
||||
@classmethod
|
||||
def _attr_plain(cls, left: str) -> str:
|
||||
return cls._attr(left, '')
|
||||
|
||||
@staticmethod
|
||||
def _edge(*, tail: str, head: str, attr: str) -> str: # pragma: no cover
|
||||
"""Return DOT edge statement line."""
|
||||
raise NotImplementedError('must be implemented by concrete subclasses')
|
||||
|
||||
@classmethod
|
||||
def _edge_plain(cls, *, tail: str, head: str) -> str:
|
||||
"""Return plain DOT edge statement line."""
|
||||
return cls._edge(tail=tail, head=head, attr='')
|
||||
|
||||
def __init__(self, *,
|
||||
name: typing.Optional[str] = None,
|
||||
comment: typing.Optional[str] = None,
|
||||
graph_attr=None, node_attr=None, edge_attr=None, body=None,
|
||||
strict: bool = False, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.name = name
|
||||
"""str: DOT source identifier for the ``graph`` or ``digraph`` statement."""
|
||||
|
||||
self.comment = comment
|
||||
"""str: DOT source comment for the first source line."""
|
||||
|
||||
self.graph_attr = dict(graph_attr) if graph_attr is not None else {}
|
||||
"""~typing.Dict[str, str]: Attribute-value pairs applying to the graph."""
|
||||
|
||||
self.node_attr = dict(node_attr) if node_attr is not None else {}
|
||||
"""~typing.Dict[str, str]: Attribute-value pairs applying to all nodes."""
|
||||
|
||||
self.edge_attr = dict(edge_attr) if edge_attr is not None else {}
|
||||
"""~typing.Dict[str, str]: Attribute-value pairs applying to all edges."""
|
||||
|
||||
self.body = list(body) if body is not None else []
|
||||
"""~typing.List[str]: Verbatim DOT source lines including final newline."""
|
||||
|
||||
self.strict = strict
|
||||
"""bool: Rendering should merge multi-edges."""
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
return super()._copy_kwargs(name=self.name,
|
||||
comment=self.comment,
|
||||
graph_attr=dict(self.graph_attr),
|
||||
node_attr=dict(self.node_attr),
|
||||
edge_attr=dict(self.edge_attr),
|
||||
body=list(self.body),
|
||||
strict=self.strict)
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=1)
|
||||
def clear(self, keep_attrs: bool = False) -> None:
|
||||
"""Reset content to an empty body, clear graph/node/egde_attr mappings.
|
||||
|
||||
Args:
|
||||
keep_attrs (bool): preserve graph/node/egde_attr mappings
|
||||
"""
|
||||
if not keep_attrs:
|
||||
for a in (self.graph_attr, self.node_attr, self.edge_attr):
|
||||
a.clear()
|
||||
self.body.clear()
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=1)
|
||||
def __iter__(self, subgraph: bool = False) -> typing.Iterator[str]:
|
||||
r"""Yield the DOT source code line by line (as graph or subgraph).
|
||||
|
||||
Yields: Line ending with a newline (``'\n'``).
|
||||
"""
|
||||
if self.comment:
|
||||
yield self._comment(self.comment)
|
||||
|
||||
if subgraph:
|
||||
if self.strict:
|
||||
raise ValueError('subgraphs cannot be strict')
|
||||
head = self._subgraph if self.name else self._subgraph_plain
|
||||
else:
|
||||
head = self._head_strict if self.strict else self._head
|
||||
yield head(self._quote(self.name) + ' ' if self.name else '')
|
||||
|
||||
for kw in ('graph', 'node', 'edge'):
|
||||
attrs = getattr(self, f'{kw}_attr')
|
||||
if attrs:
|
||||
yield self._attr(kw, self._attr_list(None, kwargs=attrs))
|
||||
|
||||
yield from self.body
|
||||
|
||||
yield self._tail
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=3)
|
||||
def node(self, name: str,
|
||||
label: typing.Optional[str] = None,
|
||||
_attributes=None, **attrs) -> None:
|
||||
"""Create a node.
|
||||
|
||||
Args:
|
||||
name: Unique identifier for the node inside the source.
|
||||
label: Caption to be displayed (defaults to the node ``name``).
|
||||
attrs: Any additional node attributes (must be strings).
|
||||
|
||||
Attention:
|
||||
When rendering ``label``, backslash-escapes
|
||||
and strings of the form ``<...>`` have a special meaning.
|
||||
See the sections :ref:`backslash-escapes` and
|
||||
:ref:`quoting-and-html-like-labels` in the user guide for details.
|
||||
"""
|
||||
name = self._quote(name)
|
||||
attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
|
||||
line = self._node(name, attr_list)
|
||||
self.body.append(line)
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=4)
|
||||
def edge(self, tail_name: str, head_name: str,
|
||||
label: typing.Optional[str] = None,
|
||||
_attributes=None, **attrs) -> None:
|
||||
"""Create an edge between two nodes.
|
||||
|
||||
Args:
|
||||
tail_name: Start node identifier
|
||||
(format: ``node[:port[:compass]]``).
|
||||
head_name: End node identifier
|
||||
(format: ``node[:port[:compass]]``).
|
||||
label: Caption to be displayed near the edge.
|
||||
attrs: Any additional edge attributes (must be strings).
|
||||
|
||||
Note:
|
||||
The ``tail_name`` and ``head_name`` strings are separated
|
||||
by (optional) colon(s) into ``node`` name, ``port`` name,
|
||||
and ``compass`` (e.g. ``sw``).
|
||||
See :ref:`details in the User Guide <node-ports-compass>`.
|
||||
|
||||
Attention:
|
||||
When rendering ``label``, backslash-escapes
|
||||
and strings of the form ``<...>`` have a special meaning.
|
||||
See the sections :ref:`backslash-escapes` and
|
||||
:ref:`quoting-and-html-like-labels` in the user guide for details.
|
||||
"""
|
||||
tail_name = self._quote_edge(tail_name)
|
||||
head_name = self._quote_edge(head_name)
|
||||
attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
|
||||
line = self._edge(tail=tail_name, head=head_name, attr=attr_list)
|
||||
self.body.append(line)
|
||||
|
||||
def edges(self, tail_head_iter) -> None:
|
||||
"""Create a bunch of edges.
|
||||
|
||||
Args:
|
||||
tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs
|
||||
(format:``node[:port[:compass]]``).
|
||||
|
||||
|
||||
Note:
|
||||
The ``tail_name`` and ``head_name`` strings are separated
|
||||
by (optional) colon(s) into ``node`` name, ``port`` name,
|
||||
and ``compass`` (e.g. ``sw``).
|
||||
See :ref:`details in the User Guide <node-ports-compass>`.
|
||||
"""
|
||||
edge = self._edge_plain
|
||||
quote = self._quote_edge
|
||||
self.body += [edge(tail=quote(t), head=quote(h))
|
||||
for t, h in tail_head_iter]
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def attr(self, kw: typing.Optional[str] = None,
|
||||
_attributes=None, **attrs) -> None:
|
||||
"""Add a general or graph/node/edge attribute statement.
|
||||
|
||||
Args:
|
||||
kw: Attributes target
|
||||
(``None`` or ``'graph'``, ``'node'``, ``'edge'``).
|
||||
attrs: Attributes to be set (must be strings, may be empty).
|
||||
|
||||
See the :ref:`usage examples in the User Guide <attributes>`.
|
||||
"""
|
||||
if kw is not None and kw.lower() not in ('graph', 'node', 'edge'):
|
||||
raise ValueError('attr statement must target graph, node, or edge:'
|
||||
f' {kw!r}')
|
||||
if attrs or _attributes:
|
||||
if kw is None:
|
||||
a_list = self._a_list(None, kwargs=attrs, attributes=_attributes)
|
||||
line = self._attr_plain(a_list)
|
||||
else:
|
||||
attr_list = self._attr_list(None, kwargs=attrs, attributes=_attributes)
|
||||
line = self._attr(kw, attr_list)
|
||||
self.body.append(line)
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def subgraph(self, graph=None,
|
||||
name: typing.Optional[str] = None,
|
||||
comment: typing.Optional[str] = None,
|
||||
graph_attr=None, node_attr=None, edge_attr=None,
|
||||
body=None):
|
||||
"""Add the current content of the given sole ``graph`` argument
|
||||
as subgraph or return a context manager
|
||||
returning a new graph instance
|
||||
created with the given (``name``, ``comment``, etc.) arguments
|
||||
whose content is added as subgraph
|
||||
when leaving the context manager's ``with``-block.
|
||||
|
||||
Args:
|
||||
graph: An instance of the same kind
|
||||
(:class:`.Graph`, :class:`.Digraph`) as the current graph
|
||||
(sole argument in non-with-block use).
|
||||
name: Subgraph name (``with``-block use).
|
||||
comment: Subgraph comment (``with``-block use).
|
||||
graph_attr: Subgraph-level attribute-value mapping
|
||||
(``with``-block use).
|
||||
node_attr: Node-level attribute-value mapping
|
||||
(``with``-block use).
|
||||
edge_attr: Edge-level attribute-value mapping
|
||||
(``with``-block use).
|
||||
body: Verbatim lines to add to the subgraph ``body``
|
||||
(``with``-block use).
|
||||
|
||||
See the :ref:`usage examples in the User Guide <subgraphs-clusters>`.
|
||||
|
||||
When used as a context manager, the returned new graph instance
|
||||
uses ``strict=None`` and the parent graph's values
|
||||
for ``directory``, ``format``, ``engine``, and ``encoding`` by default.
|
||||
|
||||
Note:
|
||||
If the ``name`` of the subgraph begins with
|
||||
``'cluster'`` (all lowercase)
|
||||
the layout engine will treat it as a special cluster subgraph.
|
||||
"""
|
||||
if graph is None:
|
||||
kwargs = self._copy_kwargs()
|
||||
kwargs.pop('filename', None)
|
||||
kwargs.update(name=name, comment=comment,
|
||||
graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr,
|
||||
body=body, strict=None)
|
||||
subgraph = self.__class__(**kwargs)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def subgraph_contextmanager(*, parent):
|
||||
"""Return subgraph and add to parent on exit."""
|
||||
yield subgraph
|
||||
parent.subgraph(subgraph)
|
||||
|
||||
return subgraph_contextmanager(parent=self)
|
||||
|
||||
args = [name, comment, graph_attr, node_attr, edge_attr, body]
|
||||
if not all(a is None for a in args):
|
||||
raise ValueError('graph must be sole argument of subgraph()')
|
||||
|
||||
if graph.directed != self.directed:
|
||||
raise ValueError(f'{self!r} cannot add subgraph of different kind:'
|
||||
f' {graph!r}')
|
||||
|
||||
self.body += [f'\t{line}' for line in graph.__iter__(subgraph=True)]
|
||||
41
.venv/lib/python3.10/site-packages/graphviz/encoding.py
Normal file
41
.venv/lib/python3.10/site-packages/graphviz/encoding.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Encoding parameter handling and default."""
|
||||
|
||||
import typing
|
||||
|
||||
import codecs
|
||||
import locale
|
||||
|
||||
from . import copying
|
||||
|
||||
__all__ = ['DEFAULT_ENCODING', 'Encoding']
|
||||
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
|
||||
|
||||
class Encoding(copying.CopyBase):
|
||||
"""Encoding used for input and output with ``'utf-8'`` default."""
|
||||
|
||||
_encoding = DEFAULT_ENCODING
|
||||
|
||||
def __init__(self, *, encoding: typing.Optional[str] = DEFAULT_ENCODING,
|
||||
**kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.encoding = encoding
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
return super()._copy_kwargs(encoding=self._encoding, **kwargs)
|
||||
|
||||
@property
|
||||
def encoding(self) -> str:
|
||||
"""The encoding for the saved source file."""
|
||||
return self._encoding
|
||||
|
||||
@encoding.setter
|
||||
def encoding(self, encoding: typing.Optional[str]) -> None:
|
||||
if encoding is None:
|
||||
encoding = locale.getpreferredencoding()
|
||||
|
||||
codecs.lookup(encoding) # raise early
|
||||
self._encoding = encoding
|
||||
31
.venv/lib/python3.10/site-packages/graphviz/exceptions.py
Normal file
31
.venv/lib/python3.10/site-packages/graphviz/exceptions.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Commonly used exception classes."""
|
||||
|
||||
from .backend.execute import ExecutableNotFound, CalledProcessError
|
||||
|
||||
__all__ = ['ExecutableNotFound', 'CalledProcessError',
|
||||
'RequiredArgumentError', 'FileExistsError',
|
||||
'UnknownSuffixWarning', 'FormatSuffixMismatchWarning',
|
||||
'DotSyntaxWarning']
|
||||
|
||||
|
||||
class RequiredArgumentError(TypeError):
|
||||
""":exc:`TypeError` raised if a required argument is missing."""
|
||||
|
||||
|
||||
class FileExistsError(FileExistsError):
|
||||
""":exc:`FileExistsError` raised with ``raise_if_exists=True``."""
|
||||
|
||||
|
||||
class UnknownSuffixWarning(RuntimeWarning):
|
||||
""":exc:`RuntimeWarning` raised if the suffix of ``outfile`` is unknown
|
||||
and the given ``format`` is used instead."""
|
||||
|
||||
|
||||
class FormatSuffixMismatchWarning(UserWarning):
|
||||
""":exc:`UserWarning` raised if the suffix ``outfile``
|
||||
does not match the given ``format``."""
|
||||
|
||||
|
||||
class DotSyntaxWarning(RuntimeWarning):
|
||||
""":exc:`RuntimeWarning` raised if a quoted string
|
||||
is expected to cause a ``CalledProcessError`` from rendering."""
|
||||
123
.venv/lib/python3.10/site-packages/graphviz/graphs.py
Normal file
123
.venv/lib/python3.10/site-packages/graphviz/graphs.py
Normal file
@@ -0,0 +1,123 @@
|
||||
r"""Assemble DOT source code objects.
|
||||
|
||||
Example:
|
||||
>>> doctest_mark_exe()
|
||||
|
||||
>>> import graphviz
|
||||
>>> dot = graphviz.Graph(comment='Mønti Pythøn ik den Hølie Grailen')
|
||||
|
||||
>>> dot.node('Møøse')
|
||||
>>> dot.node('trained_by', 'trained by')
|
||||
>>> dot.node('tutte', 'TUTTE HERMSGERVORDENBROTBORDA')
|
||||
|
||||
>>> dot.edge('Møøse', 'trained_by')
|
||||
>>> dot.edge('trained_by', 'tutte')
|
||||
|
||||
>>> dot.node_attr['shape'] = 'rectangle'
|
||||
|
||||
>>> print(dot.source) #doctest: +NORMALIZE_WHITESPACE
|
||||
// Mønti Pythøn ik den Hølie Grailen
|
||||
graph {
|
||||
node [shape=rectangle]
|
||||
"Møøse"
|
||||
trained_by [label="trained by"]
|
||||
tutte [label="TUTTE HERMSGERVORDENBROTBORDA"]
|
||||
"Møøse" -- trained_by
|
||||
trained_by -- tutte
|
||||
}
|
||||
|
||||
>>> dot.render('doctest-output/m00se.gv').replace('\\', '/')
|
||||
'doctest-output/m00se.gv.pdf'
|
||||
"""
|
||||
|
||||
import typing
|
||||
|
||||
from .encoding import DEFAULT_ENCODING
|
||||
from . import _tools
|
||||
from . import dot
|
||||
from . import jupyter_integration
|
||||
from . import piping
|
||||
from . import rendering
|
||||
from . import unflattening
|
||||
|
||||
__all__ = ['Graph', 'Digraph']
|
||||
|
||||
|
||||
class BaseGraph(dot.Dot,
|
||||
rendering.Render,
|
||||
jupyter_integration.JupyterIntegration, piping.Pipe,
|
||||
unflattening.Unflatten):
|
||||
"""Dot language creation and source code rendering."""
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def __init__(self, name: typing.Optional[str] = None,
|
||||
comment: typing.Optional[str] = None,
|
||||
filename=None, directory=None,
|
||||
format: typing.Optional[str] = None,
|
||||
engine: typing.Optional[str] = None,
|
||||
encoding: typing.Optional[str] = DEFAULT_ENCODING,
|
||||
graph_attr=None, node_attr=None, edge_attr=None,
|
||||
body=None,
|
||||
strict: bool = False, *,
|
||||
renderer: typing.Optional[str] = None,
|
||||
formatter: typing.Optional[str] = None) -> None:
|
||||
if filename is None and name is not None:
|
||||
filename = f'{name}.{self._default_extension}'
|
||||
|
||||
super().__init__(name=name, comment=comment,
|
||||
graph_attr=graph_attr,
|
||||
node_attr=node_attr, edge_attr=edge_attr,
|
||||
body=body, strict=strict,
|
||||
filename=filename, directory=directory,
|
||||
encoding=encoding,
|
||||
format=format, engine=engine,
|
||||
renderer=renderer, formatter=formatter)
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""The generated DOT source code as string."""
|
||||
return ''.join(self)
|
||||
|
||||
|
||||
class Graph(dot.GraphSyntax, BaseGraph):
|
||||
"""Graph source code in the DOT language.
|
||||
|
||||
Args:
|
||||
name: Graph name used in the source code.
|
||||
comment: Comment added to the first line of the source.
|
||||
filename: Filename for saving the source
|
||||
(defaults to ``name`` + ``'.gv'``).
|
||||
directory: (Sub)directory for source saving and rendering.
|
||||
format: Rendering output format (``'pdf'``, ``'png'``, ...).
|
||||
engine: Layout command used (``'dot'``, ``'neato'``, ...).
|
||||
renderer: Output renderer used (``'cairo'``, ``'gd'``, ...).
|
||||
formatter: Output formatter used (``'cairo'``, ``'gd'``, ...).
|
||||
encoding: Encoding for saving the source.
|
||||
graph_attr: Mapping of ``(attribute, value)`` pairs for the graph.
|
||||
node_attr: Mapping of ``(attribute, value)`` pairs set for all nodes.
|
||||
edge_attr: Mapping of ``(attribute, value)`` pairs set for all edges.
|
||||
body: Iterable of verbatim lines (including their final newline)
|
||||
to add to the graph ``body``.
|
||||
strict (bool): Rendering should merge multi-edges.
|
||||
|
||||
Note:
|
||||
All parameters are `optional` and can be changed under their
|
||||
corresponding attribute name after instance creation.
|
||||
"""
|
||||
|
||||
@property
|
||||
def directed(self) -> bool:
|
||||
"""``False``"""
|
||||
return False
|
||||
|
||||
|
||||
class Digraph(dot.DigraphSyntax, BaseGraph):
|
||||
"""Directed graph source code in the DOT language."""
|
||||
|
||||
if Graph.__doc__ is not None:
|
||||
__doc__ += Graph.__doc__.partition('.')[2]
|
||||
|
||||
@property
|
||||
def directed(self) -> bool:
|
||||
"""``True``"""
|
||||
return True
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Display rendered graph as SVG in Jupyter Notebooks and QtConsole."""
|
||||
|
||||
import typing
|
||||
|
||||
from . import piping
|
||||
|
||||
__all__ = ['JUPYTER_FORMATS',
|
||||
'SUPPORTED_JUPYTER_FORMATS', 'DEFAULT_JUPYTER_FORMAT',
|
||||
'get_jupyter_format_mimetype',
|
||||
'JupyterIntegration']
|
||||
|
||||
_IMAGE_JPEG = 'image/jpeg'
|
||||
|
||||
JUPYTER_FORMATS = {'jpeg': _IMAGE_JPEG,
|
||||
'jpg': _IMAGE_JPEG,
|
||||
'png': 'image/png',
|
||||
'svg': 'image/svg+xml'}
|
||||
|
||||
SUPPORTED_JUPYTER_FORMATS = set(JUPYTER_FORMATS)
|
||||
|
||||
DEFAULT_JUPYTER_FORMAT = next(_ for _ in SUPPORTED_JUPYTER_FORMATS if _ == 'svg')
|
||||
|
||||
MIME_TYPES = {'image/jpeg': '_repr_image_jpeg',
|
||||
'image/png': '_repr_image_png',
|
||||
'image/svg+xml': '_repr_image_svg_xml'}
|
||||
|
||||
assert MIME_TYPES.keys() == set(JUPYTER_FORMATS.values())
|
||||
|
||||
SVG_ENCODING = 'utf-8'
|
||||
|
||||
|
||||
def get_jupyter_format_mimetype(jupyter_format: str) -> str:
|
||||
try:
|
||||
return JUPYTER_FORMATS[jupyter_format]
|
||||
except KeyError:
|
||||
raise ValueError(f'unknown jupyter_format: {jupyter_format!r}'
|
||||
f' (must be one of {sorted(JUPYTER_FORMATS)})')
|
||||
|
||||
|
||||
def get_jupyter_mimetype_format(mimetype: str) -> str:
|
||||
if mimetype not in MIME_TYPES:
|
||||
raise ValueError(f'unsupported mimetype: {mimetype!r}'
|
||||
f' (must be one of {sorted(MIME_TYPES)})')
|
||||
|
||||
assert mimetype in JUPYTER_FORMATS.values()
|
||||
|
||||
for format, jupyter_mimetype in JUPYTER_FORMATS.items():
|
||||
if jupyter_mimetype == mimetype:
|
||||
return format
|
||||
|
||||
raise RuntimeError # pragma: no cover
|
||||
|
||||
|
||||
class JupyterIntegration(piping.Pipe):
|
||||
"""Display rendered graph as SVG in Jupyter Notebooks and QtConsole."""
|
||||
|
||||
_jupyter_mimetype = get_jupyter_format_mimetype(DEFAULT_JUPYTER_FORMAT)
|
||||
|
||||
def _repr_mimebundle_(self,
|
||||
include: typing.Optional[typing.Iterable[str]] = None,
|
||||
exclude: typing.Optional[typing.Iterable[str]] = None,
|
||||
**_) -> typing.Dict[str, typing.Union[bytes, str]]:
|
||||
r"""Return the rendered graph as IPython mimebundle.
|
||||
|
||||
Args:
|
||||
include: Iterable of mimetypes to include in the result.
|
||||
If not given or ``None``: ``['image/sxg+xml']``.
|
||||
exclude: Iterable of minetypes to exclude from the result.
|
||||
Overrides ``include``.
|
||||
|
||||
Returns:
|
||||
Mapping from mimetypes to data.
|
||||
|
||||
Example:
|
||||
>>> doctest_mark_exe()
|
||||
>>> import graphviz
|
||||
>>> dot = graphviz.Graph()
|
||||
>>> dot._repr_mimebundle_() # doctest: +ELLIPSIS
|
||||
{'image/svg+xml': '<?xml version=...
|
||||
>>> dot._repr_mimebundle_(include=['image/png']) # doctest: +ELLIPSIS
|
||||
{'image/png': b'\x89PNG...
|
||||
>>> dot._repr_mimebundle_(include=[])
|
||||
{}
|
||||
>>> dot._repr_mimebundle_(include=['image/svg+xml', 'image/jpeg'],
|
||||
... exclude=['image/svg+xml']) # doctest: +ELLIPSIS
|
||||
{'image/jpeg': b'\xff...
|
||||
>>> list(dot._repr_mimebundle_(include=['image/png', 'image/jpeg']))
|
||||
['image/jpeg', 'image/png']
|
||||
|
||||
See also:
|
||||
IPython documentation:
|
||||
- https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#functions
|
||||
- https://ipython.readthedocs.io/en/stable/config/integrating.html#MyObject._repr_mimebundle_ # noqa: E501
|
||||
- https://nbviewer.org/github/ipython/ipython/blob/master/examples/IPython%20Kernel/Custom%20Display%20Logic.ipynb#Custom-Mimetypes-with-_repr_mimebundle_ # noqa: E501
|
||||
"""
|
||||
include = set(include) if include is not None else {self._jupyter_mimetype}
|
||||
include -= set(exclude or [])
|
||||
return {mimetype: getattr(self, method_name)()
|
||||
for mimetype, method_name in MIME_TYPES.items()
|
||||
if mimetype in include}
|
||||
|
||||
def _repr_image_jpeg(self) -> bytes:
|
||||
"""Return the rendered graph as JPEG bytes."""
|
||||
return self.pipe(format='jpeg')
|
||||
|
||||
def _repr_image_png(self) -> bytes:
|
||||
"""Return the rendered graph as PNG bytes."""
|
||||
return self.pipe(format='png')
|
||||
|
||||
def _repr_image_svg_xml(self) -> str:
|
||||
"""Return the rendered graph as SVG string."""
|
||||
return self.pipe(format='svg', encoding=SVG_ENCODING)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Hold and verify parameters for running Graphviz ``dot``."""
|
||||
|
||||
from .engines import ENGINES, verify_engine
|
||||
from .formats import FORMATS, verify_format
|
||||
from .renderers import RENDERERS, verify_renderer
|
||||
from .formatters import FORMATTERS, verify_formatter
|
||||
|
||||
from . mixins import Parameters
|
||||
|
||||
__all__ = ['ENGINES', 'FORMATS', 'RENDERERS', 'FORMATTERS',
|
||||
'verify_engine', 'verify_format',
|
||||
'verify_renderer', 'verify_formatter',
|
||||
'Parameters']
|
||||
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,16 @@
|
||||
"""Rendering parameter handling."""
|
||||
|
||||
from .. import copying
|
||||
|
||||
__all__ = ['ParameterBase']
|
||||
|
||||
|
||||
class ParameterBase(copying.CopyBase):
|
||||
"""Rendering parameter."""
|
||||
|
||||
def _getattr_from_dict(self, attrname: str, *, default=None):
|
||||
"""Return self.attrname if attrname is in the instance dictionary
|
||||
(as oposed to on the type)."""
|
||||
if attrname in self.__dict__:
|
||||
return getattr(self, attrname)
|
||||
return default
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Rendering engine parameter handling."""
|
||||
|
||||
import typing
|
||||
|
||||
from . import base
|
||||
|
||||
__all__ = ['ENGINES', 'verify_engine', 'Engine']
|
||||
|
||||
ENGINES = {'dot', # https://www.graphviz.org/pdf/dot.1.pdf
|
||||
'neato',
|
||||
'twopi',
|
||||
'circo',
|
||||
'fdp',
|
||||
'sfdp',
|
||||
'patchwork',
|
||||
'osage'}
|
||||
|
||||
DEFAULT_ENGINE = 'dot'
|
||||
|
||||
REQUIRED = True
|
||||
|
||||
|
||||
def verify_engine(engine: str, *, required: bool = REQUIRED) -> None:
|
||||
if engine is None:
|
||||
if required:
|
||||
raise ValueError('missing engine')
|
||||
elif engine.lower() not in ENGINES:
|
||||
raise ValueError(f'unknown engine: {engine!r}'
|
||||
f' (must be one of {sorted(ENGINES)})')
|
||||
|
||||
|
||||
class Engine(base.ParameterBase):
|
||||
"""Rendering engine parameter with ``'dot''`` default."""
|
||||
|
||||
_engine = DEFAULT_ENGINE
|
||||
|
||||
_verify_engine = staticmethod(verify_engine)
|
||||
|
||||
def __init__(self, *, engine: typing.Optional[str] = None, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if engine is not None:
|
||||
self.engine = engine
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
engine = self._getattr_from_dict('_engine')
|
||||
if engine is not None:
|
||||
kwargs['engine'] = engine
|
||||
return super()._copy_kwargs(**kwargs)
|
||||
|
||||
@property
|
||||
def engine(self) -> str:
|
||||
"""The layout engine used for rendering
|
||||
(``'dot'``, ``'neato'``, ...)."""
|
||||
return self._engine
|
||||
|
||||
@engine.setter
|
||||
def engine(self, engine: str) -> None:
|
||||
engine = engine.lower()
|
||||
self._verify_engine(engine)
|
||||
self._engine = engine
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Rendering format parameter handling."""
|
||||
|
||||
import typing
|
||||
|
||||
from . import base
|
||||
|
||||
__all__ = ['FORMATS', 'verify_format', 'Format']
|
||||
|
||||
FORMATS = {'bmp', # https://graphviz.org/docs/outputs/
|
||||
'canon', 'dot', 'gv', 'xdot', 'xdot1.2', 'xdot1.4',
|
||||
'cgimage',
|
||||
'cmap',
|
||||
'eps',
|
||||
'exr',
|
||||
'fig',
|
||||
'gd', 'gd2',
|
||||
'gif',
|
||||
'gtk',
|
||||
'ico',
|
||||
'imap', 'cmapx',
|
||||
'imap_np', 'cmapx_np',
|
||||
'ismap',
|
||||
'jp2',
|
||||
'jpg', 'jpeg', 'jpe',
|
||||
'json', 'json0', 'dot_json', 'xdot_json', # Graphviz 2.40
|
||||
'pct', 'pict',
|
||||
'pdf',
|
||||
'pic',
|
||||
'plain', 'plain-ext',
|
||||
'png',
|
||||
'pov',
|
||||
'ps',
|
||||
'ps2',
|
||||
'psd',
|
||||
'sgi',
|
||||
'svg', 'svgz',
|
||||
'tga',
|
||||
'tif', 'tiff',
|
||||
'tk',
|
||||
'vml', 'vmlz',
|
||||
'vrml',
|
||||
'wbmp',
|
||||
'webp',
|
||||
'xlib', 'x11'}
|
||||
|
||||
DEFAULT_FORMAT = 'pdf'
|
||||
|
||||
REQUIRED = True
|
||||
|
||||
|
||||
def verify_format(format: str, *, required: bool = REQUIRED) -> None:
|
||||
if format is None:
|
||||
if required:
|
||||
raise ValueError('missing format')
|
||||
elif format.lower() not in FORMATS:
|
||||
raise ValueError(f'unknown format: {format!r}'
|
||||
f' (must be one of {sorted(FORMATS)})')
|
||||
|
||||
|
||||
class Format(base.ParameterBase):
|
||||
"""Rendering format parameter with ``'pdf'`` default."""
|
||||
|
||||
_format = DEFAULT_FORMAT
|
||||
|
||||
_verify_format = staticmethod(verify_format)
|
||||
|
||||
def __init__(self, *, format: typing.Optional[str] = None, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if format is not None:
|
||||
self.format = format
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
format = self._getattr_from_dict('_format')
|
||||
if format is not None:
|
||||
kwargs['format'] = format
|
||||
return super()._copy_kwargs(**kwargs)
|
||||
|
||||
@property
|
||||
def format(self) -> str:
|
||||
"""The output format used for rendering
|
||||
(``'pdf'``, ``'png'``, ...)."""
|
||||
return self._format
|
||||
|
||||
@format.setter
|
||||
def format(self, format: str) -> None:
|
||||
format = format.lower()
|
||||
self._verify_format(format)
|
||||
self._format = format
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Rendering formatter parameter handling."""
|
||||
|
||||
import typing
|
||||
|
||||
from . import base
|
||||
|
||||
__all__ = ['FORMATTERS', 'verify_formatter', 'Formatter']
|
||||
|
||||
FORMATTERS = {'cairo',
|
||||
'core',
|
||||
'gd',
|
||||
'gdiplus',
|
||||
'gdwbmp',
|
||||
'xlib'}
|
||||
|
||||
REQUIRED = False
|
||||
|
||||
|
||||
def verify_formatter(formatter: typing.Optional[str], *,
|
||||
required: bool = REQUIRED) -> None:
|
||||
if formatter is None:
|
||||
if required:
|
||||
raise ValueError('missing formatter')
|
||||
elif formatter.lower() not in FORMATTERS:
|
||||
raise ValueError(f'unknown formatter: {formatter!r}'
|
||||
f' (must be None or one of {sorted(FORMATTERS)})')
|
||||
|
||||
|
||||
class Formatter(base.ParameterBase):
|
||||
"""Rendering engine parameter (no default)."""
|
||||
|
||||
_formatter = None
|
||||
|
||||
_verify_formatter = staticmethod(verify_formatter)
|
||||
|
||||
def __init__(self, *, formatter: typing.Optional[str] = None, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.formatter = formatter
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
formatter = self._getattr_from_dict('_formatter')
|
||||
if formatter is not None:
|
||||
kwargs['formatter'] = formatter
|
||||
return super()._copy_kwargs(**kwargs)
|
||||
|
||||
@property
|
||||
def formatter(self) -> typing.Optional[str]:
|
||||
"""The output formatter used for rendering
|
||||
(``'cairo'``, ``'gd'``, ...)."""
|
||||
return self._formatter
|
||||
|
||||
@formatter.setter
|
||||
def formatter(self, formatter: typing.Optional[str]) -> None:
|
||||
if formatter is None:
|
||||
self.__dict__.pop('_formatter', None)
|
||||
else:
|
||||
formatter = formatter.lower()
|
||||
self._verify_formatter(formatter)
|
||||
self._formatter = formatter
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Mixin classes used to inherit parameter functionality."""
|
||||
|
||||
import typing
|
||||
|
||||
from . import engines
|
||||
from . import formats
|
||||
from . import renderers
|
||||
from . import formatters
|
||||
|
||||
__all__ = ['Parameters']
|
||||
|
||||
|
||||
class Parameters(engines.Engine, formats.Format,
|
||||
renderers.Renderer, formatters.Formatter):
|
||||
"""Parameters for calling ``graphviz.render()`` and ``graphviz.pipe()``."""
|
||||
|
||||
def _get_parameters(self, *,
|
||||
engine: typing.Optional[str] = None,
|
||||
format: typing.Optional[str] = None,
|
||||
renderer: typing.Optional[str] = None,
|
||||
formatter: typing.Optional[str] = None,
|
||||
verify: bool = False,
|
||||
**kwargs):
|
||||
if engine is None:
|
||||
engine = self.engine
|
||||
elif verify:
|
||||
self._verify_engine(engine)
|
||||
|
||||
if format is None:
|
||||
format = self.format
|
||||
elif verify:
|
||||
self._verify_format(format)
|
||||
|
||||
if renderer is None:
|
||||
renderer = self.renderer
|
||||
elif verify:
|
||||
self._verify_renderer(renderer)
|
||||
|
||||
if formatter is None:
|
||||
formatter = self.formatter
|
||||
elif verify:
|
||||
self._verify_formatter(formatter)
|
||||
|
||||
kwargs.update(engine=engine, format=format,
|
||||
renderer=renderer, formatter=formatter)
|
||||
return kwargs
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Rendering renderer parameter handling."""
|
||||
|
||||
import typing
|
||||
|
||||
from . import base
|
||||
|
||||
__all__ = ['RENDERERS', 'verify_renderer', 'Renderer']
|
||||
|
||||
RENDERERS = {'cairo', # $ dot -T:
|
||||
'dot',
|
||||
'fig',
|
||||
'gd',
|
||||
'gdiplus',
|
||||
'map',
|
||||
'pic',
|
||||
'pov',
|
||||
'ps',
|
||||
'svg',
|
||||
'tk',
|
||||
'vml',
|
||||
'vrml',
|
||||
'xdot'}
|
||||
|
||||
|
||||
REQUIRED = False
|
||||
|
||||
|
||||
def verify_renderer(renderer: typing.Optional[str], *,
|
||||
required: bool = REQUIRED) -> None:
|
||||
if renderer is None:
|
||||
if required:
|
||||
raise ValueError('missing renderer')
|
||||
elif renderer.lower() not in RENDERERS:
|
||||
raise ValueError(f'unknown renderer: {renderer!r}'
|
||||
f' (must be None or one of {sorted(RENDERERS)})')
|
||||
|
||||
|
||||
class Renderer(base.ParameterBase):
|
||||
"""Rendering renderer parameter (no default)."""
|
||||
|
||||
_renderer = None
|
||||
|
||||
_verify_renderer = staticmethod(verify_renderer)
|
||||
|
||||
def __init__(self, *, renderer: typing.Optional[str] = None, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.renderer = renderer
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
renderer = self._getattr_from_dict('_renderer')
|
||||
if renderer is not None:
|
||||
kwargs['renderer'] = renderer
|
||||
return super()._copy_kwargs(**kwargs)
|
||||
|
||||
@property
|
||||
def renderer(self) -> typing.Optional[str]:
|
||||
"""The output renderer used for rendering
|
||||
(``'cairo'``, ``'gd'``, ...)."""
|
||||
return self._renderer
|
||||
|
||||
@renderer.setter
|
||||
def renderer(self, renderer: typing.Optional[str]) -> None:
|
||||
if renderer is None:
|
||||
self.__dict__.pop('_renderer', None)
|
||||
else:
|
||||
renderer = renderer.lower()
|
||||
self._verify_renderer(renderer)
|
||||
self._renderer = renderer
|
||||
161
.venv/lib/python3.10/site-packages/graphviz/piping.py
Normal file
161
.venv/lib/python3.10/site-packages/graphviz/piping.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Pipe DOT code objects through Graphviz ``dot``."""
|
||||
|
||||
import codecs
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from . import _tools
|
||||
from . import backend
|
||||
from . import exceptions
|
||||
from . import base
|
||||
from . import encoding
|
||||
|
||||
__all__ = ['Pipe']
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pipe(encoding.Encoding, base.Base, backend.Pipe):
|
||||
"""Pipe source lines through the Graphviz layout command."""
|
||||
|
||||
@typing.overload
|
||||
def pipe(self,
|
||||
format: typing.Optional[str] = ...,
|
||||
renderer: typing.Optional[str] = ...,
|
||||
formatter: typing.Optional[str] = ...,
|
||||
neato_no_op: typing.Union[bool, int, None] = ...,
|
||||
quiet: bool = ..., *,
|
||||
engine: typing.Optional[str] = ...,
|
||||
encoding: None = ...) -> bytes:
|
||||
"""Return bytes with default ``encoding=None``."""
|
||||
|
||||
@typing.overload
|
||||
def pipe(self,
|
||||
format: typing.Optional[str] = ...,
|
||||
renderer: typing.Optional[str] = ...,
|
||||
formatter: typing.Optional[str] = ...,
|
||||
neato_no_op: typing.Union[bool, int, None] = ...,
|
||||
quiet: bool = ..., *,
|
||||
engine: typing.Optional[str] = ...,
|
||||
encoding: str) -> str:
|
||||
"""Return string when given encoding."""
|
||||
|
||||
@typing.overload
|
||||
def pipe(self,
|
||||
format: typing.Optional[str] = ...,
|
||||
renderer: typing.Optional[str] = ...,
|
||||
formatter: typing.Optional[str] = ...,
|
||||
neato_no_op: typing.Union[bool, int, None] = ...,
|
||||
quiet: bool = ..., *,
|
||||
engine: typing.Optional[str] = ...,
|
||||
encoding: typing.Optional[str]) -> typing.Union[bytes, str]:
|
||||
"""Return bytes or string depending on encoding argument."""
|
||||
|
||||
def pipe(self,
|
||||
format: typing.Optional[str] = None,
|
||||
renderer: typing.Optional[str] = None,
|
||||
formatter: typing.Optional[str] = None,
|
||||
neato_no_op: typing.Union[bool, int, None] = None,
|
||||
quiet: bool = False, *,
|
||||
engine: typing.Optional[str] = None,
|
||||
encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
|
||||
"""Return the source piped through the Graphviz layout command.
|
||||
|
||||
Args:
|
||||
format: The output format used for rendering
|
||||
(``'pdf'``, ``'png'``, etc.).
|
||||
renderer: The output renderer used for rendering
|
||||
(``'cairo'``, ``'gd'``, ...).
|
||||
formatter: The output formatter used for rendering
|
||||
(``'cairo'``, ``'gd'``, ...).
|
||||
neato_no_op: Neato layout engine no-op flag.
|
||||
quiet (bool): Suppress ``stderr`` output
|
||||
from the layout subprocess.
|
||||
engine: Layout engine for rendering
|
||||
(``'dot'``, ``'neato'``, ...).
|
||||
encoding: Encoding for decoding the stdout.
|
||||
|
||||
Returns:
|
||||
Bytes or if encoding is given decoded string
|
||||
(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
|
||||
>>> source = 'graph { spam }'
|
||||
>>> graphviz.Source(source, format='svg').pipe()[:14]
|
||||
b'<?xml version='
|
||||
>>> graphviz.Source(source, format='svg').pipe(encoding='ascii')[:14]
|
||||
'<?xml version='
|
||||
>>> graphviz.Source(source, format='svg').pipe(encoding='utf-8')[:14]
|
||||
'<?xml version='
|
||||
"""
|
||||
return self._pipe_legacy(format,
|
||||
renderer=renderer,
|
||||
formatter=formatter,
|
||||
neato_no_op=neato_no_op,
|
||||
quiet=quiet,
|
||||
engine=engine,
|
||||
encoding=encoding)
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def _pipe_legacy(self,
|
||||
format: typing.Optional[str] = None,
|
||||
renderer: typing.Optional[str] = None,
|
||||
formatter: typing.Optional[str] = None,
|
||||
neato_no_op: typing.Union[bool, int, None] = None,
|
||||
quiet: bool = False, *,
|
||||
engine: typing.Optional[str] = None,
|
||||
encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
|
||||
return self._pipe_future(format,
|
||||
renderer=renderer,
|
||||
formatter=formatter,
|
||||
neato_no_op=neato_no_op,
|
||||
quiet=quiet,
|
||||
engine=engine,
|
||||
encoding=encoding)
|
||||
|
||||
def _pipe_future(self, format: typing.Optional[str] = None, *,
|
||||
renderer: typing.Optional[str] = None,
|
||||
formatter: typing.Optional[str] = None,
|
||||
neato_no_op: typing.Union[bool, int, None] = None,
|
||||
quiet: bool = False,
|
||||
engine: typing.Optional[str] = None,
|
||||
encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
|
||||
args, kwargs = self._get_pipe_parameters(engine=engine,
|
||||
format=format,
|
||||
renderer=renderer,
|
||||
formatter=formatter,
|
||||
neato_no_op=neato_no_op,
|
||||
quiet=quiet,
|
||||
verify=True)
|
||||
|
||||
args.append(iter(self))
|
||||
|
||||
if encoding is not None:
|
||||
if codecs.lookup(encoding) is codecs.lookup(self.encoding):
|
||||
# common case: both stdin and stdout need the same encoding
|
||||
return self._pipe_lines_string(*args, encoding=encoding, **kwargs)
|
||||
try:
|
||||
raw = self._pipe_lines(*args, input_encoding=self.encoding, **kwargs)
|
||||
except exceptions.CalledProcessError as e:
|
||||
*args, output, stderr = e.args
|
||||
if output is not None:
|
||||
output = output.decode(self.encoding)
|
||||
if stderr is not None:
|
||||
stderr = stderr.decode(self.encoding)
|
||||
raise e.__class__(*args, output=output, stderr=stderr)
|
||||
else:
|
||||
return raw.decode(encoding)
|
||||
return self._pipe_lines(*args, input_encoding=self.encoding, **kwargs)
|
||||
221
.venv/lib/python3.10/site-packages/graphviz/quoting.py
Normal file
221
.venv/lib/python3.10/site-packages/graphviz/quoting.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""Quote strings to be valid DOT identifiers, assemble quoted attribute lists."""
|
||||
|
||||
import functools
|
||||
import re
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from . import _tools
|
||||
from . import exceptions
|
||||
|
||||
__all__ = ['quote', 'quote_edge',
|
||||
'a_list', 'attr_list',
|
||||
'escape', 'nohtml']
|
||||
|
||||
# https://www.graphviz.org/doc/info/lang.html
|
||||
# https://www.graphviz.org/doc/info/attrs.html#k:escString
|
||||
|
||||
HTML_STRING = re.compile(r'<.*>$', re.DOTALL)
|
||||
|
||||
ID = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*|-?(\.[0-9]+|[0-9]+(\.[0-9]*)?))$')
|
||||
|
||||
KEYWORDS = {'node', 'edge', 'graph', 'digraph', 'subgraph', 'strict'}
|
||||
|
||||
COMPASS = {'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c', '_'} # TODO
|
||||
|
||||
FINAL_ODD_BACKSLASHES = re.compile(r'(?<!\\)(?:\\{2})*\\$')
|
||||
|
||||
QUOTE_WITH_OPTIONAL_BACKSLASHES = re.compile(r'''
|
||||
(?P<escaped_backslashes>(?:\\{2})*)
|
||||
\\? # treat \" same as "
|
||||
(?P<literal_quote>")
|
||||
''', flags=re.VERBOSE)
|
||||
|
||||
ESCAPE_UNESCAPED_QUOTES = functools.partial(QUOTE_WITH_OPTIONAL_BACKSLASHES.sub,
|
||||
r'\g<escaped_backslashes>'
|
||||
r'\\'
|
||||
r'\g<literal_quote>')
|
||||
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=1)
|
||||
def quote(identifier: str,
|
||||
is_html_string=HTML_STRING.match,
|
||||
is_valid_id=ID.match,
|
||||
dot_keywords=KEYWORDS,
|
||||
endswith_odd_number_of_backslashes=FINAL_ODD_BACKSLASHES.search,
|
||||
escape_unescaped_quotes=ESCAPE_UNESCAPED_QUOTES) -> str:
|
||||
r"""Return DOT identifier from string, quote if needed.
|
||||
|
||||
>>> quote('') # doctest: +NO_EXE
|
||||
'""'
|
||||
|
||||
>>> quote('spam')
|
||||
'spam'
|
||||
|
||||
>>> quote('spam spam')
|
||||
'"spam spam"'
|
||||
|
||||
>>> quote('-4.2')
|
||||
'-4.2'
|
||||
|
||||
>>> quote('.42')
|
||||
'.42'
|
||||
|
||||
>>> quote('<<b>spam</b>>')
|
||||
'<<b>spam</b>>'
|
||||
|
||||
>>> quote(nohtml('<>'))
|
||||
'"<>"'
|
||||
|
||||
>>> print(quote('"'))
|
||||
"\""
|
||||
|
||||
>>> print(quote('\\"'))
|
||||
"\""
|
||||
|
||||
>>> print(quote('\\\\"'))
|
||||
"\\\""
|
||||
|
||||
>>> print(quote('\\\\\\"'))
|
||||
"\\\""
|
||||
"""
|
||||
if is_html_string(identifier) and not isinstance(identifier, NoHtml):
|
||||
pass
|
||||
elif not is_valid_id(identifier) or identifier.lower() in dot_keywords:
|
||||
if endswith_odd_number_of_backslashes(identifier):
|
||||
warnings.warn('expect syntax error scanning invalid quoted string:'
|
||||
f' {identifier!r}',
|
||||
category=exceptions.DotSyntaxWarning)
|
||||
return f'"{escape_unescaped_quotes(identifier)}"'
|
||||
return identifier
|
||||
|
||||
|
||||
def quote_edge(identifier: str) -> str:
|
||||
"""Return DOT edge statement node_id from string, quote if needed.
|
||||
|
||||
>>> quote_edge('spam') # doctest: +NO_EXE
|
||||
'spam'
|
||||
|
||||
>>> quote_edge('spam spam:eggs eggs')
|
||||
'"spam spam":"eggs eggs"'
|
||||
|
||||
>>> quote_edge('spam:eggs:s')
|
||||
'spam:eggs:s'
|
||||
"""
|
||||
node, _, rest = identifier.partition(':')
|
||||
parts = [quote(node)]
|
||||
if rest:
|
||||
port, _, compass = rest.partition(':')
|
||||
parts.append(quote(port))
|
||||
if compass:
|
||||
parts.append(compass)
|
||||
return ':'.join(parts)
|
||||
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=1)
|
||||
def a_list(label: typing.Optional[str] = None,
|
||||
kwargs=None, attributes=None) -> str:
|
||||
"""Return assembled DOT a_list string.
|
||||
|
||||
>>> a_list('spam', kwargs={'spam': None, 'ham': 'ham ham', 'eggs': ''}) # doctest: +NO_EXE
|
||||
'label=spam eggs="" ham="ham ham"'
|
||||
"""
|
||||
result = [f'label={quote(label)}'] if label is not None else []
|
||||
if kwargs:
|
||||
result += [f'{quote(k)}={quote(v)}'
|
||||
for k, v in _tools.mapping_items(kwargs) if v is not None]
|
||||
if attributes:
|
||||
if hasattr(attributes, 'items'):
|
||||
attributes = _tools.mapping_items(attributes)
|
||||
result += [f'{quote(k)}={quote(v)}'
|
||||
for k, v in attributes if v is not None]
|
||||
return ' '.join(result)
|
||||
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=1)
|
||||
def attr_list(label: typing.Optional[str] = None,
|
||||
kwargs=None, attributes=None) -> str:
|
||||
"""Return assembled DOT attribute list string.
|
||||
|
||||
Sorts ``kwargs`` and ``attributes`` if they are plain dicts
|
||||
(to avoid unpredictable order from hash randomization in Python < 3.7).
|
||||
|
||||
>>> attr_list() # doctest: +NO_EXE
|
||||
''
|
||||
|
||||
>>> attr_list('spam spam', kwargs={'eggs': 'eggs', 'ham': 'ham ham'})
|
||||
' [label="spam spam" eggs=eggs ham="ham ham"]'
|
||||
|
||||
>>> attr_list(kwargs={'spam': None, 'eggs': ''})
|
||||
' [eggs=""]'
|
||||
"""
|
||||
content = a_list(label, kwargs=kwargs, attributes=attributes)
|
||||
if not content:
|
||||
return ''
|
||||
return f' [{content}]'
|
||||
|
||||
|
||||
class Quote:
|
||||
"""Quote strings to be valid DOT identifiers, assemble quoted attribute lists."""
|
||||
|
||||
_quote = staticmethod(quote)
|
||||
_quote_edge = staticmethod(quote_edge)
|
||||
|
||||
_a_list = staticmethod(a_list)
|
||||
_attr_list = staticmethod(attr_list)
|
||||
|
||||
|
||||
def escape(s: str) -> str:
|
||||
r"""Return string disabling special meaning of backslashes and ``'<...>'``.
|
||||
|
||||
Args:
|
||||
s: String in which backslashes and ``'<...>'``
|
||||
should be treated as literal.
|
||||
|
||||
Returns:
|
||||
Escaped string subclass instance.
|
||||
|
||||
Raises:
|
||||
TypeError: If ``s`` is not a ``str``.
|
||||
|
||||
Example:
|
||||
>>> import graphviz # doctest: +NO_EXE
|
||||
>>> print(graphviz.escape(r'\l'))
|
||||
\\l
|
||||
|
||||
See also:
|
||||
Upstream documentation:
|
||||
https://www.graphviz.org/doc/info/attrs.html#k:escString
|
||||
"""
|
||||
return nohtml(s.replace('\\', '\\\\'))
|
||||
|
||||
|
||||
class NoHtml(str):
|
||||
"""String subclass that does not treat ``'<...>'`` as DOT HTML string."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
def nohtml(s: str) -> str:
|
||||
"""Return string not treating ``'<...>'`` as DOT HTML string in quoting.
|
||||
|
||||
Args:
|
||||
s: String in which leading ``'<'`` and trailing ``'>'``
|
||||
should be treated as literal.
|
||||
|
||||
Returns:
|
||||
String subclass instance.
|
||||
|
||||
Raises:
|
||||
TypeError: If ``s`` is not a ``str``.
|
||||
|
||||
Example:
|
||||
>>> import graphviz # doctest: +NO_EXE
|
||||
>>> g = graphviz.Graph()
|
||||
>>> g.node(graphviz.nohtml('<>-*-<>'))
|
||||
>>> print(g.source) # doctest: +NORMALIZE_WHITESPACE
|
||||
graph {
|
||||
"<>-*-<>"
|
||||
}
|
||||
"""
|
||||
return NoHtml(s)
|
||||
186
.venv/lib/python3.10/site-packages/graphviz/rendering.py
Normal file
186
.venv/lib/python3.10/site-packages/graphviz/rendering.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""Save DOT code objects, render with Graphviz ``dot``, and open in viewer."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import typing
|
||||
|
||||
from . import _tools
|
||||
from . import backend
|
||||
from . import saving
|
||||
|
||||
__all__ = ['Render']
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Render(saving.Save, backend.Render, backend.View):
|
||||
"""Write source lines to file and render with Graphviz."""
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def render(self,
|
||||
filename: typing.Union[os.PathLike, str, None] = None,
|
||||
directory: typing.Union[os.PathLike, str, None] = None,
|
||||
view: bool = False,
|
||||
cleanup: bool = False,
|
||||
format: typing.Optional[str] = None,
|
||||
renderer: typing.Optional[str] = None,
|
||||
formatter: typing.Optional[str] = None,
|
||||
neato_no_op: typing.Union[bool, int, None] = None,
|
||||
quiet: bool = False,
|
||||
quiet_view: bool = False, *,
|
||||
outfile: typing.Union[os.PathLike, str, None] = None,
|
||||
engine: typing.Optional[str] = None,
|
||||
raise_if_result_exists: bool = False,
|
||||
overwrite_source: bool = False) -> str:
|
||||
r"""Save the source to file and render with the Graphviz engine.
|
||||
|
||||
Args:
|
||||
filename: Filename for saving the source
|
||||
(defaults to ``name`` + ``'.gv'``).s
|
||||
directory: (Sub)directory for source saving and rendering.
|
||||
view (bool): Open the rendered result
|
||||
with the default application.
|
||||
cleanup (bool): Delete the source file
|
||||
after successful rendering.
|
||||
format: The output format used for rendering
|
||||
(``'pdf'``, ``'png'``, etc.).
|
||||
renderer: The output renderer used for rendering
|
||||
(``'cairo'``, ``'gd'``, ...).
|
||||
formatter: The output formatter used for rendering
|
||||
(``'cairo'``, ``'gd'``, ...).
|
||||
neato_no_op: Neato layout engine no-op flag.
|
||||
quiet (bool): Suppress ``stderr`` output
|
||||
from the layout subprocess.
|
||||
quiet_view (bool): Suppress ``stderr`` output
|
||||
from the viewer process
|
||||
(implies ``view=True``, ineffective on Windows platform).
|
||||
outfile: Path for the rendered output file.
|
||||
engine: Layout engine for rendering
|
||||
(``'dot'``, ``'neato'``, ...).
|
||||
raise_if_result_exists: Raise :exc:`graphviz.FileExistsError`
|
||||
if the result file exists.
|
||||
overwrite_source: 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 ``formatter`` is given
|
||||
but ``renderer`` is None.
|
||||
ValueError: If ``outfile`` is the same file as the source file
|
||||
unless ``overwite_source=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.
|
||||
RuntimeError: If viewer opening is requested but not supported.
|
||||
|
||||
Example:
|
||||
>>> doctest_mark_exe()
|
||||
>>> import graphviz
|
||||
>>> dot = graphviz.Graph(name='spam', directory='doctest-output')
|
||||
>>> dot.render(format='png').replace('\\', '/')
|
||||
'doctest-output/spam.gv.png'
|
||||
>>> dot.render(outfile='spam.svg').replace('\\', '/')
|
||||
'doctest-output/spam.svg'
|
||||
|
||||
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.
|
||||
"""
|
||||
outfile = _tools.promote_pathlike(outfile)
|
||||
if outfile is not None:
|
||||
format = self._get_format(outfile, format=format)
|
||||
if directory is None:
|
||||
outfile = pathlib.Path(self.directory, outfile)
|
||||
|
||||
args, kwargs = self._get_render_parameters(engine=engine,
|
||||
format=format,
|
||||
renderer=renderer,
|
||||
formatter=formatter,
|
||||
neato_no_op=neato_no_op,
|
||||
quiet=quiet,
|
||||
outfile=outfile,
|
||||
raise_if_result_exists=raise_if_result_exists,
|
||||
overwrite_source=overwrite_source,
|
||||
verify=True)
|
||||
|
||||
if outfile is not None and filename is None:
|
||||
filename = self._get_filepath(outfile)
|
||||
|
||||
filepath = self.save(filename, directory=directory, skip_existing=None)
|
||||
|
||||
args.append(filepath)
|
||||
|
||||
rendered = self._render(*args, **kwargs)
|
||||
|
||||
if cleanup:
|
||||
log.debug('delete %r', filepath)
|
||||
os.remove(filepath)
|
||||
|
||||
if quiet_view or view:
|
||||
self._view(rendered, format=self._format, quiet=quiet_view)
|
||||
|
||||
return rendered
|
||||
|
||||
def _view(self, filepath: typing.Union[os.PathLike, str], *,
|
||||
format: str, quiet: bool) -> None:
|
||||
"""Start the right viewer based on file format and platform."""
|
||||
methodnames = [
|
||||
f'_view_{format}_{backend.viewing.PLATFORM}',
|
||||
f'_view_{backend.viewing.PLATFORM}',
|
||||
]
|
||||
for name in methodnames:
|
||||
view_method = getattr(self, name, None)
|
||||
if view_method is not None:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError(f'{self.__class__!r} has no built-in viewer'
|
||||
f' support for {format!r}'
|
||||
f' on {backend.viewing.PLATFORM!r} platform')
|
||||
view_method(filepath, quiet=quiet)
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def view(self,
|
||||
filename: typing.Union[os.PathLike, str, None] = None,
|
||||
directory: typing.Union[os.PathLike, str, None] = None,
|
||||
cleanup: bool = False,
|
||||
quiet: bool = False,
|
||||
quiet_view: bool = False) -> str:
|
||||
"""Save the source to file, open the rendered result in a viewer.
|
||||
|
||||
Convenience short-cut for running ``.render(view=True)``.
|
||||
|
||||
Args:
|
||||
filename: Filename for saving the source
|
||||
(defaults to ``name`` + ``'.gv'``).
|
||||
directory: (Sub)directory for source saving and rendering.
|
||||
cleanup (bool): Delete the source file after successful rendering.
|
||||
quiet (bool): Suppress ``stderr`` output from the layout subprocess.
|
||||
quiet_view (bool): Suppress ``stderr`` output
|
||||
from the viewer process (ineffective on Windows).
|
||||
|
||||
Returns:
|
||||
The (possibly relative) path of the rendered file.
|
||||
|
||||
Raises:
|
||||
graphviz.ExecutableNotFound: If the Graphviz executable
|
||||
is not found.
|
||||
graphviz.CalledProcessError: If the exit status is non-zero.
|
||||
RuntimeError: If opening the viewer is not supported.
|
||||
|
||||
Short-cut method for calling :meth:`.render` with ``view=True``.
|
||||
|
||||
Note:
|
||||
There is no option to wait for the application to close,
|
||||
and no way to retrieve the application's exit status.
|
||||
"""
|
||||
return self.render(filename=filename, directory=directory, view=True,
|
||||
cleanup=cleanup, quiet=quiet, quiet_view=quiet_view)
|
||||
83
.venv/lib/python3.10/site-packages/graphviz/saving.py
Normal file
83
.venv/lib/python3.10/site-packages/graphviz/saving.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Save DOT source lines to a file."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
|
||||
from . import _defaults
|
||||
from . import _tools
|
||||
from . import base
|
||||
from . import encoding
|
||||
|
||||
__all__ = ['Save']
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Save(encoding.Encoding, base.Base):
|
||||
"""Save DOT source lines to file."""
|
||||
|
||||
directory: typing.Union[str, bytes] = ''
|
||||
|
||||
_default_extension = _defaults.DEFAULT_SOURCE_EXTENSION
|
||||
|
||||
_mkdirs = staticmethod(_tools.mkdirs)
|
||||
|
||||
def __init__(self, *,
|
||||
filename: typing.Union[os.PathLike, str],
|
||||
directory: typing.Union[os.PathLike, str, None] = None,
|
||||
**kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if filename is None:
|
||||
filename = f'{self.__class__.__name__}.{self._default_extension}'
|
||||
|
||||
self.filename = os.fspath(filename)
|
||||
"""str: Target file name for saving the DOT source file."""
|
||||
|
||||
if directory is not None:
|
||||
self.directory = os.fspath(directory)
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
assert 'directory' not in kwargs
|
||||
if 'directory' in self.__dict__:
|
||||
kwargs['directory'] = self.directory
|
||||
return super()._copy_kwargs(filename=self.filename, **kwargs)
|
||||
|
||||
@property
|
||||
def filepath(self) -> str:
|
||||
"""The target path for saving the DOT source file."""
|
||||
return os.path.join(self.directory, self.filename)
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def save(self, filename: typing.Union[os.PathLike, str, None] = None,
|
||||
directory: typing.Union[os.PathLike, str, None] = None, *,
|
||||
skip_existing: typing.Optional[bool] = False) -> str:
|
||||
"""Save the DOT source to file. Ensure the file ends with a newline.
|
||||
|
||||
Args:
|
||||
filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``)
|
||||
directory: (Sub)directory for source saving and rendering.
|
||||
skip_existing: Skip write if file exists (default: ``False``).
|
||||
|
||||
Returns:
|
||||
The (possibly relative) path of the saved source file.
|
||||
"""
|
||||
if filename is not None:
|
||||
self.filename = filename
|
||||
if directory is not None:
|
||||
self.directory = directory
|
||||
|
||||
filepath = self.filepath
|
||||
if skip_existing and os.path.exists(filepath):
|
||||
return filepath
|
||||
|
||||
self._mkdirs(filepath)
|
||||
|
||||
log.debug('write lines to %r', filepath)
|
||||
with open(filepath, 'w', encoding=self.encoding) as fd:
|
||||
for uline in self:
|
||||
fd.write(uline)
|
||||
|
||||
return filepath
|
||||
147
.venv/lib/python3.10/site-packages/graphviz/sources.py
Normal file
147
.venv/lib/python3.10/site-packages/graphviz/sources.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Save DOT code objects, render with Graphviz dot, and open in viewer."""
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
|
||||
from .encoding import DEFAULT_ENCODING
|
||||
from . import _tools
|
||||
from . import saving
|
||||
from . import jupyter_integration
|
||||
from . import piping
|
||||
from . import rendering
|
||||
from . import unflattening
|
||||
|
||||
__all__ = ['Source']
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Source(rendering.Render, saving.Save,
|
||||
jupyter_integration.JupyterIntegration, piping.Pipe,
|
||||
unflattening.Unflatten):
|
||||
"""Verbatim DOT source code string to be rendered by Graphviz.
|
||||
|
||||
Args:
|
||||
source: The verbatim DOT source code string.
|
||||
filename: Filename for saving the source (defaults to ``'Source.gv'``).
|
||||
directory: (Sub)directory for source saving and rendering.
|
||||
format: Rendering output format (``'pdf'``, ``'png'``, ...).
|
||||
engine: Layout engine used (``'dot'``, ``'neato'``, ...).
|
||||
encoding: Encoding for saving the source.
|
||||
|
||||
Note:
|
||||
All parameters except ``source`` are optional. All of them
|
||||
can be changed under their corresponding attribute name
|
||||
after instance creation.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def from_file(cls, filename: typing.Union[os.PathLike, str],
|
||||
directory: typing.Union[os.PathLike, str, None] = None,
|
||||
format: typing.Optional[str] = None,
|
||||
engine: typing.Optional[str] = None,
|
||||
encoding: typing.Optional[str] = DEFAULT_ENCODING,
|
||||
renderer: typing.Optional[str] = None,
|
||||
formatter: typing.Optional[str] = None) -> 'Source':
|
||||
"""Return an instance with the source string read from the given file.
|
||||
|
||||
Args:
|
||||
filename: Filename for loading/saving the source.
|
||||
directory: (Sub)directory for source loading/saving and rendering.
|
||||
format: Rendering output format (``'pdf'``, ``'png'``, ...).
|
||||
engine: Layout command used (``'dot'``, ``'neato'``, ...).
|
||||
encoding: Encoding for loading/saving the source.
|
||||
"""
|
||||
directory = _tools.promote_pathlike_directory(directory)
|
||||
filepath = (os.path.join(directory, filename) if directory.parts
|
||||
else os.fspath(filename))
|
||||
|
||||
if encoding is None:
|
||||
encoding = locale.getpreferredencoding()
|
||||
|
||||
log.debug('read %r with encoding %r', filepath, encoding)
|
||||
with open(filepath, encoding=encoding) as fd:
|
||||
source = fd.read()
|
||||
|
||||
return cls(source,
|
||||
filename=filename, directory=directory,
|
||||
format=format, engine=engine, encoding=encoding,
|
||||
renderer=renderer, formatter=formatter,
|
||||
loaded_from_path=filepath)
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def __init__(self, source: str,
|
||||
filename: typing.Union[os.PathLike, str, None] = None,
|
||||
directory: typing.Union[os.PathLike, str, None] = None,
|
||||
format: typing.Optional[str] = None,
|
||||
engine: typing.Optional[str] = None,
|
||||
encoding: typing.Optional[str] = DEFAULT_ENCODING, *,
|
||||
renderer: typing.Optional[str] = None,
|
||||
formatter: typing.Optional[str] = None,
|
||||
loaded_from_path: typing.Optional[os.PathLike] = None) -> None:
|
||||
super().__init__(filename=filename, directory=directory,
|
||||
format=format, engine=engine,
|
||||
renderer=renderer, formatter=formatter,
|
||||
encoding=encoding)
|
||||
self._loaded_from_path = loaded_from_path
|
||||
self._source = source
|
||||
|
||||
# work around pytype false alarm
|
||||
_source: str
|
||||
_loaded_from_path: typing.Optional[os.PathLike]
|
||||
|
||||
def _copy_kwargs(self, **kwargs):
|
||||
"""Return the kwargs to create a copy of the instance."""
|
||||
return super()._copy_kwargs(source=self._source,
|
||||
loaded_from_path=self._loaded_from_path,
|
||||
**kwargs)
|
||||
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
r"""Yield the DOT source code read from file line by line.
|
||||
|
||||
Yields: Line ending with a newline (``'\n'``).
|
||||
"""
|
||||
lines = self._source.splitlines(keepends=True)
|
||||
yield from lines[:-1]
|
||||
for line in lines[-1:]:
|
||||
suffix = '\n' if not line.endswith('\n') else ''
|
||||
yield line + suffix
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""The DOT source code as string.
|
||||
|
||||
Normalizes so that the string always ends in a final newline.
|
||||
"""
|
||||
source = self._source
|
||||
if not source.endswith('\n'):
|
||||
source += '\n'
|
||||
return source
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=2)
|
||||
def save(self, filename: typing.Union[os.PathLike, str, None] = None,
|
||||
directory: typing.Union[os.PathLike, str, None] = None, *,
|
||||
skip_existing: typing.Optional[bool] = None) -> str:
|
||||
"""Save the DOT source to file. Ensure the file ends with a newline.
|
||||
|
||||
Args:
|
||||
filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``)
|
||||
directory: (Sub)directory for source saving and rendering.
|
||||
skip_existing: Skip write if file exists (default: ``None``).
|
||||
By default skips if instance was loaded from the target path:
|
||||
``.from_file(self.filepath)``.
|
||||
|
||||
Returns:
|
||||
The (possibly relative) path of the saved source file.
|
||||
"""
|
||||
skip = (skip_existing is None and self._loaded_from_path
|
||||
and os.path.samefile(self._loaded_from_path, self.filepath))
|
||||
if skip:
|
||||
log.debug('.save(skip_existing=None) skip writing Source.from_file(%r)',
|
||||
self.filepath)
|
||||
return super().save(filename=filename, directory=directory,
|
||||
skip_existing=skip)
|
||||
63
.venv/lib/python3.10/site-packages/graphviz/unflattening.py
Normal file
63
.venv/lib/python3.10/site-packages/graphviz/unflattening.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Pipe source through the Graphviz *unflatten* preprocessor."""
|
||||
|
||||
import typing
|
||||
|
||||
import graphviz
|
||||
from . import _tools
|
||||
from . import base
|
||||
from . import backend
|
||||
from . import encoding
|
||||
|
||||
__all__ = ['Unflatten']
|
||||
|
||||
|
||||
class Unflatten(encoding.Encoding, base.Base, backend.Unflatten):
|
||||
"""Pipe source through the Graphviz *unflatten* preprocessor."""
|
||||
|
||||
@_tools.deprecate_positional_args(supported_number=1)
|
||||
def unflatten(self,
|
||||
stagger: typing.Optional[int] = None,
|
||||
fanout: bool = False,
|
||||
chain: typing.Optional[int] = None) -> 'graphviz.Source':
|
||||
"""Return a new :class:`.Source` instance with the source
|
||||
piped through the Graphviz *unflatten* preprocessor.
|
||||
|
||||
Args:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
Prepocessed DOT source code (improved layout aspect ratio).
|
||||
|
||||
Raises:
|
||||
graphviz.RequiredArgumentError: If ``fanout`` is given
|
||||
but ``stagger`` is None.
|
||||
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
|
||||
"""
|
||||
from . import sources
|
||||
|
||||
out = self._unflatten(self.source,
|
||||
stagger=stagger, fanout=fanout, chain=chain,
|
||||
encoding=self.encoding)
|
||||
|
||||
kwargs = self._copy_kwargs()
|
||||
return sources.Source(out,
|
||||
filename=kwargs.get('filename'),
|
||||
directory=kwargs.get('directory'),
|
||||
format=kwargs.get('format'),
|
||||
engine=kwargs.get('engine'),
|
||||
encoding=kwargs.get('encoding'),
|
||||
renderer=kwargs.get('renderer'),
|
||||
formatter=kwargs.get('formatter'),
|
||||
loaded_from_path=None)
|
||||
Reference in New Issue
Block a user