Coverage for app/venv/lib/python3.14/site-packages/weblate/utils/requirements.py: 70%
96 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
1# Copyright © Michal Čihař <michal@weblate.org>
2#
3# SPDX-License-Identifier: GPL-3.0-or-later
5import sys
6from importlib.metadata import PackageNotFoundError, metadata
7from typing import TYPE_CHECKING, cast
9from django.core.cache import cache
10from django.core.exceptions import ImproperlyConfigured
11from django.db import DatabaseError, connection
13import weblate.utils.version
14from weblate.utils.cache import is_redis_cache
15from weblate.utils.db import using_postgresql
16from weblate.utils.errors import report_error
17from weblate.vcs.git import GitRepository, GitWithGerritRepository, SubversionRepository
18from weblate.vcs.mercurial import HgRepository
20if TYPE_CHECKING: 20 ↛ 21line 20 didn't jump to line 21 because the condition on line 20 was never true
21 from django_redis.cache import RedisCache
23REQUIRES = [
24 "Django",
25 "siphashc",
26 "translate-toolkit",
27 "lxml",
28 "Pillow",
29 "nh3",
30 "python-dateutil",
31 "social-auth-core",
32 "social-auth-app-django",
33 "django-crispy-forms",
34 "oauthlib",
35 "django-compressor",
36 "djangorestframework",
37 "django-filter",
38 "django-appconf",
39 "user-agents",
40 "filelock",
41 "rapidfuzz",
42 "openpyxl",
43 "celery",
44 "django-celery-beat",
45 "kombu",
46 "translation-finder",
47 "weblate-language-data",
48 "html2text",
49 "pycairo",
50 "pygobject",
51 "diff-match-patch",
52 "requests",
53 "django-redis",
54 "hiredis",
55 "sentry_sdk",
56 "Cython",
57 "mistletoe",
58 "GitPython",
59 "borgbackup",
60 "pyparsing",
61 "ahocorasick_rs",
62 "charset-normalizer",
63 "cyrtranslit",
64 "drf_spectacular",
65]
67OPTIONAL = [
68 "psycopg",
69 "psycopg-binary",
70 "phply",
71 "ruamel.yaml",
72 "tesserocr",
73 "boto3",
74 "aeidon",
75 "iniparse",
76 "mysqlclient",
77 "google-cloud-translate",
78 "openai",
79]
82def get_version_module(name, optional=False) -> tuple[str, str, str] | None:
83 """
84 Return module object.
86 On error raises verbose exception with name and URL.
87 """
88 try:
89 package = metadata(name)
90 except PackageNotFoundError as exc:
91 if optional:
92 return None
93 msg = f"Missing dependency {name}, please install using: pip install {name}"
94 raise ImproperlyConfigured(msg) from exc
95 url = package.get("Home-page")
96 if url is None and (project_urls := package.get_all("Project-URL")):
97 for project_url in project_urls:
98 url_name, current_url = project_url.split(",", 1)
99 if url_name.lower().strip() == "homepage":
100 url = current_url.strip()
101 break
102 if url is None:
103 url = f"https://pypi.org/project/{name}/"
104 return (
105 package["Name"],
106 url,
107 package["Version"],
108 )
111def get_optional_versions() -> list[tuple[str, str, str]]:
112 """Return versions of optional modules."""
113 result = []
115 for name in OPTIONAL:
116 module = get_version_module(name, True)
117 if module is not None: 117 ↛ 115line 117 didn't jump to line 115 because the condition on line 117 was always true
118 result.append(module)
120 if HgRepository.is_supported(): 120 ↛ 125line 120 didn't jump to line 125 because the condition on line 120 was always true
121 result.append(
122 ("Mercurial", "https://www.mercurial-scm.org/", HgRepository.get_version())
123 )
125 if SubversionRepository.is_supported(): 125 ↛ 134line 125 didn't jump to line 134 because the condition on line 125 was always true
126 result.append(
127 (
128 "git-svn",
129 "https://git-scm.com/docs/git-svn",
130 SubversionRepository.get_version(),
131 )
132 )
134 if GitWithGerritRepository.is_supported(): 134 ↛ 143line 134 didn't jump to line 143 because the condition on line 134 was always true
135 result.append(
136 (
137 "git-review",
138 "https://pypi.org/project/git-review/",
139 GitWithGerritRepository.get_version(),
140 )
141 )
143 return result
146def get_versions() -> list[tuple[str, str, str]]:
147 """Return list of used versions."""
148 result: list[tuple[str, str, str]] = [
149 module for name in REQUIRES if (module := get_version_module(name))
150 ]
152 result.append(("Python", "https://www.python.org/", sys.version.split()[0]))
154 try:
155 result.append(("Git", "https://git-scm.com/", GitRepository.get_version()))
156 except OSError as exc:
157 msg = "Could not run git, please install it."
158 raise ImproperlyConfigured(msg) from exc
160 return result
163def get_db_version() -> tuple[str, str, str] | None:
164 if using_postgresql(): 164 ↛ 178line 164 didn't jump to line 178 because the condition on line 164 was always true
165 try:
166 with connection.cursor() as cursor:
167 cursor.execute("SHOW server_version")
168 version = cursor.fetchone()
169 except (RuntimeError, DatabaseError):
170 report_error("PostgreSQL version check")
171 return None
173 return (
174 "PostgreSQL server",
175 "https://www.postgresql.org/",
176 version[0].split(" ")[0],
177 )
178 try:
179 with connection.cursor() as cursor:
180 version = cursor.connection.get_server_info()
181 except (RuntimeError, DatabaseError):
182 report_error("MySQL version check")
183 return None
184 return (
185 f"{connection.display_name} sever",
186 "https://mariadb.org/"
187 if connection.mysql_is_mariadb # type: ignore[attr-defined]
188 else "https://www.mysql.com/",
189 version.split("-", 1)[0],
190 )
193def get_cache_version() -> tuple[str, str, str] | None:
194 if is_redis_cache(): 194 ↛ 206line 194 didn't jump to line 206 because the condition on line 194 was always true
195 try:
196 client_info = cast("RedisCache", cache).client.get_client().info()
197 except RuntimeError:
198 report_error("Redis version check")
199 return None
201 if version := client_info.get("valkey_version"): 201 ↛ 203line 201 didn't jump to line 203 because the condition on line 201 was always true
202 return ("Valkey server", "https://valkey.io/", version)
203 if version := client_info.get("redis_version"):
204 return ("Redis server", "https://redis.io/", version)
206 return None
209def get_db_cache_version() -> list[tuple[str, str, str]]:
210 """Return the list of all the Database and Cache version."""
211 result = []
212 cache_version = get_cache_version()
213 if cache_version: 213 ↛ 215line 213 didn't jump to line 215 because the condition on line 213 was always true
214 result.append(cache_version)
215 db_version = get_db_version()
216 if db_version: 216 ↛ 218line 216 didn't jump to line 218 because the condition on line 216 was always true
217 result.append(db_version)
218 return result
221def get_versions_list() -> list[tuple[str, str, str]]:
222 """Return list with version information summary."""
223 return [
224 ("Weblate", "https://weblate.org/", weblate.utils.version.GIT_VERSION),
225 *get_versions(),
226 *get_optional_versions(),
227 *get_db_cache_version(),
228 ]