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