Anonymous avatar Anonymous committed af39f03 Merge

merged branch tarek-postdev in default

Comments (0)

Files changed (4)

+.*pyc$
+
+======
+verlib
+======
+
+``verlib`` provides a version managment system.
+
+The pseudo-format supported is::
+
+    N.N[.N]+[abc]N[.N]+[.(dev|post)N+|(devNpostN)]
+
+XXX explain why it's better than setuptools, from debian/red-hat PoV
+
+XXX explain here in details (no code example)
+XXX give tons of examples
+
+RationalVersion
+===============
+
+The `RationalVersion` class is used to hold a version and to compare it with
+others. It takes a string as an argument, that contains the representation of
+the version::
+
+    >>> from verlib import RationalVersion
+    >>> version = RationalVersion('1.0')
+
+The version can be represented as a string::
+
+    >>> str(version)
+    '1.0'
+
+Or compared with others::
+
+    >>> RationalVersion('1.0') > RationalVersion('0.9')
+    True
+    >>> RationalVersion('1.0') < RationalVersion('1.1')
+    True
+
+A class method called ``from_parts`` is available if you want to create an
+instance by providing the parts that composes the version.
+
+Each part is a tuple and there are three parts:
+
+- the main version part
+- the pre-release part
+- the `devpost` marker part
+
+Examples ::
+
+    >>> version = RationalVersion.from_parts((1, 0))
+    >>> str(version)
+    '1.0'
+
+    >>> version = RationalVersion.from_parts((1, 0), ('c', 4))
+    >>> str(version)
+    '1.0c4'
+
+    >>> version = RationalVersion.from_parts((1, 0), ('c', 4), ('dev', 34))
+    >>> str(version)
+    '1.0c4.dev34'
+
+suggest_rational_version
+========================
+
+XXX explain here suggest_rational_version
+"""Tests for distutils.version."""
+import unittest
+import doctest
+import os
+
+from verlib import RationalVersion as V
+from verlib import IrrationalVersionError
+from verlib import suggest_rational_version as suggest
+
+class VersionTestCase(unittest.TestCase):
+
+    versions = ((V('1.0'), '1.0'),
+                (V('1.1'), '1.1'),
+                (V('1.2.3'), '1.2.3'),
+                (V('1.2'), '1.2'),
+                (V('1.2.3a4'), '1.2.3a4'),
+                (V('1.2c4'), '1.2c4'),
+                (V('1.2.3.4'), '1.2.3.4'),
+                (V('1.2.3.4.0b3'), '1.2.3.4b3'),
+                (V('1.2.0.0.0'), '1.2'),
+                (V('1.0.dev345'), '1.0.dev345'),
+                (V('1.0.dev456post623'), '1.0.dev456post623'))
+
+    def test_basic_versions(self):
+
+        for v, s in self.versions:
+            self.assertEquals(str(v), s)
+
+    def test_from_parts(self):
+
+        for v, s in self.versions:
+            parts = v.parts
+            v2 = V.from_parts(*v.parts)
+            self.assertEquals(v, v2)
+            self.assertEquals(str(v), str(v2))
+
+    def test_irrational_versions(self):
+
+        irrational = ('1', '1.2a', '1.2.3b', '1.02',
+                      '1.2a03', '1.2a3.04')
+
+        for s in irrational:
+            self.assertRaises(IrrationalVersionError, V, s)
+
+    def test_comparison(self):
+        r"""
+        >>> V('1.2.0') == '1.2'
+        Traceback (most recent call last):
+        ...
+        TypeError: cannot compare RationalVersion and str
+
+        >>> V('1.2.0') == V('1.2')
+        True
+        >>> V('1.2.0') == V('1.2.3')
+        False
+        >>> V('1.2.0') < V('1.2.3')
+        True
+        >>> (V('1.0') > V('1.0b2'))
+        True
+        >>> (V('1.0') > V('1.0c2') > V('1.0c1') > V('1.0b2') > V('1.0b1')
+        ...  > V('1.0a2') > V('1.0a1'))
+        True
+        >>> (V('1.0.0') > V('1.0.0c2') > V('1.0.0c1') > V('1.0.0b2') > V('1.0.0b1')
+        ...  > V('1.0.0a2') > V('1.0.0a1'))
+        True
+
+        >>> V('1.0') < V('1.0.dev456post623')
+        True
+
+        >>> V('1.0.dev456post623') < V('1.0.post456')
+        True
+
+        >>> (V('1.0a1')
+        ...  < V('1.0a2.dev456')
+        ...  < V('1.0a2')
+        ...  < V('1.0a2.1.dev456')  # e.g. need to do a quick post release on 1.0a2
+        ...  < V('1.0a2.1')
+        ...  < V('1.0b1.dev456')
+        ...  < V('1.0b2')
+        ...  < V('1.0c1.dev456')
+        ...  < V('1.0c1')
+        ...  < V('1.0.dev456')
+        ...  < V('1.0')
+        ...  < V('1.0.dev456post623')  # development version of a post release
+        ...  < V('1.0.post456'))
+        True
+        """
+        # must be a simpler way to call the docstrings
+        doctest.run_docstring_examples(self.test_comparison, globals(),
+                                       name='test_comparison')
+
+    def test_suggest_rational_version(self):
+
+        self.assertEquals(suggest('1.0'), '1.0')
+        self.assertEquals(suggest('1.0-alpha1'), '1.0a1')
+        self.assertEquals(suggest('1.0rc2'), '1.0c2')
+        self.assertEquals(suggest('walla walla washington'), None)
+
+        # from setuptools
+        self.assertEquals(suggest('0.4a1.r10'), '0.4a1.post10')
+        self.assertEquals(suggest('0.7a1dev-r66608'), '0.7a1.dev66608')
+        self.assertEquals(suggest('0.6a9.dev-r41475'), '0.6a9.dev41475')
+        self.assertEquals(suggest('2.4preview1'), '2.4c1')
+        self.assertEquals(suggest('2.4rc1'), '2.4c1')
+        self.assertEquals(suggest('2.4pre1') , '2.4c1')
+        self.assertEquals(suggest('2.1-rc2'), '2.1c2')
+
+def test_suite():
+    README = os.path.join(os.path.dirname(__file__), 'README.txt')
+    suite = [doctest.DocFileSuite(README), unittest.makeSuite(VersionTestCase)]
+    return unittest.TestSuite(suite)
+
+if __name__ == "__main__":
+    unittest.main(defaultTest="test_suite")
+
-
-r"""
-"Rational" version definition and parsing for DistutilsVersionFight
-discussion at PyCon 2009.
-
->>> from verlib import RationalVersion as V
->>> v = V('1.2.3')
->>> str(v)
-'1.2.3'
-
->>> v = V('1.2')
->>> str(v)
-'1.2'
-
->>> v = V('1.2.3a4')
->>> str(v)
-'1.2.3a4'
-
->>> v = V('1.2c4')
->>> str(v)
-'1.2c4'
-
->>> v = V('1.2.3.4')
->>> str(v)
-'1.2.3.4'
-
->>> v = V('1.2.3.4.0b3')
->>> str(v)
-'1.2.3.4b3'
-
->>> V('1.2.0.0.0') == V('1.2')
-True
-
-# Irrational version strings
-
->>> v = V('1')
-Traceback (most recent call last):
-  ...
-IrrationalVersionError: 1
->>> v = V('1.2a')
-Traceback (most recent call last):
-  ...
-IrrationalVersionError: 1.2a
->>> v = V('1.2.3b')
-Traceback (most recent call last):
-  ...
-IrrationalVersionError: 1.2.3b
->>> v = V('1.02')
-Traceback (most recent call last):
-  ...
-IrrationalVersionError: cannot have leading zero in version number segment: '02' in '1.02'
->>> v = V('1.2a03')
-Traceback (most recent call last):
-  ...
-IrrationalVersionError: cannot have leading zero in version number segment: '03' in '1.2a03'
->>> v = V('1.2a3.04')
-Traceback (most recent call last):
-  ...
-IrrationalVersionError: cannot have leading zero in version number segment: '04' in '1.2a3.04'
-
-# Comparison
-
->>> V('1.2.0') == '1.2'
-Traceback (most recent call last):
-  ...
-TypeError: cannot compare RationalVersion and str
-
->>> V('1.2.0') == V('1.2')
-True
->>> V('1.2.0') == V('1.2.3')
-False
->>> V('1.2.0') < V('1.2.3')
-True
->>> (V('1.0') > V('1.0b2'))
-True
->>> (V('1.0') > V('1.0c2') > V('1.0c1') > V('1.0b2') > V('1.0b1')
-...  > V('1.0a2') > V('1.0a1'))
-True
->>> (V('1.0.0') > V('1.0.0c2') > V('1.0.0c1') > V('1.0.0b2') > V('1.0.0b1')
-...  > V('1.0.0a2') > V('1.0.0a1'))
-True
-
->>> (V('1.0a1')
-...  < V('1.0a2.dev456')
-...  < V('1.0a2')
-...  < V('1.0a2.1.dev456')  # e.g. need to do a quick post release on 1.0a2
-...  < V('1.0a2.1')
-...  < V('1.0b1.dev456')
-...  < V('1.0b2')
-...  < V('1.0c1.dev456')
-...  < V('1.0c1')
-...  < V('1.0.dev456')
-...  < V('1.0')
-...  < V('1.0.post456'))
-True
-
 """
