Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/graphviz/sources.py: 53%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

53 statements  

1"""Save DOT code objects, render with Graphviz dot, and open in viewer.""" 

2 

3import locale 

4import logging 

5import os 

6import typing 

7 

8from .encoding import DEFAULT_ENCODING 

9from . import _tools 

10from . import saving 

11from . import jupyter_integration 

12from . import piping 

13from . import rendering 

14from . import unflattening 

15 

16__all__ = ['Source'] 

17 

18 

19log = logging.getLogger(__name__) 

20 

21 

22class Source(rendering.Render, saving.Save, 

23 jupyter_integration.JupyterIntegration, piping.Pipe, 

24 unflattening.Unflatten): 

25 """Verbatim DOT source code string to be rendered by Graphviz. 

26 

27 Args: 

28 source: The verbatim DOT source code string. 

29 filename: Filename for saving the source (defaults to ``'Source.gv'``). 

30 directory: (Sub)directory for source saving and rendering. 

31 format: Rendering output format (``'pdf'``, ``'png'``, ...). 

32 engine: Layout engine used (``'dot'``, ``'neato'``, ...). 

33 encoding: Encoding for saving the source. 

34 

35 Note: 

36 All parameters except ``source`` are optional. All of them 

37 can be changed under their corresponding attribute name 

38 after instance creation. 

39 """ 

40 

41 @classmethod 

42 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='cls') 

43 def from_file(cls, filename: typing.Union[os.PathLike, str], 

44 directory: typing.Union[os.PathLike, str, None] = None, 

45 format: typing.Optional[str] = None, 

46 engine: typing.Optional[str] = None, 

47 encoding: typing.Optional[str] = DEFAULT_ENCODING, 

48 renderer: typing.Optional[str] = None, 

49 formatter: typing.Optional[str] = None) -> 'Source': 

50 """Return an instance with the source string read from the given file. 

51 

52 Args: 

53 filename: Filename for loading/saving the source. 

54 directory: (Sub)directory for source loading/saving and rendering. 

55 format: Rendering output format (``'pdf'``, ``'png'``, ...). 

56 engine: Layout command used (``'dot'``, ``'neato'``, ...). 

57 encoding: Encoding for loading/saving the source. 

58 """ 

59 directory = _tools.promote_pathlike_directory(directory) 

60 filepath = (os.path.join(directory, filename) if directory.parts 

61 else os.fspath(filename)) 

62 

63 if encoding is None: 

64 encoding = locale.getpreferredencoding() 

65 

66 log.debug('read %r with encoding %r', filepath, encoding) 

67 with open(filepath, encoding=encoding) as fd: 

68 source = fd.read() 

69 

70 return cls(source, 

71 filename=filename, directory=directory, 

72 format=format, engine=engine, encoding=encoding, 

73 renderer=renderer, formatter=formatter, 

74 loaded_from_path=filepath) 

75 

76 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') 

77 def __init__(self, source: str, 

78 filename: typing.Union[os.PathLike, str, None] = None, 

79 directory: typing.Union[os.PathLike, str, None] = None, 

80 format: typing.Optional[str] = None, 

81 engine: typing.Optional[str] = None, 

82 encoding: typing.Optional[str] = DEFAULT_ENCODING, *, 

83 renderer: typing.Optional[str] = None, 

84 formatter: typing.Optional[str] = None, 

85 loaded_from_path: typing.Optional[os.PathLike] = None) -> None: 

86 super().__init__(filename=filename, directory=directory, 

87 format=format, engine=engine, 

88 renderer=renderer, formatter=formatter, 

89 encoding=encoding) 

90 self._loaded_from_path = loaded_from_path 

91 self._source = source 

92 

93 # work around pytype false alarm 

94 _source: str 

95 _loaded_from_path: typing.Optional[os.PathLike] 

96 

97 def _copy_kwargs(self, **kwargs): 

98 """Return the kwargs to create a copy of the instance.""" 

99 return super()._copy_kwargs(source=self._source, 

100 loaded_from_path=self._loaded_from_path, 

101 **kwargs) 

102 

103 def __iter__(self) -> typing.Iterator[str]: 

104 r"""Yield the DOT source code read from file line by line. 

105 

106 Yields: Line ending with a newline (``'\n'``). 

107 """ 

108 lines = self._source.splitlines(keepends=True) 

109 yield from lines[:-1] 

110 for line in lines[-1:]: 

111 suffix = '\n' if not line.endswith('\n') else '' 

112 yield line + suffix 

113 

114 @property 

115 def source(self) -> str: 

116 """The DOT source code as string. 

117 

118 Normalizes so that the string always ends in a final newline. 

119 """ 

120 source = self._source 

121 if not source.endswith('\n'): 

122 source += '\n' 

123 return source 

124 

125 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') 

126 def save(self, filename: typing.Union[os.PathLike, str, None] = None, 

127 directory: typing.Union[os.PathLike, str, None] = None, *, 

128 skip_existing: typing.Optional[bool] = None) -> str: 

129 """Save the DOT source to file. Ensure the file ends with a newline. 

130 

131 Args: 

132 filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) 

133 directory: (Sub)directory for source saving and rendering. 

134 skip_existing: Skip write if file exists (default: ``None``). 

135 By default skips if instance was loaded from the target path: 

136 ``.from_file(self.filepath)``. 

137 

138 Returns: 

139 The (possibly relative) path of the saved source file. 

140 """ 

141 skip = (skip_existing is None and self._loaded_from_path 

142 and os.path.samefile(self._loaded_from_path, self.filepath)) 

143 if skip: 

144 log.debug('.save(skip_existing=None) skip writing Source.from_file(%r)', 

145 self.filepath) 

146 return super().save(filename=filename, directory=directory, 

147 skip_existing=skip)