Coverage for src/debputy/packager_provided_files.py: 84%

140 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1import collections 

2import dataclasses 

3from typing import Mapping, Iterable, Dict, List, Optional, Tuple 

4 

5from debputy.packages import BinaryPackage 

6from debputy.plugin.api import VirtualPath 

7from debputy.plugin.api.impl_types import PackagerProvidedFileClassSpec 

8from debputy.util import _error 

9 

10 

11@dataclasses.dataclass(frozen=True, slots=True) 

12class PackagerProvidedFile: 

13 path: VirtualPath 

14 package_name: str 

15 installed_as_basename: str 

16 provided_key: str 

17 definition: PackagerProvidedFileClassSpec 

18 match_priority: int = 0 

19 fuzzy_match: bool = False 

20 

21 def compute_dest(self) -> Tuple[str, str]: 

22 return self.definition.compute_dest( 

23 self.installed_as_basename, 

24 owning_package=self.package_name, 

25 path=self.path, 

26 ) 

27 

28 

29@dataclasses.dataclass(frozen=True, slots=True) 

30class PerPackagePackagerProvidedResult: 

31 auto_installable: List[PackagerProvidedFile] 

32 reserved_only: Dict[str, List[PackagerProvidedFile]] 

33 

34 

35def _find_package_name_prefix( 

36 binary_packages: Mapping[str, BinaryPackage], 

37 main_binary_package: str, 

38 max_periods_in_package_name: int, 

39 path: VirtualPath, 

40 *, 

41 allow_fuzzy_matches: bool = False, 

42) -> Iterable[Tuple[str, str, bool, bool]]: 

43 if max_periods_in_package_name < 1: 

44 prefix, remaining = path.name.split(".", 1) 

45 package_name = prefix 

46 bug_950723 = False 

47 if allow_fuzzy_matches and package_name.endswith("@"): 47 ↛ 48line 47 didn't jump to line 48, because the condition on line 47 was never true

48 package_name = package_name[:-1] 

49 bug_950723 = True 

50 if package_name in binary_packages: 50 ↛ 53line 50 didn't jump to line 53, because the condition on line 50 was never false

51 yield package_name, remaining, True, bug_950723 

52 else: 

53 yield main_binary_package, path.name, False, False 

54 return 

55 

56 parts = path.name.split(".", max_periods_in_package_name + 1) 

57 for p in range(len(parts) - 1, 0, -1): 

58 name = ".".join(parts[0:p]) 

59 bug_950723 = False 

60 if allow_fuzzy_matches and name.endswith("@"): 60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true

61 name = name[:-1] 

62 bug_950723 = True 

63 

64 if name in binary_packages: 

65 remaining = ".".join(parts[p:]) 

66 yield name, remaining, True, bug_950723 

67 # main package case 

68 yield main_binary_package, path.name, False, False 

69 

70 

71def _find_definition( 

72 packager_provided_files: Mapping[str, PackagerProvidedFileClassSpec], 

73 basename: str, 

74) -> Tuple[Optional[str], Optional[PackagerProvidedFileClassSpec]]: 

75 definition = packager_provided_files.get(basename) 

76 if definition is not None: 

77 return None, definition 

78 install_as_name = basename 

79 file_class = "" 

80 while "." in install_as_name: 

81 install_as_name, file_class_part = install_as_name.rsplit(".", 1) 

82 file_class = ( 

83 file_class_part + "." + file_class if file_class != "" else file_class_part 

84 ) 

85 definition = packager_provided_files.get(file_class) 

86 if definition is not None: 

87 return install_as_name, definition 

88 return None, None 

89 

90 

91def _check_mismatches( 

92 path: VirtualPath, 

93 definition: PackagerProvidedFileClassSpec, 

94 owning_package: BinaryPackage, 

95 install_as_name: Optional[str], 

96 had_arch: bool, 

97) -> None: 

98 if install_as_name is not None and not definition.allow_name_segment: 98 ↛ 99line 98 didn't jump to line 99, because the condition on line 98 was never true

99 _error( 

100 f'The file "{path.fs_path}" looks like a packager provided file for' 

101 f' {owning_package.name} of type {definition.stem} with the custom name "{install_as_name}".' 

102 " However, this file type does not allow custom naming. The file type was registered" 

103 f" by {definition.debputy_plugin_metadata.plugin_name} in case you disagree and want" 

104 " to file a bug/feature request." 

105 ) 