-
+"Rational" version definition and parsing for DistutilsVersionFight discussion at PyCon 2009.
+"""
 import sys
 import re
-from pprint import pprint
-
 
 class IrrationalVersionError(Exception):
     """This is an irrational version."""
+    pass
 
 class HugeMajorVersionNumError(IrrationalVersionError):
     """An irrational version because the major version number is huge
     See `error_on_huge_major_num` option in `RationalVersion` for details.
     This guard can be disabled by setting that option False.
     """
+    pass
 
+# A marker used in the second and third parts of the `parts` tuple, for
+# versions that don't have those segments, to sort properly. A example
+# of versions in sort order ('highest' last):
+#   1.0b1                 ((1,0), ('b',1), ('f',))
+#   1.0.dev345            ((1,0), ('f',),  ('dev', 345))
+#   1.0                   ((1,0), ('f',),  ('f',))
+#   1.0.dev256post345     ((1,0), ('f',),  ('f', 'dev', 256, 'post', 345))
+#   1.0.post345           ((1,0), ('f',),  ('f', 'post', 345))
+#                                   ^        ^
+#   'f' < 'b' ---------------------/         |
+#                                            |
+#   'dev' < 'f' < 'post' -------------------/
+# Other letters would do, bug 'f' for 'final' is kind of nice.
+FINAL_MARKER = ('f',)
+
+VERSION_RE = re.compile(r'''
+    ^
+    (?P<version>\d+\.\d+)          # minimum 'N.N'
+    (?P<extraversion>(?:\.\d+)*)   # any number of extra '.N' segments
+    (?:
+        (?P<prerel>[abc])             # 'a'=alpha, 'b'=beta, 'c'=release candidate
+        (?P<prerelversion>\d+(?:\.\d+)*)
+    )?
+    (?P<devpost>\.((dev(?P<devpost1>\d+)post(?P<devpost2>\d+))|
+                            (dev(?P<dev>\d+))|
+                            (post(?P<post>\d+))))?
+    $''', re.VERBOSE)
 
 class RationalVersion(object):
     """A rational version.
         """
         self._parse(s, error_on_huge_major_num)
 
-    _version_re = re.compile(r'''
-        ^
-        (\d+\.\d+)              # minimum 'N.N'
-        ((?:\.\d+)*)            # any number of extra '.N' segments
-        (?:
-          ([abc])               # 'a'=alpha, 'b'=beta, 'c'=release candidate
-          (\d+(?:\.\d+)*)
-        )?
-        (\.(dev|post)(\d+))?    # pre- (aka development) and post-release tag
-        $
-        ''', re.VERBOSE)
-    # A marker used in the second and third parts of the `info` tuple, for
-    # versions that don't have those segments, to sort properly. A example
-    # of versions in sort order ('highest' last):
-    #   1.0b1           ((1,0), ('b',1), ('f',))
-    #   1.0.dev345      ((1,0), ('f',),  ('dev', 345))
-    #   1.0             ((1,0), ('f',),  ('f',))
-    #   1.0.post345     ((1,0), ('f',),  ('post', 345))
-    #                           ^        ^
-    #   'f' < 'b' -------------/         |
-    #                                    |
-    #   'dev' < 'f' < 'post' -----------/
-    # Other letters would do, bug 'f' for 'final' is kind of nice.
-    _final_marker = ('f',)
+    @classmethod
+    def from_parts(cls, version, prerelease=FINAL_MARKER,
+                   devpost=FINAL_MARKER):
+        return cls(cls.parts_to_str((version, prerelease, devpost)))
 
     def _parse(self, s, error_on_huge_major_num=True):
-        match = self._version_re.search(s)
+        """Parses a string version into parts."""
+        match = VERSION_RE.search(s)
         if not match:
             raise IrrationalVersionError(s)
-        groups = match.groups()
+
+        groups = match.groupdict()
         parts = []
-        block = self._parse_numdots(groups[0], s, False, 2)
-        if groups[1]:
-            block += self._parse_numdots(groups[1][1:], s)
+
+        # main version
+        block = self._parse_numdots(groups['version'], s, False, 2)
+        extraversion = groups.get('extraversion')
+        if extraversion not in ('', None):
+            block += self._parse_numdots(extraversion[1:], s)
         parts.append(tuple(block))
-        if groups[2]:
-            block = [groups[2]]
-            block += self._parse_numdots(groups[3], s, pad_zeros_length=1)
+
+        # prerelease
+        prerel = groups.get('prerel')
+        if prerel is not None:
+            block = [prerel]
+            block += self._parse_numdots(groups.get('prerelversion'), s,
+                                         pad_zeros_length=1)
             parts.append(tuple(block))
         else:
-            parts.append(self._final_marker)
-        if groups[4]:
-            parts.append((groups[5], int(groups[6])))
+            parts.append(FINAL_MARKER)
+
+        if groups.get('devpost') is not None:
+            dev = groups.get('dev')
+            post = groups.get('post')
+            if dev is not None:
+                parts.append(('dev', dev))
+            elif post is not None:
+                parts.append((FINAL_MARKER[0], 'post', post))
+            else:
+                parts.append((FINAL_MARKER[0],
+                              'dev', groups.get('devpost1'),
+                              'post', groups.get('devpost2')))
         else:
-            parts.append(self._final_marker)
-        self.info = tuple(parts)
-        if error_on_huge_major_num and self.info[0][0] > 1980:
+            parts.append(FINAL_MARKER)
+        self.parts = tuple(parts)
+        if error_on_huge_major_num and self.parts[0][0] > 1980:
             raise HugeMajorVersionNumError("huge major version number, %r, "
-                "which might cause future problems: %r" % (self.info[0][0], s))
-        #print "_parse(%r) -> %r" % (s, self.info)
+                "which might cause future problems: %r" % (self.parts[0][0], s))
 
     def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True,
-            pad_zeros_length=0):
+                       pad_zeros_length=0):
         """Parse 'N.N.N' sequences, return a list of ints.
 
         @param s {str} 'N.N.N..." sequence to be parsed
         return nums
 
     def __str__(self):
