Coverage for britney2/installability/universe.py: 96%

42 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-06-17 09:00 +0000

1# Copyright (C) 2012 Niels Thykier <niels@thykier.net> 

2 

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. 

7 

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. 

12 

13from collections.abc import Iterator 

14from dataclasses import dataclass 

15from typing import TYPE_CHECKING 

16 

17if TYPE_CHECKING: 17 ↛ 18line 17 didn't jump to line 18 because the condition on line 17 was never true

18 from .. import BinaryPackageId 

19 

20 

21@dataclass(slots=True, frozen=True, eq=False, repr=False) 

22class BinaryPackageRelation: 

23 """All relations of a given binary package""" 

24 

25 pkg_ids: frozenset["BinaryPackageId"] 

26 dependencies: frozenset[frozenset["BinaryPackageId"]] 

27 negative_dependencies: frozenset["BinaryPackageId"] | None 

28 reverse_dependencies: set["BinaryPackageId"] | None 

29 

30 

31class BinaryPackageUniverse: 

32 """A "universe" of all binary packages and their relations 

33 

34 The package universe is a read-only ("immutable") data structure 

35 that knows of all binary packages and their internal relations. 

36 The relations are either in Conjunctive Normal Form (CNF) represented 

37 via sets of sets of the package ids or simply sets of package ids. 

38 

39 Being immutable, the universe does *not* track stateful data such 

40 as "which package is in what suite?" nor "is this package installable 

41 in that suite?". 

42 

43 The universe also includes some packages that are considered "broken". 

44 These packages have been identified to always be uninstallability 

45 regardless of the selection of package available (e.g. the depend 

46 on a non-existent package or has a relation that is impossible to 

47 satisfy). 

48 

49 For these packages, the universe only tracks that they 

50 exist and that they are broken. This implies that their relations 

51 have been nulled into empty sets and they have been removed from 

52 the relations of other packages. This optimizes analysis of the 

53 universe on packages that is/can be installable at the expense 

54 of a "minor" lie about the "broken" packages. 

55 """ 

56 

57 def __init__( 

58 self, 

59 relations: dict["BinaryPackageId", BinaryPackageRelation], 

60 essential_packages: frozenset["BinaryPackageId"], 

61 broken_packages: frozenset["BinaryPackageId"], 

62 equivalent_packages: frozenset["BinaryPackageId"], 

63 ) -> None: 

64 self._relations = relations 

65 self._essential_packages = essential_packages 

66 self._broken_packages = broken_packages 

67 self._equivalent_packages = equivalent_packages 

68 

69 def dependencies_of( 

70 self, pkg_id: "BinaryPackageId" 

71 ) -> frozenset[frozenset["BinaryPackageId"]]: 

72 """Returns the set of dependencies of a given package 

73 

74 :return: A set containing the package ids all of the dependencies 

75 of the input package in CNF. 

76 """ 

77 return self._relations[pkg_id].dependencies 

78 

79 def negative_dependencies_of( 

80 self, pkg_id: "BinaryPackageId" 

81 ) -> frozenset["BinaryPackageId"]: 

82 """Returns the set of negative dependencies of a given package 

83 

84 Note that there is no "reverse_negative_dependencies_of" method, 

85 since negative dependencies have no "direction" unlike positive 

86 dependencies. 

87 

88 :return: A set containing the package ids all of the negative 

89 dependencies of the input package. 

90 """ 

91 return self._relations[pkg_id].negative_dependencies or frozenset() 

92 

93 def reverse_dependencies_of( 

94 self, pkg_id: "BinaryPackageId" 

95 ) -> set["BinaryPackageId"]: 

96 """Returns the set of reverse dependencies of a given package 

97 

98 Note that a package is considered a reverse dependency of the 

99 given package as long as at least one of its dependency relations 

100 *could* be satisfied by the given package. 

101 

102 :return: A set containing the package ids all of the reverse 

103 dependencies of the input package. 

104 """ 

105 return self._relations[pkg_id].reverse_dependencies or set() 

106 

107 def are_equivalent( 

108 self, pkg_id1: "BinaryPackageId", pkg_id2: "BinaryPackageId" 

109 ) -> bool: 

110 """Test if pkg_id1 and pkg_id2 are equivalent 

111 

112 :param pkg_id1: The id of the first package 

113 :param pkg_id2: The id of the second package 

114 :return: True if pkg_id1 and pkg_id2 have the same "signature" in 

115 the package dependency graph (i.e. relations can not tell 

116 them apart semantically except for their name). Otherwise False. 

117 

118 Note that this can return True even if pkg_id1 and pkg_id2 can 

119 tell each other apart. 

120 """ 

121 return pkg_id2 in self.packages_equivalent_to(pkg_id1) 

122 

123 def packages_equivalent_to( 

124 self, pkg_id: "BinaryPackageId" 

125 ) -> frozenset["BinaryPackageId"]: 

126 """Determine which packages are equivalent to a given package 

127 

128 :return: A frozenset of all package ids that are equivalent to the 

129 input package. 

130 """ 

131 return self._relations[pkg_id].pkg_ids 

132 

133 def relations_of(self, pkg_id: "BinaryPackageId") -> BinaryPackageRelation: 

134 """Get the direct relations of a given package 

135 

136 :param pkg_id: The BinaryPackageId of a binary package. 

137 :return: A BinaryPackageRelation describing all known direct 

138 relations for the package. 

139 """ 

140 return self._relations[pkg_id] 

141 

142 @property 

143 def essential_packages(self) -> frozenset["BinaryPackageId"]: 

144 """A frozenset of all "Essential: yes" binaries in the universe 

145 

146 :return: All of the binaries that are marked as essential. 

147 """ 

148 return self._essential_packages 

149 

150 @property 

151 def broken_packages(self) -> frozenset["BinaryPackageId"]: 

152 """A frozenset of all broken binaries in the universe 

153 

154 :return: All binaries that are considered "broken" and had their relations nulled. 

155 """ 

156 return self._broken_packages 

157 

158 @property 

159 def equivalent_packages(self) -> frozenset["BinaryPackageId"]: 

160 """A frozenset of all binary packages that are equivalent to at least one other package 

161 

162 The binary packages in this set has the property that "universe.packages_equivalent_to(pkg_id)" 

163 will return a set of at least 2 or more elements for each of them. 

164 

165 :return: All packages that are equivalent to other packages. 

166 """ 

167 return self._equivalent_packages 

168 

169 def __contains__(self, pkg_id: "BinaryPackageId") -> bool: 

170 return pkg_id in self._relations 

171 

172 def __iter__(self) -> Iterator["BinaryPackageId"]: 

173 yield from self._relations