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

1import dataclasses 

2import os.path 

3import subprocess 

4from typing import List, Optional, TypeVar 

5from collections.abc import Callable, Sequence 

6 

7from debian.debian_support import Version 

8 

9from debputy.util import _error 

10 

11 

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 

19 

20 

21LSP_CHECKS: list[LSPSelfCheck] = [] 

22 

23C = TypeVar("C", bound="Callable") 

24 

25 

26def lsp_import_check( 

27 packages: Sequence[str], 

28 *, 

29 feature_name: str | None = None, 

30 is_mandatory: bool = False, 

31) -> Callable[[C], C]: 

32 

33 def _wrapper(func: C) -> C: 

34 

35 def _impl(): 

36 try: 

37 r = func() 

38 except ImportError: 

39 return False 

40 return r is None or bool(r) 

41 

42 suffix = "fix this issue" if is_mandatory else "enable this feature" 

43 

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 

54 

55 return _wrapper 

56 

57 

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]: 

65 

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 

77 

78 return _wrapper 

79 

80 

81def _feature_name(feature: str | None, func: Callable[[], None]) -> str: 

82 if feature is not None: 

83 return feature 

84 return func.__name__.replace("_", " ") 

85 

86 

87@lsp_import_check(["python3-lsprotocol", "python3-pygls"], is_mandatory=True) 

88def minimum_requirements() -> bool: 

89 import pygls.server 

90 

91 # The hasattr is mostly irrelevant; but it avoids the import being flagged as redundant. 

92 return hasattr(pygls.server, "LanguageServer") 

93 

94 

95@lsp_import_check(["python3-levenshtein"]) 

96def typo_detection() -> bool: 

97 import Levenshtein 

98 

99 # The hasattr is mostly irrelevant; but it avoids the import being flagged as redundant. 

100 return hasattr(Levenshtein, "distance") 

101 

102 

103@lsp_import_check(["hunspell-en-us", "python3-hunspell"]) 

104def spell_checking() -> bool: 

105 import hunspell 

106 

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 ) 

111 

112 

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~") 

137 

138 

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 

159 

160 return False 

161 

162 

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 )