-        main, prerel, devpost = self.info
-        s = '.'.join(str(v) for v in main if v)
-        if prerel is not self._final_marker:
+        return self.parts_to_str(self.parts)
+
+    @classmethod
+    def parts_to_str(cls, parts):
+        """Transforms a version expressed in tuple into its string
+        representation."""
+        main, prerel, devpost = parts
+        s = '.'.join(str(v) for v in main)
+        if prerel is not FINAL_MARKER:
             s += prerel[0]
-            s += '.'.join(str(v) for v in prerel[1:] if v)
-        if devpost is not self._final_marker:
-            s += '.' + ''.join(str(v) for v in prerel[1:] if v)
+            s += '.'.join(str(v) for v in prerel[1:])
+        if devpost is not FINAL_MARKER:
+            if devpost[0] == 'f':
+                devpost = devpost[1:]
+            s += '.' + ''.join(str(v) for v in devpost)
         return s
 
     def __repr__(self):
         return "%s('%s')" % (self.__class__.__name__, self)
 
-    # Comparison
+    def _cannot_compare(self, other):
+        raise TypeError("cannot compare %s and %s"
+                % (type(self).__name__, type(other).__name__))
+
     def __eq__(self, other):
         if not isinstance(other, RationalVersion):
-            raise TypeError("cannot compare %s and %s"
-                % (type(self).__name__, type(other).__name__))
-        return self.info == other.info
+            self._cannot_compare(other)
+        return self.parts == other.parts
+
     def __lt__(self, other):
         if not isinstance(other, RationalVersion):
