Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/graphviz/backend/rendering.py: 52%

82 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:43 +0000

1"""Render DOT source files with Graphviz ``dot``.""" 

2 

3import os 

4import pathlib 

5import typing 

6import warnings 

7 

8from .._defaults import DEFAULT_SOURCE_EXTENSION 

9from .. import _tools 

10from .. import exceptions 

11from .. import parameters 

12 

13from . import dot_command 

14from . import execute 

15 

16__all__ = ['get_format', 'get_filepath', 'render'] 

17 

18 

19def get_format(outfile: pathlib.Path, *, format: typing.Optional[str]) -> str: 

20 """Return format inferred from outfile suffix and/or given ``format``. 

21 

22 Args: 

23 outfile: Path for the rendered output file. 

24 format: Output format for rendering (``'pdf'``, ``'png'``, ...). 

25 

26 Returns: 

27 The given ``format`` falling back to the inferred format. 

28 

29 Warns: 

30 graphviz.UnknownSuffixWarning: If the suffix of ``outfile`` 

31 is empty/unknown. 

32 graphviz.FormatSuffixMismatchWarning: If the suffix of ``outfile`` 

33 does not match the given ``format``. 

34 """ 

35 try: 

36 inferred_format = infer_format(outfile) 

37 except ValueError: 

38 if format is None: 

39 msg = ('cannot infer rendering format' 

40 f' from suffix {outfile.suffix!r}' 

41 f' of outfile: {os.fspath(outfile)!r}' 

42 ' (provide format or outfile with a suffix' 

43 f' from {get_supported_suffixes()!r})') 

44 raise exceptions.RequiredArgumentError(msg) 

45 

46 warnings.warn(f'unknown outfile suffix {outfile.suffix!r}' 

47 f' (expected: {"." + format!r})', 

48 category=exceptions.UnknownSuffixWarning) 

49 return format 

50 else: 

51 assert inferred_format is not None 

52 if format is not None and format.lower() != inferred_format: 

53 warnings.warn(f'expected format {inferred_format!r} from outfile' 

54 f' differs from given format: {format!r}', 

55 category=exceptions.FormatSuffixMismatchWarning) 

56 return format 

57 

58 return inferred_format 

59 

60 

61def get_supported_suffixes() -> typing.List[str]: 

62 """Return a sorted list of supported outfile suffixes for exception/warning messages. 

63 

64 >>> get_supported_suffixes() # doctest: +ELLIPSIS 

65 ['.bmp', ...] 

66 """ 

67 return [f'.{format}' for format in get_supported_formats()] 

68 

69 

70def get_supported_formats() -> typing.List[str]: 

71 """Return a sorted list of supported formats for exception/warning messages. 

72 

73 >>> get_supported_formats() # doctest: +ELLIPSIS 

74 ['bmp', ...] 

75 """ 

76 return sorted(parameters.FORMATS) 

77 

78 

79def infer_format(outfile: pathlib.Path) -> str: 

80 """Return format inferred from outfile suffix. 

81 

82 Args: 

83 outfile: Path for the rendered output file. 

84 

85 Returns: 

86 The inferred format. 

87 

88 Raises: 

89 ValueError: If the suffix of ``outfile`` is empty/unknown. 

90 

91 >>> infer_format(pathlib.Path('spam.pdf')) # doctest: +NO_EXE 

92 'pdf' 

93 

94 >>> infer_format(pathlib.Path('spam.gv.svg')) 

95 'svg' 

96 

97 >>> infer_format(pathlib.Path('spam.PNG')) 

98 'png' 

99 

100 >>> infer_format(pathlib.Path('spam')) 

101 Traceback (most recent call last): 

102 ... 

103 ValueError: cannot infer rendering format from outfile: 'spam' (missing suffix) 

104 

105 >>> infer_format(pathlib.Path('spam.wav')) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE 

106 Traceback (most recent call last): 

107 ... 

108 ValueError: cannot infer rendering format from suffix '.wav' of outfile: 'spam.wav' 

109 (unknown format: 'wav', provide outfile with a suffix from ['.bmp', ...]) 

110 """ 

111 if not outfile.suffix: 

112 raise ValueError('cannot infer rendering format from outfile:' 

113 f' {os.fspath(outfile)!r} (missing suffix)') 

114 

115 start, sep, format_ = outfile.suffix.partition('.') 

116 assert sep and not start, f"{outfile.suffix!r}.startswith('.')" 

117 format_ = format_.lower() 

118 

119 try: 

120 parameters.verify_format(format_) 

121 except ValueError: 