106 if had_arch: 

107 if owning_package.is_arch_all: 107 ↛ 108line 107 didn't jump to line 108, because the condition on line 107 was never true

108 _error( 

109 f'The file "{path.fs_path}" looks like an architecture specific packager provided file for' 

110 f" {owning_package.name} of type {definition.stem}." 

111 " However, the package in question is arch:all. The use of architecture specific files" 

112 " for arch:all packages does not make sense." 

113 ) 

114 if not definition.allow_architecture_segment: 114 ↛ 115line 114 didn't jump to line 115, because the condition on line 114 was never true

115 _error( 

116 f'The file "{path.fs_path}" looks like an architecture specific packager provided file for' 

117 f" {owning_package.name} of type {definition.stem}." 

118 " However, this file type does not allow architecture specific variants. The file type was registered" 

119 f" by {definition.debputy_plugin_metadata.plugin_name} in case you disagree and want" 

120 " to file a bug/feature request." 

121 ) 

122 

123 

124def _split_path( 

125 packager_provided_files: Mapping[str, PackagerProvidedFileClassSpec], 

126 binary_packages: Mapping[str, BinaryPackage], 

127 main_binary_package: str, 

128 max_periods_in_package_name: int, 

129 path: VirtualPath, 

130 *, 

131 allow_fuzzy_matches: bool = False, 

132) -> Iterable[PackagerProvidedFile]: 

133 owning_package_name = main_binary_package 

134 basename = path.name 

135 match_priority = 0 

136 had_arch = False 

137 if "." not in basename: 

138 definition = packager_provided_files.get(basename) 

139 if definition is None: 139 ↛ 140line 139 didn't jump to line 140, because the condition on line 139 was never true

140 return 

141 if definition.packageless_is_fallback_for_all_packages: 

142 yield from ( 

143 PackagerProvidedFile( 

144 path=path, 

145 package_name=n, 

146 installed_as_basename=n, 

147 provided_key=".UNNAMED.", 

148 definition=definition, 

149 match_priority=match_priority, 

150 fuzzy_match=False, 

151 ) 

152 for n in binary_packages 

153 ) 

154 else: 

155 yield PackagerProvidedFile( 

156 path=path, 

157 package_name=owning_package_name, 

158 installed_as_basename=owning_package_name, 

159 provided_key=".UNNAMED.", 

160 definition=definition, 

161 match_priority=match_priority, 

162 fuzzy_match=False, 

163 ) 

164 return 

165 

166 for ( 

167 owning_package_name, 

168 basename, 

169 explicit_package, 

170 bug_950723, 

171 ) in _find_package_name_prefix( 

172 binary_packages, 

173 main_binary_package, 

174 max_periods_in_package_name, 

175 path, 

176 allow_fuzzy_matches=allow_fuzzy_matches, 

177 ): 

178 owning_package = binary_packages[owning_package_name] 

179 match_priority = 1 if explicit_package else 0 

180 fuzzy_match = False 

181 

182 if allow_fuzzy_matches and basename.endswith(".in") and len(basename) > 3: 182 ↛ 183line 182 didn't jump to line 183, because the condition on line 182 was never true

183 basename = basename[:-3] 

184 fuzzy_match = True 

185 

186 if "." in basename: 

187 remaining, last_word = basename.rsplit(".", 1) 

188 # We cannot use "resolved_architecture" as it would return "all". 

189 if last_word == owning_package.package_deb_architecture_variable("ARCH"): 

190 match_priority = 3 

191 basename = remaining 

192 had_arch = True 

193 elif last_word == owning_package.package_deb_architecture_variable( 193 ↛ 196line 193 didn't jump to line 196, because the condition on line 193 was never true

194 "ARCH_OS" 

195 ): 

196 match_priority = 2 

197 basename = remaining 

198 had_arch = True 

199 elif last_word == "all" and owning_package.is_arch_all: 199 ↛ 202line 199 didn't jump to line 202, because the condition on line 199 was never true

200 # This case does not make sense, but we detect it so we can report an error 

201 # via _check_mismatches. 

202 match_priority = -1 

203 basename = remaining 

204 had_arch = True 

205 