-            raise TypeError("cannot compare %s and %s"
-                % (type(self).__name__, type(other).__name__))
-        return self.info < other.info
+            self._cannot_compare(other)
+        return self.parts < other.parts
+
     def __ne__(self, other):
         return not self.__eq__(other)
+
     def __gt__(self, other):
         return not (self.__lt__(other) or self.__eq__(other))
+
     def __le__(self, other):
         return self.__eq__(other) or self.__lt__(other)
+
     def __ge__(self, other):
         return self.__eq__(other) or self.__gt__(other)
 
-
 def suggest_rational_version(s):
     """Suggest a rational version close to the given version string.
 
 
     @param s {str} An irrational version string.
     @returns A rational version string, or None, if couldn't determine one.
-
-    >>> suggest_rational_version('1.0')
-    '1.0'
-    >>> suggest_rational_version('1.0-alpha1')
-    '1.0a1'
-    >>> suggest_rational_version('1.0rc2')
-    '1.0c2'
-    >>> suggest_rational_version('walla walla washington')  # no suggestion
     """
     try:
         RationalVersion(s)
     except IrrationalVersionError:
         pass
 
-    rs = (s
-        .lower()
-        .replace('-alpha', 'a')
-        .replace('-beta', 'b')
-        .replace('alpha', 'a')
-        .replace('beta', 'b')
-        .replace('rc', 'c')
-        .replace('-', '.')
-        .replace('+', '.')
-        .replace('_', '.')
-        .replace(' ', '')
-        # Clean: 0.2.final, 0.5.0final
-        .replace('.final', '')
-        .replace('final', '')
-        )
+    rs = s.lower()
+
+    # part of this could use maketrans
+    for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
+                       ('beta', 'b'), ('rc', 'c'),
+                       ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
+                       ('final', '')):
+        rs = rs.replace(orig, repl)
 
     # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
     rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
     # PyPI stats: 245 (7.56%) better
     rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
 