122 raise ValueError('cannot infer rendering format' 

123 f' from suffix {outfile.suffix!r}' 

124 f' of outfile: {os.fspath(outfile)!r}' 

125 f' (unknown format: {format_!r},' 

126 ' provide outfile with a suffix' 

127 f' from {get_supported_suffixes()!r})') 

128 return format_ 

129 

130 

131def get_outfile(filepath: typing.Union[os.PathLike, str], *, 

132 format: str, 

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

134 formatter: typing.Optional[str] = None) -> pathlib.Path: 

135 """Return ``filepath`` + ``[[.formatter].renderer].format``. 

136 

137 See also: 

138 https://www.graphviz.org/doc/info/command.html#-O 

139 """ 

140 filepath = _tools.promote_pathlike(filepath) 

141 

142 parameters.verify_format(format, required=True) 

143 parameters.verify_renderer(renderer, required=False) 

144 parameters.verify_formatter(formatter, required=False) 

145 

146 suffix_args = (formatter, renderer, format) 

147 suffix = '.'.join(a for a in suffix_args if a is not None) 

148 return filepath.with_suffix(f'{filepath.suffix}.{suffix}') 

149 

150 

151def get_filepath(outfile: typing.Union[os.PathLike, str]) -> pathlib.Path: 

152 """Return ``outfile.with_suffix('.gv')``.""" 

153 outfile = _tools.promote_pathlike(outfile) 

154 return outfile.with_suffix(f'.{DEFAULT_SOURCE_EXTENSION}') 

155 

156 

157@typing.overload 

158def render(engine: str, 

159 format: str, 

160 filepath: typing.Union[os.PathLike, str], 

161 renderer: typing.Optional[str] = ..., 

162 formatter: typing.Optional[str] = ..., 

163 neato_no_op: typing.Union[bool, int, None] = ..., 

164 quiet: bool = ..., *, 

165 outfile: typing.Union[os.PathLike, str, None] = ..., 

166 raise_if_result_exists: bool = ..., 

167 overwrite_filepath: bool = ...) -> str: 

168 """Require ``format`` and ``filepath`` with default ``outfile=None``.""" 

169 

170 

171@typing.overload 

172def render(engine: str, 

173 format: typing.Optional[str] = ..., 

174 filepath: typing.Union[os.PathLike, str, None] = ..., 

175 renderer: typing.Optional[str] = ..., 

176 formatter: typing.Optional[str] = ..., 

177 neato_no_op: typing.Union[bool, int, None] = ..., 

178 quiet: bool = False, *, 

179 outfile: typing.Union[os.PathLike, str, None] = ..., 

180 raise_if_result_exists: bool = ..., 

181 overwrite_filepath: bool = ...) -> str: 

182 """Optional ``format`` and ``filepath`` with given ``outfile``.""" 

183 

184 

185@typing.overload 

186def render(engine: str, 

187 format: typing.Optional[str] = ..., 

188 filepath: typing.Union[os.PathLike, str, None] = ..., 

189 renderer: typing.Optional[str] = ..., 

190 formatter: typing.Optional[str] = ..., 

191 neato_no_op: typing.Union[bool, int, None] = ..., 

192 quiet: bool = False, *, 

193 outfile: typing.Union[os.PathLike, str, None] = ..., 

194 raise_if_result_exists: bool = ..., 

195 overwrite_filepath: bool = ...) -> str: 

196 """Required/optional ``format`` and ``filepath`` depending on ``outfile``.""" 

197 

198 

199@_tools.deprecate_positional_args(supported_number=3) 

200def render(engine: str, 

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

202 filepath: typing.Union[os.PathLike, str, None] = None, 

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

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

205 neato_no_op: typing.Union[bool, int, None] = None, 

206 quiet: bool = False, *, 

207 outfile: typing.Union[os.PathLike, str, None] = None, 

208 raise_if_result_exists: bool = False, 

209 overwrite_filepath: bool = False) -> str: 

