Coverage for src/debputy/lsp/lsp_self_check.py: 51%
75 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
1import dataclasses
2import os.path
3import subprocess
4from typing import List, Optional, TypeVar
5from collections.abc import Callable, Sequence
7from debian.debian_support import Version
9from debputy.util import _error
12@dataclasses.dataclass(slots=True, frozen=True)
13class LSPSelfCheck:
14 feature: str
15 test: Callable[[], bool]
16 problem: str
17 how_to_fix: str
18 is_mandatory: bool = False
21LSP_CHECKS: list[LSPSelfCheck] = []
23C = TypeVar("C", bound="Callable")
26def lsp_import_check(
27 packages: Sequence[str],
28 *,
29 feature_name: str | None = None,
30 is_mandatory: bool = False,
31) -> Callable[[C], C]:
33 def _wrapper(func: C) -> C:
35 def _impl():
36 try:
37 r = func()
38 except ImportError:
39 return False
40 return r is None or bool(r)
42 suffix = "fix this issue" if is_mandatory else "enable this feature"
44 LSP_CHECKS.append(
45 LSPSelfCheck(
46 _feature_name(feature_name, func),
47 _impl,
48 "Missing dependencies",
49 f"Run `apt satisfy '{', '.join(packages)}'` to {suffix}",
50 is_mandatory=is_mandatory,
51 )
52 )
53 return func
55 return _wrapper
58def lsp_generic_check(
59 problem: str,
60 how_to_fix: str,
61 *,
62 feature_name: str | None = None,
63 is_mandatory: bool = False,
64) -> Callable[[C], C]:
66 def _wrapper(func: C) -> C:
67 LSP_CHECKS.append(
68 LSPSelfCheck(
69 _feature_name(feature_name, func),
70 func,
71 problem,
72 how_to_fix,
73 is_mandatory=is_mandatory,
74 )
75 )
76 return func
78 return _wrapper
81def _feature_name(feature: str | None, func: Callable[[], None]) -> str:
82 if feature is not None:
83 return feature
84 return func.__name__.replace("_", " ")
87@lsp_import_check(["python3-lsprotocol", "python3-pygls"], is_mandatory=True)
88def minimum_requirements() -> bool:
89 import pygls.server
91 # The hasattr is mostly irrelevant; but it avoids the import being flagged as redundant.
92 return hasattr(pygls.server, "LanguageServer")
95@lsp_import_check(["python3-levenshtein"])
96def typo_detection() -> bool:
97 import Levenshtein
99 # The hasattr is mostly irrelevant; but it avoids the import being flagged as redundant.
100 return hasattr(Levenshtein, "distance")
103@lsp_import_check(["hunspell-en-us", "python3-hunspell"])
104def spell_checking() -> bool:
105 import hunspell
107 # The hasattr is mostly irrelevant; but it avoids the import being flagged as redundant.
108 return hasattr(hunspell, "HunSpell") and os.path.exists(
109 "/usr/share/hunspell/en_US.dic"
110 )
113@lsp_generic_check(
114 feature_name="extra dh support",
115 problem="Missing dependencies",
116 how_to_fix="Run `apt satisfy debhelper (>= 13.16~)` to enable this feature",
117)
118def check_dh_version() -> bool:
119 try:
120 output = subprocess.check_output(
121 [
122 "dpkg-query",
123 "-W",
124 "--showformat=${Version} ${db:Status-Status}\n",
125 "debhelper",
126 ]
127 ).decode("utf-8")
128 except (FileNotFoundError, subprocess.CalledProcessError):
129 return False
130 else:
131 parts = output.split()
132 if len(parts) != 2:
133 return False
134 if parts[1] != "installed":
135 return False
136 return Version(parts[0]) >= Version("13.16~")
139@lsp_generic_check(
140 feature_name="apt cache packages",
141 problem="Missing apt or empty apt cache",
142 how_to_fix="",
143)
144def check_apt_cache() -> bool:
145 try:
146 output = subprocess.check_output(
147 [
148 "apt-get",
149 "indextargets",
150 "--format",
151 "$(IDENTIFIER)",
152 ]
153 ).decode("utf-8")
154 except (FileNotFoundError, subprocess.CalledProcessError):
155 return False
156 for line in output.splitlines():
157 if line.strip() == "Packages":
158 return True
160 return False
163def assert_can_start_lsp() -> None:
164 for self_check in LSP_CHECKS:
165 if self_check.is_mandatory and not self_check.test():
166 _error(
167 f"Cannot start the language server. {self_check.problem}. {self_check.how_to_fix}"
168 )