+    # the 'dev-rNNN' tag is a dev tag
+    rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
+
+    # The 'r' and the '-' tags are post release tags
+    #   0.4a1.r10       ->  0.4a1.post10
+    #   0.9.33-17222    ->  0.9.3.post17222
+    #   0.9.33-r17222   ->  0.9.3.post17222
+    rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
+
     # Clean 'r' instead of 'dev' usage:
-    #   0.4a1.r10       ->  0.4a1.dev10
     #   0.9.33+r17222   ->  0.9.3.dev17222
     #   1.0dev123       ->  1.0.dev123
     #   1.0.git123      ->  1.0.dev123
     #   1.0.bzr123      ->  1.0.dev123
     #   0.1a0dev.123    ->  0.1a0.dev123
     # PyPI stats:  ~150 (~4%) better
-    rs = re.sub(r"\.?(r|dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
+    rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
 
     # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
     #   0.2.pre1        ->  0.2c1
-    #   0.2.pre1        ->  0.2c1
+    #   0.2-c1         ->  0.2c1
     #   1.0preview123   ->  1.0c123
     # PyPI stats: ~21 (0.62%) better
-    rs = re.sub(r"\.?(pre|preview)(\d+)$", r"c\g<2>", rs)
-
+    rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
 
     try:
         RationalVersion(rs)
         pass
     return None
 
-
-#---- mainline and test
-
-def _test():
-    import doctest
-    doctest.testmod()
-
-def _play():
-    V = RationalVersion
-    print V('1.0.dev123') < V('1.0.dev456') < V('1.0') < V('1.0.post456') < V('1.0.post789')
-
-if __name__ == "__main__":
-    #_play()
-    _test()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.