210 r"""Render file with ``engine`` into ``format`` and return result filename. 

211 

212 Args: 

213 engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...). 

214 format: Output format for rendering (``'pdf'``, ``'png'``, ...). 

215 Can be omitted if an ``outfile`` with a known ``format`` is given, 

216 i.e. if ``outfile`` ends with a known ``.{format}`` suffix. 

217 filepath: Path to the DOT source file to render. 

218 Can be omitted if ``outfile`` is given, 

219 in which case it defaults to ``outfile.with_suffix('.gv')``. 

220 renderer: Output renderer (``'cairo'``, ``'gd'``, ...). 

221 formatter: Output formatter (``'cairo'``, ``'gd'``, ...). 

222 neato_no_op: Neato layout engine no-op flag. 

223 quiet: Suppress ``stderr`` output from the layout subprocess. 

224 outfile: Path for the rendered output file. 

225 raise_if_result_exits: Raise :exc:`graphviz.FileExistsError` 

226 if the result file exists. 

227 overwrite_filepath: Allow ``dot`` to write to the file it reads from. 

228 Incompatible with ``raise_if_result_exists``. 

229 

230 Returns: 

231 The (possibly relative) path of the rendered file. 

232 

233 Raises: 

234 ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` 

235 are unknown. 

236 graphviz.RequiredArgumentError: If ``format`` or ``filepath`` are None 

237 unless ``outfile`` is given. 

238 graphviz.RequiredArgumentError: If ``formatter`` is given 

239 but ``renderer`` is None. 

240 ValueError: If ``outfile`` and ``filename`` are the same file 

241 unless ``overwite_filepath=True``. 

242 graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable 

243 is not found. 

244 graphviz.CalledProcessError: If the returncode (exit status) 

245 of the rendering ``dot`` subprocess is non-zero. 

246 graphviz.FileExistsError: If ``raise_if_exists`` 

247 and the result file exists. 

248 

249 Warns: 

250 graphviz.UnknownSuffixWarning: If the suffix of ``outfile`` 

251 is empty or unknown. 

252 graphviz.FormatSuffixMismatchWarning: If the suffix of ``outfile`` 

253 does not match the given ``format``. 

254 

255 Example: 

256 >>> doctest_mark_exe() 

257 >>> import pathlib 

258 >>> import graphviz 

259 >>> assert pathlib.Path('doctest-output/spam.gv').write_text('graph { spam }') == 14 

260 >>> graphviz.render('dot', 'png', 'doctest-output/spam.gv').replace('\\', '/') 

261 'doctest-output/spam.gv.png' 

262 >>> graphviz.render('dot', filepath='doctest-output/spam.gv', 

263 ... outfile='doctest-output/spam.png').replace('\\', '/') 

264 'doctest-output/spam.png' 

265 >>> graphviz.render('dot', outfile='doctest-output/spam.pdf').replace('\\', '/') 

266 'doctest-output/spam.pdf' 

267 

268 Note: 

269 The layout command is started from the directory of ``filepath``, 

270 so that references to external files 

271 (e.g. ``[image=images/camelot.png]``) 

272 can be given as paths relative to the DOT source file. 

273 

274 See also: 

275 Upstream docs: https://www.graphviz.org/doc/info/command.html 

276 """ 

277 if raise_if_result_exists and overwrite_filepath: 

278 raise ValueError('overwrite_filepath cannot be combined' 

279 ' with raise_if_result_exists') 

280 

281 filepath, outfile = map(_tools.promote_pathlike, (filepath, outfile)) 

282 

283 if outfile is not None: 

284 format = get_format(outfile, format=format) 

285 

286 if filepath is None: 

287 filepath = get_filepath(outfile) 

288 

289 if (not overwrite_filepath and outfile.name == filepath.name 

290 and outfile.resolve() == filepath.resolve()): # noqa: E129 

291 raise ValueError(f'outfile {outfile.name!r} must be different' 

292 f' from input file {filepath.name!r}' 

293 ' (pass overwrite_filepath=True to override)') 

294 

295 outfile_arg = (outfile.resolve() if outfile.parent != filepath.parent 

296 else outfile.name) 

297 

298 # https://www.graphviz.org/doc/info/command.html#-o 

299 args = ['-o', outfile_arg, filepath.name] 

300 elif filepath is None: 

301 raise exceptions.RequiredArgumentError('filepath: (required if outfile is not given,' 

302 f' got {filepath!r})') 

303 elif format is None: 

304 raise exceptions.RequiredArgumentError('format: (required if outfile is not given,' 

305 f' got {format!r})') 

306 else: 

307 outfile = get_outfile(filepath, 

308 format=format, 

309 renderer=renderer, 

310 formatter=formatter) 

311 # https://www.graphviz.org/doc/info/command.html#-O 

312 args = ['-O', filepath.name] 

313 

314 cmd = dot_command.command(engine, format, 

315 renderer=renderer, 

316 formatter=formatter, 

317 neato_no_op=neato_no_op) 

318 

319 if raise_if_result_exists and os.path.exists(outfile): 

320 raise exceptions.FileExistsError(f'output file exists: {os.fspath(outfile)!r}') 

321 

322 cmd += args 

323 

324 execute.run_check(cmd, 

325 cwd=filepath.parent if filepath.parent.parts else None, 

326 quiet=quiet, 

327 capture_output=True) 

328 

329 return os.fspath(outfile)