File size: 6,181 Bytes
d015b2a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
"""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)
|