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

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

86 statements  

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

2 

3import os 

4import pathlib 

5from typing import overload 

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 

18DOUBLE_SUFFIXES = {f'.{fmt}' for fmt in parameters.FORMATS 

19 if fmt in ('xdot1.2', 'xdot1.4')} 

20 

21 

22def get_format(outfile: pathlib.Path, *, format: str | None) -> str: 

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

24 

25 Args: 

26 outfile: Path for the rendered output file. 

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

28 

29 Returns: 

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

31 

32 Warns: 

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

34 is empty/unknown. 

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

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

37 """ 

38 try: 

39 inferred_format = infer_format(outfile) 

40 except ValueError: 

41 if format is None: 

42 msg = ('cannot infer rendering format' 

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

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

45 ' (provide format or outfile with a suffix' 

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

47 raise exceptions.RequiredArgumentError(msg) 

48 

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

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

51 category=exceptions.UnknownSuffixWarning) 

52 return format 

53 else: 

54 assert inferred_format is not None 

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

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

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

58 category=exceptions.FormatSuffixMismatchWarning) 

59 return format 

60 

61 return inferred_format 

62 

63 

64def get_supported_suffixes() -> list[str]: 

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

66 

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

68 ['.bmp', ...] 

69 """ 

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

71 

72 

73def get_supported_formats() -> list[str]: 

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

75 

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

77 ['bmp', ...] 

78 """ 

79 return sorted(parameters.FORMATS) 

80 

81 

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

83 """Return format inferred from outfile suffix. 

84 

85 Args: 

86 outfile: Path for the rendered output file. 

87 

88 Returns: 

89 The inferred format. 

90 

91 Raises: 

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

93 

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

95 'pdf' 

96 

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

98 'svg' 

99 

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

101 'png' 

102 

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

104 Traceback (most recent call last): 

105 ... 

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

107 

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

109 Traceback (most recent call last): 

110 ... 

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

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

113 """ 

114 if not outfile.suffix: 

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

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

117 

118 if (double_sfx := ''.join(outfile.suffixes[-2:]).lower()) in DOUBLE_SUFFIXES: 

119 return double_sfx.removeprefix('.') 

120 

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

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

123 format_ = format_.lower() 

124 

125 try: 

126 parameters.verify_format(format_) 

127 except ValueError: 

128 raise ValueError('cannot infer rendering format' 

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

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

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

132 ' provide outfile with a suffix' 

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

134 return format_ 

135 

136 

137def get_outfile(filepath: os.PathLike[str] | str, *, 

138 format: str, 

139 renderer: str | None = None, 

140 formatter: str | None = None) -> pathlib.Path: 

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

142 

143 See also: 

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

145 """ 

146 filepath = _tools.promote_pathlike(filepath) 

147 

148 parameters.verify_format(format, required=True) 

149 parameters.verify_renderer(renderer, required=False) 

150 parameters.verify_formatter(formatter, required=False) 

151 

152 suffix_args = (formatter, renderer, format) 

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

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

155 

156 

157def get_filepath(outfile: os.PathLike[str] | str) -> pathlib.Path: 

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

159 outfile = _tools.promote_pathlike(outfile) 

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

161 

162 

163@overload 

164def render(engine: str, 

165 format: str, 

166 filepath: os.PathLike[str] | str, 

167 renderer: str | None = ..., 

168 formatter: str | None = ..., 

169 neato_no_op: bool | int | None = ..., 

170 quiet: bool = ..., *, 

171 outfile: None = ..., 

172 raise_if_result_exists: bool = ..., 

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

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

175 

176 

177@overload 

178def render(engine: str, 

179 format: str | None = ..., 

180 filepath: os.PathLike[str] | str | None = ..., 

181 renderer: str | None = ..., 

182 formatter: str | None = ..., 

183 neato_no_op: bool | int | None = ..., 

184 quiet: bool = False, *, 

185 outfile: os.PathLike[str] | str, 

186 raise_if_result_exists: bool = ..., 

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

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

189 

190 

191@overload 

192def render(engine: str, 

193 format: str | None = ..., 

194 filepath: os.PathLike[str] | str | None = ..., 

195 renderer: str | None = ..., 

196 formatter: str | None = ..., 

197 neato_no_op: bool | int | None = ..., 

198 quiet: bool = False, *, 

199 outfile: os.PathLike[str] | str | None = ..., 

200 raise_if_result_exists: bool = ..., 

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

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

203 

204 

205@_tools.deprecate_positional_args(supported_number=3) 

206def render(engine: str, 

207 format: str | None = None, 

208 filepath: os.PathLike[str] | str | None = None, 

209 renderer: str | None = None, 

210 formatter: str | None = None, 

211 neato_no_op: bool | int | None = None, 

212 quiet: bool = False, *, 

213 outfile: os.PathLike[str] | str | None = None, 

214 raise_if_result_exists: bool = False, 

215 overwrite_filepath: bool = False) -> str: 

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

217 

218 Args: 

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

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

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

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

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

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

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

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

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

228 neato_no_op: Neato layout engine no-op flag. 

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

230 outfile: Path for the rendered output file. 

231 raise_if_result_exists: Raise :exc:`graphviz.FileExistsError` 

232 if the result file exists. 

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

234 Incompatible with ``raise_if_result_exists``. 

235 

236 Returns: 

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

238 

239 Raises: 

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

241 are unknown. 

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

243 unless ``outfile`` is given. 

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

245 but ``renderer`` is None. 

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

247 unless ``overwite_filepath=True``. 

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

249 is not found. 

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

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

252 graphviz.FileExistsError: If ``raise_if_exists`` 

253 and the result file exists. 

254 

255 Warns: 

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

257 is empty or unknown. 

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

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

260 

261 Example: 

262 >>> doctest_mark_exe() 

263 >>> import pathlib 

264 >>> import graphviz 

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

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

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

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

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

270 'doctest-output/spam.png' 

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

272 'doctest-output/spam.pdf' 

273 

274 Note: 

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

276 so that references to external files 

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

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

279 

280 See also: 

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

282 """ 

283 if raise_if_result_exists and overwrite_filepath: 

284 raise ValueError('overwrite_filepath cannot be combined' 

285 ' with raise_if_result_exists') 

286 

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

288 

289 if outfile is not None: 

290 format = get_format(outfile, format=format) 

291 

292 if filepath is None: 

293 filepath = get_filepath(outfile) 

294 

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

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

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

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

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

300 

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

302 else outfile.name) 

303 

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

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

306 elif filepath is None: 

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

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

309 elif format is None: 

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

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

312 else: 

313 outfile = get_outfile(filepath, 

314 format=format, 

315 renderer=renderer, 

316 formatter=formatter) 

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

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

319 

320 cmd = dot_command.command(engine, format, 

321 renderer=renderer, 

322 formatter=formatter, 

323 neato_no_op=neato_no_op) 

324 

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

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

327 

328 cmd += args 

329 

330 execute.run_check(cmd, 

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

332 quiet=quiet, 

333 capture_output=True) 

334 

335 return os.fspath(outfile)