Thanks for the comments. Version 2, updates: * Use "import sys" instead of "from sys import hexversion" * Use super() to call functions from parent class. * Fix initialization, used to pass m.groups() to parent object which is a leftover from older versions where I used to subclass list instead of str. Removed __str__ functions and __str which are useless as well. * Added readonly attributes cvs, main_version and revision to version, pv and cpv, package to pv, cpv and category to cpv. I think this is a better way than making __parts available so it's guaranteed that there'll be no unwanted change in internal state. * version uses __init__ now. pv and cpv still use __new__ because to fit code easily they return None when the argument isn't a valid PV or CPV instead of raising TypeError.
Please comment. --- pym/portage/versions.py | 332 +++++++++++++++++++++++++++++++++++++++++----- 1 files changed, 296 insertions(+), 36 deletions(-) diff --git a/pym/portage/versions.py b/pym/portage/versions.py index 261fa9d..507d7a1 100644 --- a/pym/portage/versions.py +++ b/pym/portage/versions.py @@ -4,6 +4,7 @@ # $Id$ import re +import warnings ver_regexp = re.compile("^(cvs\\.)?(\\d+)((\\.\\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\\d*)*)(-r(\\d+))?$") suffix_regexp = re.compile("^(alpha|beta|rc|pre|p)(\\d*)$") @@ -12,6 +13,245 @@ endversion_keys = ["pre", "p", "alpha", "beta", "rc"] from portage.exception import InvalidData +# builtin all() is new in Python-2.5 +# TODO Move compatibility stuff to a new module portage.compat +# and import from it like from portage.compat import all +import sys +if sys.hexversion < 0x02050000: + def all(iterable): + for i in iterable: + if not bool(i): + return False + return True + +def needs_version(func): + """Decorator for functions that require non-keyword arguments of type version.""" + def func_proxy(*args, **kwargs): + if not all([isinstance(arg, version) for arg in args]): + raise TypeError("Not all non-keyword arguments are of type version") + return func(*args, **kwargs) + func_proxy.__doc__ = func.__doc__ + return func_proxy + +def needs_pv(func): + """Decorator for functions that require non-keyword arguments of type pv.""" + def func_proxy(*args, **kwargs): + if not all([isinstance(arg, pv) for arg in args]): + raise TypeError("Not all non-keyword arguments are of type pv") + return func(*args, **kwargs) + func_proxy.__doc__ = func.__doc__ + return func_proxy + +def needs_cpv(func): + """Decorator for functions that require non-keyword arguments of type cpv.""" + def func_proxy(*args, **kwargs): + if not all([isinstance(arg, cpv) for arg in args]): + raise TypeError("Not all non-keyword arguments are of type cpv") + return func(*args, **kwargs) + func_proxy.__doc__ = func.__doc__ + return func_proxy + +class version(str): + """Represents a package version""" + + __hash = None + __parts = () + + def __init__(self, value): + m = ver_regexp.match(value) + if m is None: + raise TypeError("Syntax error in version: %s" % value) + else: + super(version, self).__init__(value) + self.__hash = hash(m.groups()) + hash(value) + self.__parts = m.groups() + + def __repr__(self): + return "<%s object at 0x%x: %s>" % (self.__class__.__name__, + id(self), self) + + def __hash__(self): + return self.__hash + + def __getitem__(self, i): + return self.__parts[i] + + def __getslice__(self, i, j): + return self.__parts[i:j] + + def __len__(self): + return len(self.__parts) + + @needs_version + def __cmp__(self, y): + return vercmp(self, y) + + @needs_version + def __eq__(self, y): + return vercmp(self, y) == 0 + + @needs_version + def __ne__(self, y): + return vercmp(self, y) != 0 + + @needs_version + def __lt__(self, y): + return vercmp(self, y) < 0 + + @needs_version + def __le__(self, y): + return vercmp(self, y) <= 0 + + @needs_version + def __gt__(self, y): + return vercmp(self, y) > 0 + + @needs_version + def __ge__(self, y): + return vercmp(self, y) >= 0 + + @property + def cvs(self): + return self.__parts[0] + + @property + def main_version(self): + mv = self.__parts[1:3] + mv += self.__parts[4:6] + return "".join(mv) + + @property + def revision(self): + return self.__parts[8] + +class pv(str): + """Represents a pv""" + + __hash = None + __parts = () + + def __new__(cls, value): + parts = pkgsplit(value) + if parts is None: + # Ideally a TypeError should be raised here. + # But to fit code using this easily, fail silently. + return None + else: + new_pv = super(pv, cls).__new__(cls, value) + new_pv.__hash = hash(parts) + hash(value) + new_pv.__parts = (parts[0], version("-".join(parts[1:]))) + + return new_pv + + def __repr__(self): + return "<%s object at 0x%x: %s>" % (self.__class__.__name__, + id(self), self) + + def __hash__(self): + return self.__hash + + def __getitem__(self, i): + return self.__parts[i] + + def __getslice__(self, i, j): + return self.__parts[i:j] + + def __len__(self): + return len(self.__parts) + + @needs_pv + def __cmp__(self, y): + if self.__parts[0] != y.__parts[0]: + return None + else: + return cmp(self[1], y[1]) + + @property + def package(self): + return self.__parts[0] + + @property + def version(self): + return self.__parts[1] + + @property + def cvs(self): + return self.__parts[1].cvs + + @property + def main_version(self): + return self.__parts[1].main_version + + @property + def revision(self): + return self.__parts[1].revision + +class cpv(str): + """Represents a cpv""" + + __hash = None + __parts = () + + def __new__(cls, value): + parts = catpkgsplit(value) + if parts is None: + # Ideally a TypeError should be raised here. + # But to fit code using this easily, fail silently. + return None + else: + new_cpv = super(cpv, cls).__new__(cls, value) + new_cpv.__hash = hash(parts) + hash(value) + new_cpv.__parts = (parts[0], pv("-".join(parts[1:]))) + + return new_cpv + + def __repr__(self): + return "<%s object at 0x%x: %s>" % (self.__class__.__name__, + id(self), self) + + def __hash__(self): + return self.__hash + + def __getitem__(self, i): + return self.__parts[i] + + def __getslice__(self, i, j): + return self.__parts[i:j] + + def __len__(self): + return len(self.__parts) + + @needs_cpv + def __cmp__(self, y): + if self[0] != y[0]: + return None + else: + return cmp(self[1], y[1]) + + @property + def category(self): + return self.__parts[0] + + @property + def package(self): + return self.__parts[1] + + @property + def version(self): + return self.__parts[1].version + + @property + def cvs(self): + return self.__parts[1].cvs + + @property + def main_version(self): + return self.__parts[1].main_version + + @property + def revision(self): + return self.__parts[1].revision + def ververify(myver, silent=1): if ver_regexp.match(myver): return 1 @@ -45,43 +285,63 @@ def vercmp(ver1, ver2, silent=1): 4. None if ver1 or ver2 are invalid (see ver_regexp in portage.versions.py) """ - if ver1 == ver2: - return 0 - mykey=ver1+":"+ver2 - try: - return vercmp_cache[mykey] - except KeyError: - pass - match1 = ver_regexp.match(ver1) - match2 = ver_regexp.match(ver2) - - # checking that the versions are valid - if not match1 or not match1.groups(): - if not silent: - print "!!! syntax error in version: %s" % ver1 - return None - if not match2 or not match2.groups(): - if not silent: - print "!!! syntax error in version: %s" % ver2 - return None + if isinstance(ver1, version) and isinstance(ver2, version): + if ver1._str == ver2._str: + return 0 + mykey = ver1._str+":"+ver2._str + if mykey in vercmp_cache: + return vercmp_cache[mykey] + + group1 = ver1[:] + group2 = ver2[:] + elif isinstance(ver1, str) and isinstance(ver2, str): + ## Backwards compatibility + msg = "vercmp(str,str) is deprecated use portage.version object instead" + warnings.warn(msg, DeprecationWarning) + + if ver1 == ver2: + return 0 + mykey=ver1+":"+ver2 + try: + return vercmp_cache[mykey] + except KeyError: + pass + match1 = ver_regexp.match(ver1) + match2 = ver_regexp.match(ver2) + + # checking that the versions are valid + if not match1 or not match1.groups(): + if not silent: + print "!!! syntax error in version: %s" % ver1 + return None + if not match2 or not match2.groups(): + if not silent: + print "!!! syntax error in version: %s" % ver2 + return None + + group1 = match1.groups() + group2 = match2.groups() + else: + raise TypeError( + "Arguments aren't of type str,str or version,version") # shortcut for cvs ebuilds (new style) - if match1.group(1) and not match2.group(1): + if group1[0] and not group2[0]: vercmp_cache[mykey] = 1 return 1 - elif match2.group(1) and not match1.group(1): + elif group2[0] and not group1[0]: vercmp_cache[mykey] = -1 return -1 # building lists of the version parts before the suffix # first part is simple - list1 = [int(match1.group(2))] - list2 = [int(match2.group(2))] + list1 = [int(group1[1])] + list2 = [int(group2[1])] # this part would greatly benefit from a fixed-length version pattern - if len(match1.group(3)) or len(match2.group(3)): - vlist1 = match1.group(3)[1:].split(".") - vlist2 = match2.group(3)[1:].split(".") + if len(group1[2]) or len(group2[2]): + vlist1 = group1[2][1:].split(".") + vlist2 = group2[2][1:].split(".") for i in range(0, max(len(vlist1), len(vlist2))): # Implcit .0 is given a value of -1, so that 1.0.0 > 1.0, since it # would be ambiguous if two versions that aren't literally equal @@ -111,10 +371,10 @@ def vercmp(ver1, ver2, silent=1): list2.append(int(vlist2[i].ljust(max_len, "0"))) # and now the final letter - if len(match1.group(5)): - list1.append(ord(match1.group(5))) - if len(match2.group(5)): - list2.append(ord(match2.group(5))) + if len(group1[4]): + list1.append(ord(group1[4])) + if len(group2[4]): + list2.append(ord(group2[4])) for i in range(0, max(len(list1), len(list2))): if len(list1) <= i: @@ -128,8 +388,8 @@ def vercmp(ver1, ver2, silent=1): return list1[i] - list2[i] # main version is equal, so now compare the _suffix part - list1 = match1.group(6).split("_")[1:] - list2 = match2.group(6).split("_")[1:] + list1 = group1[5].split("_")[1:] + list2 = group2[5].split("_")[1:] for i in range(0, max(len(list1), len(list2))): # Implicit _p0 is given a value of -1, so that 1 < 1_p0 @@ -154,12 +414,12 @@ def vercmp(ver1, ver2, silent=1): return r1 - r2 # the suffix part is equal to, so finally check the revision - if match1.group(10): - r1 = int(match1.group(10)) + if group1[9]: + r1 = int(group1[9]) else: r1 = 0 - if match2.group(10): - r2 = int(match2.group(10)) + if group2[9]: + r2 = int(group2[9]) else: r2 = 0 vercmp_cache[mykey] = r1 - r2 -- Regards, Ali Polatel