Anonymous avatar Anonymous committed cc93f5e

initial commit (from the last Pycon version)

Comments (0)

Files changed (1)

+
+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
+
+"""
+
+import sys
+import re
+from pprint import pprint
+
+
+class IrrationalVersionError(Exception):
+    """This is an irrational version."""
+
+class HugeMajorVersionNumError(IrrationalVersionError):
+    """An irrational version because the major version number is huge
+    (often because a year or date was used).
+
+    See `error_on_huge_major_num` option in `RationalVersion` for details.
+    This guard can be disabled by setting that option False.
+    """
+
+
+class RationalVersion(object):
+    """A rational version.
+
+    Good:
+        1.2         # equivalent to "1.2.0"
+        1.2.0
+        1.2a1
+        1.2.3a2
+        1.2.3b1
+        1.2.3c1
+        1.2.3.4
+        TODO: fill this out
+
+    Bad:
+        1           # mininum two numbers
+        1.2a        # release level must have a release serial
+        1.2.3b
+    """
+    def __init__(self, s, error_on_huge_major_num=True):
+        """Create a RationalVersion instance from a version string.
+
+        @param s {str} The version string.
+        @param error_on_huge_major_num {bool} Whether to consider an
+            apparent use of a year or full date as the major version number
+            an error. Default True. One of the observed patterns on PyPI before
+            the introduction of `RationalVersion` was version numbers like this:
+                2009.01.03
+                20040603
+                2005.01
+            This guard is here to strongly encourage the package author to
+            use an alternate version, because a release deployed into PyPI
+            and, e.g. downstream Linux package managers, will forever remove
+            the possibility of using a version number like "1.0" (i.e.
+            where the major number is less than that huge major number).
+        """
+        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',)
+
+    def _parse(self, s, error_on_huge_major_num=True):
+        match = self._version_re.search(s)
+        if not match:
+            raise IrrationalVersionError(s)
+        groups = match.groups()
+        parts = []
+        block = self._parse_numdots(groups[0], s, False, 2)
+        if groups[1]:
+            block += self._parse_numdots(groups[1][1:], s)
+        parts.append(tuple(block))
+        if groups[2]:
+            block = [groups[2]]
+            block += self._parse_numdots(groups[3], 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])))
+        else:
+            parts.append(self._final_marker)
+        self.info = tuple(parts)
+        if error_on_huge_major_num and self.info[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)
+
+    def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True,
+            pad_zeros_length=0):
+        """Parse 'N.N.N' sequences, return a list of ints.
+
+        @param s {str} 'N.N.N..." sequence to be parsed
+        @param full_ver_str {str} The full version string from which this
+            comes. Used for error strings.
+        @param drop_trailing_zeros {bool} Whether to drop trailing zeros
+            from the returned list. Default True.
+        @param pad_zeros_length {int} The length to which to pad the
+            returned list with zeros, if necessary. Default 0.
+        """
+        nums = []
+        for n in s.split("."):
+            if len(n) > 1 and n[0] == '0':
+                raise IrrationalVersionError("cannot have leading zero in "
+                    "version number segment: '%s' in %r" % (n, full_ver_str))
+            nums.append(int(n))
+        if drop_trailing_zeros:
+            while nums and nums[-1] == 0:
+                nums.pop()
+        while len(nums) < pad_zeros_length:
+            nums.append(0)
+        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:
+            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)
+        return s
+
+    def __repr__(self):
+        return "%s('%s')" % (self.__class__.__name__, self)
+
+    # Comparison
+    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
+    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
+    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.
+
+    If you have a version string that isn't rational (i.e. RationalVersion
+    doesn't like it) then you might be able to get an equivalent (or close)
+    rational version from this function.
+
+    This does a number of simple normalizations to the given string, based
+    on observation of versions currently in use on PyPI. Given a dump of
+    those version during PyCon 2009, 4287 of them:
+    - 2312 (53.93%) match RationalVersion without change
+    - with the automatic suggestion
+    - 3474 (81.04%) match when using this suggestion method
+
+    @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)
+        return s   # already rational
+    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', '')
+        )
+
+    # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
+    rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
+
+    # Clean: v0.3, v1.0
+    if rs.startswith('v'):
+        rs = rs[1:]
+
+    # Clean leading '0's on numbers.
+    #TODO: unintended side-effect on, e.g., "2003.05.09"
+    # PyPI stats: 77 (~2%) better
+    rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
+
+    # Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers
+    # zero.
+    # PyPI stats: 245 (7.56%) better
+    rs = re.sub(r"(\d+[abc])$", r"\g<1>0", 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)
+
+    # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
+    #   0.2.pre1        ->  0.2c1
+    #   0.2.pre1        ->  0.2c1
+    #   1.0preview123   ->  1.0c123
+    # PyPI stats: ~21 (0.62%) better
+    rs = re.sub(r"\.?(pre|preview)(\d+)$", r"c\g<2>", rs)
+
+
+    try:
+        RationalVersion(rs)
+        return rs   # already rational
+    except IrrationalVersionError:
+        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.