Coverage for britney2/migrationitem.py: 97%
139 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-10-17 17:32 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-10-17 17:32 +0000
1# Copyright (C) 2011 Adam D. Barratt <adsb@debian.org>
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
13import logging
14from typing import Any, Optional
16import apt_pkg
18from britney2 import BinaryPackageId, Suite, SuiteClass, Suites
21class MigrationItem:
22 def __init__(
23 self,
24 package: str,
25 suite: Suite,
26 *,
27 version: str | None = None,
28 architecture: str | None = None,
29 is_removal: bool = False,
30 is_cruft_removal: bool = False,
31 ):
32 if architecture is None:
33 architecture = "source"
35 if is_cruft_removal:
36 is_removal = True
38 self._package = package
39 self._version = version
40 self._architecture = architecture
41 self._suite = suite
42 self._is_removal = is_removal
43 self._is_cruft_removal = is_cruft_removal
44 self._uvname = self.get_uvname()
45 self._name = self.get_name()
47 def get_name(self) -> str:
48 name = self._package
49 if self._architecture != "source":
50 name = f"{name}/{self._architecture}"
51 if self._version:
52 name = f"{name}/{self._version}"
53 if self._suite.excuses_suffix:
54 name = f"{name}_{self._suite.excuses_suffix}"
55 if self._is_removal:
56 name = "-%s" % (name)
57 return name
59 def get_uvname(self) -> str:
60 name = self._package
61 if self._architecture != "source":
62 name = f"{name}/{self._architecture}"
63 if self._suite.excuses_suffix:
64 name = f"{name}_{self._suite.excuses_suffix}"
65 if self._is_removal:
66 name = "-%s" % (name)
67 return name
69 def __repr__(self) -> str:
70 return "MI(%s)" % (self.__str__())
72 def __str__(self) -> str:
73 if self.version is not None:
74 return self.name
75 else:
76 return self.uvname
78 def __eq__(self, other: object) -> bool:
79 isequal = False
80 if isinstance(other, MigrationItem): 80 ↛ 87line 80 didn't jump to line 87 because the condition on line 80 was always true
81 if self.uvname == other.uvname:
82 if self.version is None or other.version is None:
83 isequal = True
84 else:
85 isequal = self.version == other.version
87 return isequal
89 def __hash__(self) -> int:
90 if not self.version: 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true
91 raise AssertionError(
92 "trying to hash unversioned MigrationItem: %s" % (self.name)
93 )
95 return hash((self.uvname, self.version))
97 def __lt__(self, other: "MigrationItem") -> bool:
98 return (self.uvname, self.version) < (other.uvname, other.version)
100 @property
101 def name(self) -> str:
102 return self._name
104 @property
105 def is_removal(self) -> bool:
106 return self._is_removal
108 @property
109 def architecture(self) -> str:
110 return self._architecture
112 @property
113 def package(self) -> str:
114 return self._package
116 @property
117 def suite(self) -> Suite:
118 return self._suite
120 @property
121 def version(self) -> str | None:
122 return self._version
124 @property
125 def uvname(self) -> str:
126 return self._uvname
128 @property
129 def is_cruft_removal(self) -> bool:
130 return self._is_cruft_removal
133class MigrationItemFactory:
134 def __init__(self, suites: Suites) -> None:
135 self._suites = suites
136 self._all_architectures = frozenset(suites.target_suite.binaries)
137 logger_name = ".".join((self.__class__.__module__, self.__class__.__name__))
138 self.logger = logging.getLogger(logger_name)
140 def generate_removal_for_cruft_item(
141 self, pkg_id: "BinaryPackageId"
142 ) -> MigrationItem:
143 return MigrationItem(
144 package=pkg_id.package_name,
145 version=pkg_id.version,
146 architecture=pkg_id.architecture,
147 suite=self._suites.target_suite,
148 is_cruft_removal=True,
149 )
151 @staticmethod
152 def _is_right_version(
153 suite: Suite, package_name: str, expected_version: str
154 ) -> bool:
155 if package_name not in suite.sources:
156 return False
158 actual_version = suite.sources[package_name].version
159 if apt_pkg.version_compare(actual_version, expected_version) != 0:
160 return False
162 return True
164 def _find_suite_for_item(
165 self,
166 suites: Suites,
167 suite_name: str,
168 package_name: str,
169 version: str | None,
170 auto_correct: bool,
171 ) -> Suite:
172 suite = suites.by_name_or_alias[suite_name]
173 assert suite.suite_class != SuiteClass.TARGET_SUITE
174 if (
175 version is not None
176 and auto_correct
177 and not self._is_right_version(suite, package_name, version)
178 ):
179 for s in suites.source_suites:
180 if self._is_right_version(s, package_name, version):
181 suite = s
182 break
183 return suite
185 def parse_item(
186 self, item_text: str, versioned: bool = True, auto_correct: bool = True
187 ) -> MigrationItem:
188 """
190 :param item_text: The string describing the item (e.g. "glibc/2.5")
191 :param versioned: If true, a two-part item is assumed to be versioned.
192 otherwise, it is assumed to be versionless. This determines how
193 items like "foo/bar" is parsed (if versioned, "bar" is assumed to
194 be a version and otherwise "bar" is assumed to be an architecture).
195 If in doubt, use versioned=True with auto_correct=True and the
196 code will figure it out on its own.
197 :param auto_correct: If True, minor issues are automatically fixed
198 where possible. This includes handling architecture and version
199 being in the wrong order and missing/omitting a suite reference
200 for items. This feature is useful for migration items provided
201 by humans (e.g. via hints) to avoid rejecting the input over
202 trivial/minor issues with the input.
203 When False, there will be no attempt to correct the migration
204 input.
205 :return: A MigrationItem matching the spec
206 """
207 suites = self._suites
208 version = None
209 architecture = None
210 is_removal = False
211 if item_text.startswith("-"):
212 item_text = item_text[1:]
213 is_removal = True
214 parts = item_text.split("/", 3)
215 package_name = parts[0]
216 suite_name = suites.primary_source_suite.name
217 if "_" in package_name:
218 package_name, suite_name = package_name.split("_", 2)
220 if len(parts) == 3:
221 architecture = parts[1]
222 version = parts[2]
223 elif len(parts) == 2:
224 if versioned: 224 ↛ 227line 224 didn't jump to line 227 because the condition on line 224 was always true
225 version = parts[1]
226 else:
227 architecture = parts[1]
229 if auto_correct and version in self._all_architectures:
230 (architecture, version) = (version, architecture)
232 if architecture is None:
233 architecture = "source"
235 if "_" in architecture:
236 architecture, suite_name = architecture.split("_", 2)
238 if is_removal:
239 suite: Suite = suites.target_suite
240 else:
241 suite = self._find_suite_for_item(
242 suites, suite_name, package_name, version, auto_correct
243 )
245 return MigrationItem(
246 package=package_name,
247 version=version,
248 architecture=architecture,
249 suite=suite,
250 is_removal=is_removal,
251 )
253 def parse_items(self, *args: Any, **kwargs: Any) -> list[MigrationItem]:
254 return [self.parse_item(x, **kwargs) for x in args]