Coverage for bookbuilderpy/versions.py: 91%
78 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-17 23:15 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-17 23:15 +0000
1"""Get the versions of all involved libraries and tools."""
3import importlib.metadata as ilm
4import platform
5import subprocess # nosec
6from typing import Final
8import bookbuilderpy.version as ver
9from bookbuilderpy.logger import logger
11#: the name of the calibre executable tool
12TOOL_CALIBRE: Final[str] = "calibre"
13#: the name of the firefox driver tool
14TOOL_FIREFOX_DRIVER: Final[str] = "geckodriver"
15#: the name of the firefox browser executable tool
16TOOL_FIREFOX: Final[str] = "firefox"
17#: the name of the ghostscript executable tool
18TOOL_GHOSTSCRIPT: Final[str] = "gs"
19#: the name of the git executable tool
20TOOL_GIT: Final[str] = "git"
21#: the name of the pandoc executable tool
22TOOL_PANDOC: Final[str] = "pandoc"
23#: the name of the pdflatex executable tool
24TOOL_PDFLATEX: Final[str] = "pdflatex"
25#: the name of the rsvg-convert executable tool
26TOOL_RSVG_CONVERT: Final[str] = "rsvg-convert"
27#: the name of the tar executable tool
28TOOL_TAR: Final[str] = "tar"
29#: the name of the xelatex executable tool
30TOOL_XELATEX: Final[str] = "xelatex"
31#: the name of the xz executable tool
32TOOL_XZ: Final[str] = "xz"
33#: the name of the zip executable tool
34TOOL_ZIP: Final[str] = "zip"
37def __chkstr(n: str,
38 purge_starts: tuple[str, ...] =
39 ("copyright", "there is no", "covered by",
40 "the lesser gnu g", "the gnu gen", "for more info",
41 "named copying", "primary author", "currently maintaine",
42 "the author", "latest sourc", "as of above",
43 "encryption notice", "the encryption", "put in the", "and, to",
44 "in both s", "the usa", "administration regulat",
45 "this is free", "warranty", "the source ", "testing/gecko",
46 "this program", "license", "you can obt", "written by",
47 "no lsb mod", "(see the b", "bzip2 code ")) -> str | None:
48 """
49 Check whether we should keep a version string.
51 :param n: the original string
52 :param purge_starts: the strings to purge at the start
53 :return: the string, or `None` if it can be purged
54 """
55 n = n.strip()
56 if len(n) <= 0:
57 return None
58 n = n.replace("\t", " ")
59 nlen = len(n)
60 while True:
61 n = n.replace(" ", " ")
62 nlen2 = len(n)
63 if nlen2 >= nlen:
64 break
65 nlen = nlen2
66 nl: Final[str] = n.lower()
67 if any(nl.startswith(d) for d in purge_starts):
68 return None
69 return n
72def _do_call(tool: str, arg: str) -> tuple[str, bool]:
73 """
74 Invoke a sub-process.
76 :param tool: the tool
77 :param arg: the argument
78 :return: the output
79 """
80 try:
81 # nosemgrep
82 ret = subprocess.run([tool, arg], check=False, # nosec # noqa
83 text=True, capture_output=True, # nosec # noqa
84 timeout=360) # nosec # noqa
85 except FileNotFoundError:
86 return f"{tool} not found", False
87 except BaseException as be:
88 return f"encountered {type(be)} when invoking {tool}", False
90 if ret.returncode != 0:
91 return f"failed to invoke {tool}", False
93 lines = [a for a in [__chkstr(f) for f in ret.stdout.split("\n")]
94 if a]
95 if len(lines) <= 0:
96 return f"{tool} query gives empty result", False
97 return "\n".join(lines), True
100class __Versions:
101 """The internal singleton with the versions."""
103 def __init__(self):
104 """Initialize."""
105 #: the set of tool information
106 self.__has_tool: Final[dict[str, tuple[str, bool]]] = {}
107 #: the version string
108 self.__versions: str | None = None
110 def has_tool(self, tool: str) -> bool:
111 """
112 Check if the given tool is installed.
114 :param tool: the tool executable
115 :return: `True` if the tool is installed, `False` otherwise.
116 """
117 if tool in self.__has_tool:
118 return self.__has_tool[tool][1]
119 self.__has_tool[tool] = h = _do_call(tool, "--version")
120 return h[1]
122 def get_versions(self) -> str:
123 """
124 Get the versions of all involved libraries and tools.
126 :return: a string with version information of all libraries and tools
127 """
128 if self.__versions:
129 return self.__versions
131 logger("obtaining all version information.")
132 versions: Final[list[str]] = \
133 [f"python version: {platform.python_version()}",
134 f"python build: {platform.python_build()[1]}",
135 f"python compiler: {platform.python_compiler()}",
136 f"python implementation: {platform.python_implementation()}",
137 f"bookbuilderpy: {ver.__version__}"]
139 for package in ["beautifulsoup4", "markdown", "minify_html", "pyyaml",
140 "regex", "strip-hints", "urllib3", "yapf"]:
141 version = ilm.version(package).strip()
142 versions.append(f"package {package}: {version}")
144 versions.append(f"\nlinux: {_do_call('uname', '-a')[0]}")
145 versions.append(_do_call("lsb_release", "-a")[0])
147 for tool in [TOOL_CALIBRE, TOOL_FIREFOX, TOOL_FIREFOX_DRIVER,
148 TOOL_GHOSTSCRIPT, TOOL_GIT, TOOL_PANDOC,
149 TOOL_PDFLATEX, TOOL_RSVG_CONVERT, TOOL_TAR,
150 TOOL_XELATEX, TOOL_XZ, TOOL_ZIP]:
151 has: tuple[str, bool]
152 if tool in self.__has_tool:
153 has = self.__has_tool[tool]
154 else:
155 self.__has_tool[tool] = has = _do_call(tool, "--version")
156 versions.append(f"\n{tool}: {has[0]}")
158 self.__versions = "\n".join(versions)
159 return self.__versions
162#: The shared internal singleton
163__SINGLETON: Final[__Versions] = __Versions()
166def has_tool(tool: str) -> bool:
167 """
168 Check if the given tool is installed.
170 :param tool: the tool executable
171 :return: `True` if the tool is installed, `False` otherwise.
172 """
173 return __SINGLETON.has_tool(tool)
176def get_versions() -> str:
177 """
178 Get the versions of all involved libraries and tools.
180 :return: a string with version information of all libraries and tools
181 """
182 return __SINGLETON.get_versions()