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