create a new uniqify_atoms() to properly handle a list of atom instances.
[gentoolkit.git] / pym / gentoolkit / atom.py
1 #!/usr/bin/python
2 #
3 # Copyright(c) 2009, Gentoo Foundation
4 #
5 # Licensed under the GNU General Public License, v2
6 #
7 # $Header$
8
9 """Subclasses portage.dep.Atom to provide methods on a Gentoo atom string."""
10
11 __all__ = ('Atom',)
12
13 # =======
14 # Imports
15 # =======
16
17 import weakref
18
19 import portage
20
21 from gentoolkit.cpv import CPV
22 from gentoolkit.versionmatch import VersionMatch
23 from gentoolkit import errors
24
25 # =======
26 # Classes
27 # =======
28
29 class Atom(portage.dep.Atom, CPV):
30         """Portage's Atom class with improvements from pkgcore.
31
32         portage.dep.Atom provides the following instance variables:
33
34         @type operator: str
35         @ivar operator: one of ('=', '=*', '<', '>', '<=', '>=', '~', None)
36         @type cp: str
37         @ivar cp: cat/pkg
38         @type cpv: str
39         @ivar cpv: cat/pkg-ver (if ver)
40         @type slot: str or None (modified to tuple if not None)
41         @ivar slot: slot passed in as cpv:#
42         """
43
44         # Necessary for Portage versions < 2.1.7
45         _atoms = weakref.WeakValueDictionary()
46
47         def __init__(self, atom):
48                 self.atom = atom
49                 self.operator = self.blocker = self.use = self.slot = None
50
51                 try:
52                         portage.dep.Atom.__init__(self, atom)
53                 except portage.exception.InvalidAtom:
54                         raise errors.GentoolkitInvalidAtom(atom)
55
56                 # Make operator compatible with intersects
57                 if self.operator is None:
58                         self.operator = ''
59
60                 CPV.__init__(self, self.cpv)
61
62                 # use_conditional is USE flag condition for this Atom to be required:
63                 # For: !build? ( >=sys-apps/sed-4.0.5 ), use_conditional = '!build'
64                 self.use_conditional = None
65
66         def __eq__(self, other):
67                 if not isinstance(other, self.__class__):
68                         err = "other isn't of %s type, is %s"
69                         raise TypeError(err % (self.__class__, other.__class__))
70
71                 if self.operator != other.operator:
72                         return False
73
74                 if not CPV.__eq__(self, other):
75                         return False
76
77                 if bool(self.blocker) != bool(other.blocker):
78                         return False
79
80                 if self.blocker and other.blocker:
81                         if self.blocker.overlap.forbid != other.blocker.overlap.forbid:
82                                 return False
83
84                 # Don't believe Portage has something like this
85                 #c = cmp(self.negate_vers, other.negate_vers)
86                 #if c:
87                 #       return c
88
89                 if self.slot != other.slot:
90                         return False
91
92                 this_use = None
93                 if self.use is not None:
94                         this_use = sorted(self.use.tokens)
95                 that_use = None
96                 if other.use is not None:
97                         that_use = sorted(other.use.tokens)
98                 if this_use != that_use:
99                         return False
100
101                 # Not supported by Portage Atom yet
102                 #return cmp(self.repo_name, other.repo_name)
103                 return True
104
105         def __hash__(self):
106                 return hash(self.atom)
107
108         def __ne__(self, other):
109                 return not self == other
110
111         def __lt__(self, other):
112                 if not isinstance(other, self.__class__):
113                         err = "other isn't of %s type, is %s"
114                         raise TypeError(err % (self.__class__, other.__class__))
115
116                 if self.operator != other.operator:
117                         return self.operator < other.operator
118
119                 if not CPV.__eq__(self, other):
120                         return CPV.__lt__(self, other)
121
122                 if bool(self.blocker) != bool(other.blocker):
123                         # We want non blockers, then blockers, so only return True
124                         # if self.blocker is True and other.blocker is False.
125                         return bool(self.blocker) > bool(other.blocker)
126
127                 if self.blocker and other.blocker:
128                         if self.blocker.overlap.forbid != other.blocker.overlap.forbid:
129                                 # we want !! prior to !
130                                 return (self.blocker.overlap.forbid <
131                                         other.blocker.overlap.forbid)
132
133                 # Don't believe Portage has something like this
134                 #c = cmp(self.negate_vers, other.negate_vers)
135                 #if c:
136                 #       return c
137
138                 if self.slot != other.slot:
139                         if self.slot is None:
140                                 return False
141                         elif other.slot is None:
142                                 return True
143                         return self.slot < other.slot
144
145                 this_use = []
146                 if self.use is not None:
147                         this_use = sorted(self.use.tokens)
148                 that_use = []
149                 if other.use is not None:
150                         that_use = sorted(other.use.tokens)
151                 if this_use != that_use:
152                         return this_use < that_use
153
154                 # Not supported by Portage Atom yet
155                 #return cmp(self.repo_name, other.repo_name)
156
157                 return False
158
159         def __gt__(self, other):
160                 if not isinstance(other, self.__class__):
161                         err = "other isn't of %s type, is %s"
162                         raise TypeError(err % (self.__class__, other.__class__))
163
164                 return not self <= other
165
166         def __le__(self, other):
167                 if not isinstance(other, self.__class__):
168                         raise TypeError("other isn't of %s type, is %s" % (
169                                 self.__class__, other.__class__)
170                         )
171                 return self < other or self == other
172
173         def __ge__(self, other):
174                 if not isinstance(other, self.__class__):
175                         raise TypeError("other isn't of %s type, is %s" % (
176                                 self.__class__, other.__class__)
177                         )
178                 return self > other or self == other
179
180         def __repr__(self):
181                 uc = self.use_conditional
182                 uc = "%s? " % uc if uc is not None else ''
183                 return "<%s %r>" % (self.__class__.__name__, "%s%s" % (uc, self.atom))
184
185         def __setattr__(self, name, value):
186                 object.__setattr__(self, name, value)
187
188         #R0911:121:Atom.intersects: Too many return statements (20/6)
189         #R0912:121:Atom.intersects: Too many branches (23/12)
190         # pylint: disable-msg=R0911,R0912
191         def intersects(self, other):
192                 """Check if a passed in package atom "intersects" this atom.
193
194                 Lifted from pkgcore.
195
196                 Two atoms "intersect" if a package can be constructed that
197                 matches both:
198                   - if you query for just "dev-lang/python" it "intersects" both
199                         "dev-lang/python" and ">=dev-lang/python-2.4"
200                   - if you query for "=dev-lang/python-2.4" it "intersects"
201                         ">=dev-lang/python-2.4" and "dev-lang/python" but not
202                         "<dev-lang/python-2.3"
203
204                 @type other: L{gentoolkit.atom.Atom} or
205                         L{gentoolkit.versionmatch.VersionMatch}
206                 @param other: other package to compare
207                 @see: L{pkgcore.ebuild.atom}
208                 """
209                 # Our "cp" (cat/pkg) must match exactly:
210                 if self.cp != other.cp:
211                         # Check to see if one is name only:
212                         # Avoid slow partitioning if we're definitely not matching
213                         # (yes, this is hackish, but it's faster):
214                         if self.cp[-1:] != other.cp[-1:]:
215                                 return False
216
217                         if ((not self.category and self.name == other.name) or
218                                 (not other.category and other.name == self.name)):
219                                 return True
220                         return False
221
222                 # Slot dep only matters if we both have one. If we do they
223                 # must be identical:
224                 this_slot = getattr(self, 'slot', None)
225                 that_slot = getattr(other, 'slot', None)
226                 if (this_slot is not None and that_slot is not None and
227                         this_slot != that_slot):
228                         return False
229
230                 # TODO: Uncomment when Portage's Atom supports repo
231                 #if (self.repo_name is not None and other.repo_name is not None and
232                 #       self.repo_name != other.repo_name):
233                 #       return False
234
235                 # Use deps are similar: if one of us forces a flag on and the
236                 # other forces it off we do not intersect. If only one of us
237                 # cares about a flag it is irrelevant.
238
239                 # Skip the (very common) case of one of us not having use deps:
240                 this_use = getattr(self, 'use', None)
241                 that_use = getattr(other, 'use', None)
242                 if this_use and that_use:
243                         # Set of flags we do not have in common:
244                         flags = set(this_use.tokens) ^ set(that_use.tokens)
245                         for flag in flags:
246                                 # If this is unset and we also have the set version we fail:
247                                 if flag[0] == '-' and flag[1:] in flags:
248                                         return False
249
250         # Remaining thing to check is version restrictions. Get the
251         # ones we can check without actual version comparisons out of
252         # the way first.
253
254                 # If one of us is unversioned we intersect:
255                 if not self.operator or not other.operator:
256                         return True
257
258                 # If we are both "unbounded" in the same direction we intersect:
259                 if (('<' in self.operator and '<' in other.operator) or
260                         ('>' in self.operator and '>' in other.operator)):
261                         return True
262
263                 # If one of us is an exact match we intersect if the other matches it:
264                 if self.operator == '=':
265                         if other.operator == '=*':
266                                 return self.fullversion.startswith(other.fullversion)
267                         return VersionMatch(other, op=other.operator).match(self)
268                 if other.operator == '=':
269                         if self.operator == '=*':
270                                 return other.fullversion.startswith(self.fullversion)
271                         return VersionMatch(self, op=self.operator).match(other)
272
273                 # If we are both ~ matches we match if we are identical:
274                 if self.operator == other.operator == '~':
275                         return (self.version == other.version and
276                                 self.revision == other.revision)
277
278                 # If we are both glob matches we match if one of us matches the other.
279                 if self.operator == other.operator == '=*':
280                         return (self.fullversion.startswith(other.fullversion) or
281                                 other.fullversion.startswith(self.fullversion))
282
283                 # If one of us is a glob match and the other a ~ we match if the glob
284                 # matches the ~ (ignoring a revision on the glob):
285                 if self.operator == '=*' and other.operator == '~':
286                         return other.fullversion.startswith(self.version)
287                 if other.operator == '=*' and self.operator == '~':
288                         return self.fullversion.startswith(other.version)
289
290                 # If we get here at least one of us is a <, <=, > or >=:
291                 if self.operator in ('<', '<=', '>', '>='):
292                         # pylint screwup:
293                         # E0601: Using variable 'ranged' before assignment
294                         # pylint: disable-msg=E0601
295                         ranged, ranged.operator = self, self.operator
296                 else:
297                         ranged, ranged.operator = other, other.operator
298                         other, other.operator = self, self.operator
299
300                 if '<' in other.operator or '>' in other.operator:
301                         # We are both ranged, and in the opposite "direction" (or
302                         # we would have matched above). We intersect if we both
303                         # match the other's endpoint (just checking one endpoint
304                         # is not enough, it would give a false positive on <=2 vs >2)
305                         return (
306                                 VersionMatch(other, op=other.operator).match(ranged) and
307                                 VersionMatch(ranged, op=ranged.operator).match(other)
308                         )
309
310                 if other.operator == '~':
311                         # Other definitely matches its own version. If ranged also
312                         # does we're done:
313                         if VersionMatch(ranged, op=ranged.operator).match(other):
314                                 return True
315                         # The only other case where we intersect is if ranged is a
316                         # > or >= on other's version and a nonzero revision. In
317                         # that case other will match ranged. Be careful not to
318                         # give a false positive for ~2 vs <2 here:
319                         return (ranged.operator in ('>', '>=') and
320                                 VersionMatch(other, op=other.operator).match(ranged))
321
322                 if other.operator == '=*':
323                         # a glob match definitely matches its own version, so if
324                         # ranged does too we're done:
325                         if VersionMatch(ranged, op=ranged.operator).match(other):
326                                 return True
327                         if '<' in ranged.operator:
328                                 # If other.revision is not defined then other does not
329                                 # match anything smaller than its own fullversion:
330                                 if other.revision:
331                                         return False
332
333                                 # If other.revision is defined then we can always
334                                 # construct a package smaller than other.fullversion by
335                                 # tagging e.g. an _alpha1 on.
336                                 return ranged.fullversion.startswith(other.version)
337                         else:
338                                 # Remaining cases where this intersects: there is a
339                                 # package greater than ranged.fullversion and
340                                 # other.fullversion that they both match.
341                                 return ranged.fullversion.startswith(other.version)
342
343                 # Handled all possible ops.
344                 raise NotImplementedError(
345                         'Someone added an operator without adding it to intersects')
346
347         def get_depstr(self):
348                 """Returns a string representation of the original dep
349                 """
350                 uc = self.use_conditional
351                 uc = "%s? " % uc if uc is not None else ''
352                 return "%s%s" % (uc, self.atom)
353
354 # vim: set ts=4 sw=4 tw=79: