cda68d75c65f5b843c8b3b2e95f294a83ed726bc
[gentoolkit.git] / pym / gentoolkit / cpv.py
1 # Copyright(c) 2005 Jason Stubbs <jstubbs@gentoo.org>                         
2 # Copyright(c) 2005-2006 Brian Harring <ferringb@gmail.com>
3 # Copyright(c) 2009-2010 Gentoo Foundation
4 #
5 # Licensed under the GNU General Public License, v2
6 #
7 # $Header$
8
9 """Provides attributes and methods for a category/package-version string."""
10
11 __all__ = (
12         'CPV',
13         'compare_strs',
14         'split_cpv'
15 )
16
17 # =======
18 # Imports
19 # =======
20
21 import re
22
23 from portage.versions import catpkgsplit, vercmp, pkgcmp
24
25 from gentoolkit import errors
26
27 # =======
28 # Globals
29 # =======
30
31 isvalid_version_re = re.compile("^(?:cvs\\.)?(?:\\d+)(?:\\.\\d+)*[a-z]?"
32         "(?:_(p(?:re)?|beta|alpha|rc)\\d*)*$")
33 isvalid_cat_re = re.compile("^(?:[a-zA-Z0-9][-a-zA-Z0-9+._]*(?:/(?!$))?)+$")
34 _pkg_re = re.compile("^[a-zA-Z0-9+._]+$")
35 # Prefix specific revision is of the form -r0<digit>+.<digit>+
36 isvalid_rev_re = re.compile(r'(\d+|0\d+\.\d+)')
37
38 # =======
39 # Classes
40 # =======
41
42 class CPV(object):
43         """Provides methods on a category/package-version string.
44
45         Will also correctly split just a package or package-version string.
46
47         Example usage:
48                 >>> from gentoolkit.cpv import CPV
49                 >>> cpv = CPV('sys-apps/portage-2.2-r1')
50                 >>> cpv.category, cpv.name, cpv.fullversion
51                 ('sys-apps', 'portage', '2.2-r1')
52                 >>> str(cpv)
53                 'sys-apps/portage-2.2-r1'
54                 >>> # An 'rc' (release candidate) version is less than non 'rc' version:
55                 ... CPV('sys-apps/portage-2') > CPV('sys-apps/portage-2_rc10')
56                 True
57         """
58
59         def __init__(self, cpv, validate=False):
60                 self.cpv = cpv
61                 self._category = None
62                 self._name = None
63                 self._version = None
64                 self._revision = None
65                 self._cp = None
66                 self._fullversion = None
67
68                 self.validate = validate
69                 if validate and not self.name:
70                         raise errors.GentoolkitInvalidCPV(cpv)
71
72         @property
73         def category(self):
74                 if self._category is None:
75                         self._set_cpv_chunks()
76                 return self._category
77
78         @property
79         def name(self):
80                 if self._name is None:
81                         self._set_cpv_chunks()
82                 return self._name
83
84         @property
85         def version(self):
86                 if self._version is None:
87                         self._set_cpv_chunks()
88                 return self._version
89
90         @property
91         def revision(self):
92                 if self._revision is None:
93                         self._set_cpv_chunks()
94                 return self._revision
95
96         @property
97         def cp(self):
98                 if self._cp is None:
99                         sep = '/' if self.category else ''
100                         self._cp = sep.join((self.category, self.name))
101                 return self._cp
102
103         @property
104         def fullversion(self):
105                 if self._fullversion is None:
106                         sep = '-' if self.revision else ''
107                         self._fullversion = sep.join((self.version, self.revision))
108                 return self._fullversion
109
110         def _set_cpv_chunks(self):
111                 chunks = split_cpv(self.cpv, validate=self.validate)
112                 self._category = chunks[0]
113                 self._name = chunks[1]
114                 self._version = chunks[2]
115                 self._revision = chunks[3]
116
117         def __eq__(self, other):
118                 if not isinstance(other, self.__class__):
119                         return False
120                 return self.cpv == other.cpv
121
122         def __hash__(self):
123                 return hash(self.cpv)
124
125         def __ne__(self, other):
126                 return not self == other
127
128         def __lt__(self, other):
129                 if not isinstance(other, self.__class__):
130                         raise TypeError("other isn't of %s type, is %s" % (
131                                 self.__class__, other.__class__)
132                         )
133
134                 if self.category != other.category:
135                         return self.category < other.category
136                 elif self.name != other.name:
137                         return self.name < other.name
138                 else:
139                         # FIXME: this cmp() hack is for vercmp not using -1,0,1
140                         # See bug 266493; this was fixed in portage-2.2_rc31
141                         #return vercmp(self.fullversion, other.fullversion)
142                         return vercmp(self.fullversion, other.fullversion) < 0
143
144         def __gt__(self, other):
145                 if not isinstance(other, self.__class__):
146                         raise TypeError("other isn't of %s type, is %s" % (
147                                 self.__class__, other.__class__)
148                         )
149                 return not self <= other
150
151         def __le__(self, other):
152                 if not isinstance(other, self.__class__):
153                         raise TypeError("other isn't of %s type, is %s" % (
154                                 self.__class__, other.__class__)
155                         )
156                 return self < other or self == other
157
158         def __ge__(self, other):
159                 if not isinstance(other, self.__class__):
160                         raise TypeError("other isn't of %s type, is %s" % (
161                                 self.__class__, other.__class__)
162                         )
163                 return self > other or self == other
164
165         def __repr__(self):
166                 return "<%s %r>" % (self.__class__.__name__, str(self))
167
168         def __str__(self):
169                 return self.cpv
170
171
172 # =========
173 # Functions
174 # =========
175
176 def compare_strs(pkg1, pkg2):
177         """Similar to the builtin cmp, but for package strings. Usually called
178         as: package_list.sort(cpv.compare_strs)
179
180         An alternative is to use the CPV descriptor from gentoolkit.cpv:
181         >>> cpvs = sorted(CPV(x) for x in package_list)
182
183         @see: >>> help(cmp)
184         """
185
186         pkg1 = catpkgsplit(pkg1)
187         pkg2 = catpkgsplit(pkg2)
188         if pkg1[0] != pkg2[0]:
189                 return -1 if pkg1[0] < pkg2[0] else 1
190         elif pkg1[1] != pkg2[1]:
191                 return -1 if pkg1[1] < pkg2[1] else 1
192         else:
193                 return pkgcmp(pkg1[1:], pkg2[1:])
194
195
196 def split_cpv(cpv, validate=True):
197         """Split a cpv into category, name, version and revision.
198
199         Modified from pkgcore.ebuild.cpv
200
201         @type cpv: str
202         @param cpv: pkg, cat/pkg, pkg-ver, cat/pkg-ver
203         @rtype: tuple
204         @return: (category, pkg_name, version, revision)
205                 Each tuple element is a string or empty string ("").
206         """
207
208         category = name = version = revision = ''
209
210         try:
211                 category, pkgver = cpv.rsplit("/", 1)
212         except ValueError:
213                 pkgver = cpv
214         if validate and category and not isvalid_cat_re.match(category):
215                 raise errors.GentoolkitInvalidCPV(cpv)
216         pkg_chunks = pkgver.split("-")
217         lpkg_chunks = len(pkg_chunks)
218         if lpkg_chunks == 1:
219                 return (category, pkg_chunks[0], version, revision)
220         if isvalid_rev(pkg_chunks[-1]):
221                 if lpkg_chunks < 3:
222                         # needs at least ('pkg', 'ver', 'rev')
223                         raise errors.GentoolkitInvalidCPV(cpv)
224                 rev = pkg_chunks.pop(-1)
225                 if rev:
226                         revision = rev
227
228         if isvalid_version_re.match(pkg_chunks[-1]):
229                 version = pkg_chunks.pop(-1)
230
231         if not isvalid_pkg_name(pkg_chunks):
232                 raise errors.GentoolkitInvalidCPV(cpv)
233         name = '-'.join(pkg_chunks)
234
235         return (category, name, version, revision)
236
237
238 def isvalid_pkg_name(chunks):
239         if not chunks[0]:
240                 # this means a leading -
241                 return False
242         mf = _pkg_re.match
243         if not all(not s or mf(s) for s in chunks):
244                 return False
245         if chunks[-1].isdigit() or not chunks[-1]:
246                 # not allowed.
247                 return False
248         return True
249
250
251 def isvalid_rev(s):
252         return s and s[0] == 'r' and isvalid_rev_re.match(s[1:])
253
254 # vim: set ts=4 sw=4 tw=79: