[PATCH] debian_support: Add a native Python Version class

John Wright jsw at debian.org
Sun Mar 14 04:48:08 UTC 2010


Based on the DpkgVersion class by Raphael Hertzog in
svn://svn.debian.org/qa/trunk/pts/www/bin/common.py r2361

This commit introduces a BaseVersion class, which does the work of
validating version strings against Debian Policy section 5.6.12 (which
was never previously done), splitting the version into its components,
and implementing __repr__, __str__, and __hash__.  Subclasses need only
implement __cmp__, and they have access to the pre-split components.

Closes: #562257, #573009
---
 debian_bundle/debian_support.py |  200 ++++++++++++++++++++++++++++++++-------
 1 files changed, 167 insertions(+), 33 deletions(-)

diff --git a/debian_bundle/debian_support.py b/debian_bundle/debian_support.py
index 6543206..48d2eee 100644
--- a/debian_bundle/debian_support.py
+++ b/debian_bundle/debian_support.py
@@ -1,5 +1,6 @@
 # debian_support.py -- Python module for Debian metadata
 # Copyright (C) 2005 Florian Weimer <fw at deneb.enyo.de>
+# Copyright (C) 2010 John Wright <jsw at debian.org>
 # 
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -23,8 +24,13 @@ import hashlib
 import types
 
 from deprecation import function_deprecated_by
-import apt_pkg
-apt_pkg.init()
+
+try:
+    import apt_pkg
+    apt_pkg.init()
+    __have_apt_pkg = True
+except ImportError:
+    __have_apt_pkg = False
 
 class ParseError(Exception):
     """An exception which is used to signal a parse failure.
@@ -58,33 +64,152 @@ class ParseError(Exception):
 
     printOut = function_deprecated_by(print_out)
 
-class Version:
-    """Version class which uses the original APT comparison algorithm."""
+class BaseVersion(object):
+    """Base class for classes representing Debian versions
+
+    It doesn't implement any comparison, but it does check for valid versions
+    according to Section 5.6.12 in the Debian Policy Manual.  Since splitting
+    the version into epoch, upstream_version, and debian_revision components is
+    pretty much free with the validation, it sets those fields as properties of
+    the object.  A missing epoch or debian_revision results in the respective
+    property set to "0".  These properties are immutable.
+
+    It also implements __str__, just returning the raw version given to the
+    initializer.
+    """
+
+    re_valid_version = re.compile(
+            r"^((?P<epoch>\d+):)?"
+             "(?P<upstream_version>[A-Za-z0-9.+:~-]+?)"
+             "(-(?P<debian_revision>[A-Za-z0-9+.~]+))?$")
 
     def __init__(self, version):
-        """Creates a new Version object."""
-        t = type(version)
-        if t == types.UnicodeType:
-            version = version.encode('UTF-8')
-        else:
-            assert t == types.StringType, `version`
-        assert version <> ""
-        self.__asString = version
+        if not isinstance(version, (str, unicode)):
+            raise ValueError, "version must be a string or unicode object"
+
+        m = self.re_valid_version.match(version)
+        if not m:
+            raise ValueError("Invalid version string %r" % version)
+
+        self.__raw_version = version
+        self.__epoch = m.group("epoch")
+        self.__upstream_version = m.group("upstream_version")
+        self.__debian_revision = m.group("debian_revision")
+
+    epoch = property(lambda self: self.__epoch or "0")
+    upstream_version = property(lambda self: self.__upstream_version)
+    debian_revision = property(lambda self: self.__debian_revision or "0")
 
     def __str__(self):
-        return self.__asString
+        return self.__raw_version
 
     def __repr__(self):
-        return 'Version(%s)' % `self.__asString`
+        return "%s('%s')" % (self.__class__.__name__, self)
 
     def __cmp__(self, other):
-        return apt_pkg.VersionCompare(str(self), str(other))
+        raise NotImplementedError
 
     def __hash__(self):
         return hash(str(self))
 
+class AptPkgVersion(BaseVersion):
+    """Represents a Debian package version, using apt_pkg.VersionCompare"""
+
+    def __cmp__(self, other):
+        return apt_pkg.VersionCompare(str(self), str(other))
+
+# NativeVersion based on the DpkgVersion class by Raphael Hertzog in
+# svn://svn.debian.org/qa/trunk/pts/www/bin/common.py r2361
+class NativeVersion(BaseVersion):
+    """Represents a Debian package version, with native Python comparison"""
+
+    re_all_digits_or_not = re.compile("\d+|\D+")
+    re_digits = re.compile("\d+")
+    re_digit = re.compile("\d")
+    re_alpha = re.compile("[A-Za-z]")
 
-version_compare = apt_pkg.VersionCompare
+    def __cmp__(self, other):
+        # Convert other into an instance of BaseVersion if it's not already.
+        # (All we need is epoch, upstream_version, and debian_revision
+        # attributes, which BaseVersion gives us.) Requires other's string
+        # representation to be the raw version.
+        if not isinstance(other, BaseVersion):
+            try:
+                other = BaseVersion(str(other))
+            except ValueError, e:
+                raise ValueError("Couldn't convert %r to BaseVersion: %s"
+                                 % (other, e))
+
+        res = cmp(int(self.epoch), int(other.epoch))
+        if res != 0:
+            return res
+        res = self._version_cmp_part(self.upstream_version,
+                                     other.upstream_version)
+        if res != 0:
+            return res
+        return self._version_cmp_part(self.debian_revision,
+                                      other.debian_revision)
+
+    @classmethod
+    def _order(cls, x):
+        """Return an integer value for character x"""
+        if x == '~':
+            return -1
+        elif cls.re_digit.match(x):
+            return int(x) + 1
+        elif cls.re_alpha.match(x):
+            return ord(x)
+        else:
+            return ord(x) + 256
+
+    @classmethod
+    def _version_cmp_string(cls, va, vb):
+        la = [cls._order(x) for x in va]
+        lb = [cls._order(x) for x in vb]
+        while la or lb:
+            a = 0
+            b = 0
+            if la:
+                a = la.pop(0)
+            if lb:
+                b = lb.pop(0)
+            res = cmp(a, b)
+            if res != 0:
+                return res
+        return 0
+
+    @classmethod
+    def _version_cmp_part(cls, va, vb):
+        la = cls.re_all_digits_or_not.findall(va)
+        lb = cls.re_all_digits_or_not.findall(vb)
+        while la or lb:
+            a = "0"
+            b = "0"
+            if la:
+                a = la.pop(0)
+            if lb:
+                b = lb.pop(0)
+            if cls.re_digits.match(a) and cls.re_digits.match(b):
+                a = int(a)
+                b = int(b)
+                res = cmp(a, b)
+                if res != 0:
+                    return res
+            else:
+                res = cls._version_cmp_string(a, b)
+                if res != 0:
+                    return res
+        return 0
+
+if __have_apt_pkg:
+    class Version(AptPkgVersion):
+        pass
+else:
+    class Version(NativeVersion):
+        pass
+
+def version_compare(a, b):
+    return cmp(Version(a), Version(b))
 
 class PackageFile:
     """A Debian package file.