206 install_as_name, definition = _find_definition( 

207 packager_provided_files, basename 

208 ) 

209 if definition is None: 

210 continue 

211 

212 # Note: bug_950723 implies allow_fuzzy_matches 

213 if bug_950723 and not definition.bug_950723: 213 ↛ 214line 213 didn't jump to line 214, because the condition on line 213 was never true

214 continue 

215 

216 _check_mismatches( 

217 path, 

218 definition, 

219 owning_package, 

220 install_as_name, 

221 had_arch, 

222 ) 

223 if ( 

224 definition.packageless_is_fallback_for_all_packages 

225 and install_as_name is None 

226 and not had_arch 

227 and not explicit_package 

228 ): 

229 yield from ( 

230 PackagerProvidedFile( 

231 path=path, 

232 package_name=n, 

233 installed_as_basename=f"{n}@" if bug_950723 else n, 

234 provided_key=".UNNAMED." if bug_950723 else ".UNNAMED@.", 

235 definition=definition, 

236 match_priority=match_priority, 

237 fuzzy_match=fuzzy_match, 

238 ) 

239 for n in binary_packages 

240 ) 

241 else: 

242 provided_key = ( 

243 install_as_name if install_as_name is not None else ".UNNAMED." 

244 ) 

245 basename = ( 

246 install_as_name if install_as_name is not None else owning_package_name 

247 ) 

248 if bug_950723: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true

249 provided_key = f"{provided_key}@" 

250 basename = f"{basename}@" 

251 yield PackagerProvidedFile( 

252 path=path, 

253 package_name=owning_package_name, 

254 installed_as_basename=basename, 

255 provided_key=provided_key, 

256 definition=definition, 

257 match_priority=match_priority, 

258 fuzzy_match=fuzzy_match, 

259 ) 

260 return 

261 

262 

263def detect_all_packager_provided_files( 

264 packager_provided_files: Mapping[str, PackagerProvidedFileClassSpec], 

265 debian_dir: VirtualPath, 

266 binary_packages: Mapping[str, BinaryPackage], 

267 *, 

268 allow_fuzzy_matches: bool = False, 

269) -> Dict[str, PerPackagePackagerProvidedResult]: 

270 main_binary_package = [ 

271 p.name for p in binary_packages.values() if p.is_main_package 

272 ][0] 

273 provided_files: Dict[str, Dict[Tuple[str, str], PackagerProvidedFile]] = { 

274 n: {} for n in binary_packages 

275 } 

276 max_periods_in_package_name = max(name.count(".") for name in binary_packages) 

277 

278 for entry in debian_dir.iterdir: 

279 if entry.is_dir: 279 ↛ 280line 279 didn't jump to line 280, because the condition on line 279 was never true

280 continue 

281 matching_ppfs = _split_path( 

282 packager_provided_files, 

283 binary_packages, 

284 main_binary_package, 

285 max_periods_in_package_name, 

286 entry, 

287 allow_fuzzy_matches=allow_fuzzy_matches, 

288 ) 

289 for packager_provided_file in matching_ppfs: 

290 provided_files_for_package = provided_files[ 

291 packager_provided_file.package_name 

292 ] 

293 match_key = ( 

294 packager_provided_file.definition.stem, 

295 packager_provided_file.provided_key, 

296 ) 

297 existing = provided_files_for_package.get(match_key) 

298 if ( 

299 existing is not None 

300 and existing.match_priority > packager_provided_file.match_priority 

301 ): 

302 continue 

303 provided_files_for_package[match_key] = packager_provided_file 

304 

305 result = {} 

306 for package_name, provided_file_data in provided_files.items(): 

307 auto_install_list = [ 

308 x for x in provided_file_data.values() if not x.definition.reservation_only 

309 ] 

310 reservation_only = collections.defaultdict(list) 

311 for packager_provided_file in provided_file_data.values(): 

312 if not packager_provided_file.definition.reservation_only: 312 ↛ 314line 312 didn't jump to line 314, because the condition on line 312 was never false

313 continue 

314 reservation_only[packager_provided_file.definition.stem].append( 

315 packager_provided_file 

316 ) 

317 

318 result[package_name] = PerPackagePackagerProvidedResult( 

319 auto_install_list, 

320 reservation_only, 

321 ) 

322 

323 return result