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
« 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
5from debputy.packages import BinaryPackage
6from debputy.plugin.api import VirtualPath
7from debputy.plugin.api.impl_types import PackagerProvidedFileClassSpec
8from debputy.util import _error
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
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 )
29@dataclasses.dataclass(frozen=True, slots=True)
30class PerPackagePackagerProvidedResult:
31 auto_installable: List[PackagerProvidedFile]
32 reserved_only: Dict[str, List[PackagerProvidedFile]]
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
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
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
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
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 )
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
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
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
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
206 install_as_name, definition = _find_definition(
207 packager_provided_files, basename
208 )
209 if definition is None:
210 continue
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
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
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)
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
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 )
318 result[package_name] = PerPackagePackagerProvidedResult(
319 auto_install_list,
320 reservation_only,
321 )
323 return result