@@ -423,23 +548,32 @@ mergeAsSets = function_deprecated_by(merge_as_sets)
 
 def test():
     # Version
-    assert Version('0') < Version('a')
-    assert Version('1.0') < Version('1.1')
-    assert Version('1.2') < Version('1.11')
-    assert Version('1.0-0.1') < Version('1.1')
-    assert Version('1.0-0.1') < Version('1.0-1')
-    assert Version('1.0-0.1') == Version('1.0-0.1')
-    assert Version('1.0-0.1') < Version('1.0-1')
-    assert Version('1.0final-5sarge1') > Version('1.0final-5') \
-           > Version('1.0a7-2')
-    assert Version('0.9.2-5') < Version('0.9.2+cvs.1.0.dev.2004.07.28-1.5')
-    assert Version('1:500') < Version('1:5000')
-    assert Version('100:500') > Version('11:5000')
-    assert Version('1.0.4-2') > Version('1.0pre7-2')
-    assert Version('1.5~rc1') < Version('1.5')
-    assert Version('1.5~rc1') < Version('1.5+b1')
-    assert Version('1.5~rc1') < Version('1.5~rc2')
-    assert Version('1.5~rc1') > Version('1.5~dev0')
+    for (cls1, cls2) in [(AptPkgVersion, AptPkgVersion),
+                         (AptPkgVersion, NativeVersion),
+                         (NativeVersion, AptPkgVersion),
+                         (NativeVersion, NativeVersion),
+                         (str, AptPkgVersion), (AptPkgVersion, str),
+                         (str, NativeVersion), (NativeVersion, str)]:
+        assert cls1('0') < cls2('a')
+        assert cls1('1.0') < cls2('1.1')
+        assert cls1('1.2') < cls2('1.11')
+        assert cls1('1.0-0.1') < cls2('1.1')
+        assert cls1('1.0-0.1') < cls2('1.0-1')
+        assert cls1('1.0') == cls2('1.0')
+        assert cls1('1.0-0.1') == cls2('1.0-0.1')
+        assert cls1('1:1.0-0.1') == cls2('1:1.0-0.1')
+        assert cls1('1:1.0') == cls2('1:1.0')
+        assert cls1('1.0-0.1') < cls2('1.0-1')
+        assert cls1('1.0final-5sarge1') > cls2('1.0final-5') \
+               > cls2('1.0a7-2')
+        assert cls1('0.9.2-5') < cls2('0.9.2+cvs.1.0.dev.2004.07.28-1.5')
+        assert cls1('1:500') < cls2('1:5000')
+        assert cls1('100:500') > cls2('11:5000')
+        assert cls1('1.0.4-2') > cls2('1.0pre7-2')
+        assert cls1('1.5~rc1') < cls2('1.5')
+        assert cls1('1.5~rc1') < cls2('1.5+b1')
+        assert cls1('1.5~rc1') < cls2('1.5~rc2')
+        assert cls1('1.5~rc1') > cls2('1.5~dev0')
 
     # Release
     assert intern_release('sarge') < intern_release('etch')
-- 
1.7.0




More information about the pkg-python-debian-discuss mailing list