1"""Return the version number from running ``dot -V``."""
2
3import logging
4import os
5import re
6import subprocess
7from typing import Final
8
9from . import dot_command
10from . import execute
11
12VERSION_PATTERN: Final = re.compile(r'''
13 graphviz[ ]
14 version[ ]
15 (\d+)\.(\d+)
16 (?:\.(\d+)
17 (?:
18 ~dev\.\d{8}\.\d{4}
19 |
20 \.(\d+)
21 )?
22 )?
23 [ ]
24 ''', re.VERBOSE)
25
26
27log = logging.getLogger(__name__)
28
29
30def version() -> tuple[int, ...]:
31 """Return the upstream version number tuple from ``stderr`` of ``dot -V``.
32
33 Returns:
34 Two, three, or four ``int`` version ``tuple``.
35
36 Raises:
37 graphviz.ExecutableNotFound: If the Graphviz executable is not found.
38 graphviz.CalledProcessError: If the exit status is non-zero.
39 RuntimeError: If the output cannot be parsed into a version number.
40
41 Example:
42 >>> doctest_mark_exe()
43 >>> import graphviz
44 >>> graphviz.version() # doctest: +ELLIPSIS
45 (...)
46
47 Note:
48 Ignores the ``~dev.<YYYYmmdd.HHMM>`` portion of development versions.
49
50 See also:
51 Upstream release version entry format:
52 https://gitlab.com/graphviz/graphviz/-/blob/f94e91ba819cef51a4b9dcb2d76153684d06a913/gen_version.py#L17-20
53 """
54 cmd: list[os.PathLike[str] | str] = [dot_command.DOT_BINARY, '-V']
55 proc = execute.run_check(cmd,
56 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
57 encoding='ascii')
58
59 ma = VERSION_PATTERN.search(proc.stdout)
60 if ma is None:
61 raise RuntimeError(f'cannot parse {cmd!r} output: {proc.stdout!r}')
62
63 return tuple(int(d) for d in ma.groups() if d is not None)