Commits

Tarek Ziadé  committed e1d073e

includeed the new version tool -- ready to be used in DistributionMetadata

  • Participants
  • Parent commits e00c167

Comments (0)

Files changed (8)

File src/distutils2/tests/LONG_DESC.txt

+CLVault
+=======
+
+CLVault uses Keyring to provide a command-line utility to safely store
+and retrieve passwords.
+
+Install it using pip or the setup.py script::
+
+    $ python setup.py install
+
+    $ pip install clvault
+
+Once it's installed, you will have three scripts installed in your
+Python scripts folder, you can use to list, store and retrieve passwords::
+
+    $ clvault-set blog
+    Set your password:
+    Set the associated username (can be blank): tarek
+    Set a description (can be blank): My blog password
+    Password set.
+
+    $ clvault-get blog
+    The username is "tarek"
+    The password has been copied in your clipboard
+
+    $ clvault-list
+    Registered services:
+    blog    My blog password
+
+
+*clvault-set* takes a service name then prompt you for a password, and some
+optional information about your service. The password is safely stored in
+a keyring while the description is saved in a ``.clvault`` file in your
+home directory. This file is created automatically the first time the command
+is used.
+
+*clvault-get* copies the password for a given service in your clipboard, and
+displays the associated user if any.
+
+*clvault-list* lists all registered services, with their description when
+given.
+
+
+Project page: http://bitbucket.org/tarek/clvault

File src/distutils2/tests/test_pypi_versions.py

+#
+## test_pypi_versions.py
+##
+##  A very simple test to see what percentage of the current pypi packages
+##  have versions that can be converted automatically by distutils' new
+##  suggest_normalized_version() into PEP-386 compatible versions.
+##
+##  Requires : Python 2.5+
+##
+##  Written by: ssteinerX@gmail.com
+#
+
+try:
+   import cPickle as pickle
+except:
+   import pickle
+
+import xmlrpclib
+import os.path
+import unittest2
+
+from distutils2.version import suggest_normalized_version
+
+def test_pypi():
+    #
+    ## To re-run from scratch, just delete these two .pkl files
+    #
+    INDEX_PICKLE_FILE = 'pypi-index.pkl'
+    VERSION_PICKLE_FILE = 'pypi-version.pkl'
+
+    package_info = version_info = []
+
+    #
+    ## if there's a saved version of the package list
+    ##      restore it
+    ## else:
+    ##      pull the list down from pypi
+    ##      save a pickled version of it
+    #
+    if os.path.exists(INDEX_PICKLE_FILE):
+        print "Loading saved pypi data..."
+        with open(INDEX_PICKLE_FILE, 'rb') as f:
+            package_info = pickle.load(f)
+    else:
+        print "Retrieving pypi packages..."
+        server = xmlrpclib.Server('http://pypi.python.org/pypi')
+        package_info  = server.search({'name': ''})
+
+        print "Saving package info..."
+        with open(INDEX_PICKLE_FILE, 'wb') as o:
+            pickle.dump(package_info, o)
+
+    #
+    ## If there's a saved list of the versions from the packages
+    ##      restore it
+    ## else
+    ##     extract versions from the package list
+    ##     save a pickled version of it
+    #
+    versions = []
+    if os.path.exists(VERSION_PICKLE_FILE):
+        print "Loading saved version info..."
+        with open(VERSION_PICKLE_FILE, 'rb') as f:
+            versions = pickle.load(f)
+    else:
+        print "Extracting and saving version info..."
+        versions = [p['version'] for p in package_info]
+        with open(VERSION_PICKLE_FILE, 'wb') as o:
+            pickle.dump(versions, o)
+
+    total_versions = len(versions)
+    matches = 0.00
+    no_sugg = 0.00
+    have_sugg = 0.00
+
+    suggs = []
+    no_suggs = []
+
+    for ver in versions:
+        sugg = suggest_normalized_version(ver)
+        if sugg == ver:
+            matches += 1
+        elif sugg == None:
+            no_sugg += 1
+            no_suggs.append(ver)
+        else:
+            have_sugg += 1
+            suggs.append((ver, sugg))
+
+    pct = "(%2.2f%%)"
+    print "Results:"
+    print "--------"
+    print ""
+    print "Suggestions"
+    print "-----------"
+    print ""
+    for ver, sugg in suggs:
+        print "%s -> %s" % (ver, sugg)
+    print ""
+    print "No suggestions"
+    print "--------------"
+    for ver in no_suggs:
+        print ver
+    print ""
+    print "Summary:"
+    print "--------"
+    print "Total Packages  : ", total_versions
+    print "Already Match   : ", matches, pct % (matches/total_versions*100,)
+    print "Have Suggestion : ", have_sugg, pct % (have_sugg/total_versions*100,)
+    print "No Suggestion   : ", no_sugg, pct % (no_sugg/total_versions*100,)
+
+class TestPyPI(unittest2.TestCase):
+    pass
+
+def test_suite():
+    return unittest2.makeSuite(TestPyPI)
+
+if __name__ == '__main__':
+    run_unittest(test_suite())
+

File src/distutils2/tests/test_util.py

                             byte_compile)
 from distutils2 import util
 from distutils2.tests import support
-from distutils2.version import LooseVersion
 
 class FakePopen(object):
     test_class = None

File src/distutils2/tests/test_version.py

 """Tests for distutils.version."""
-import unittest2
-from distutils2.version import LooseVersion
-from distutils2.version import StrictVersion
+import unittest
+import doctest
+import os
 
-class VersionTestCase(unittest2.TestCase):
+from distutils2.version import NormalizedVersion as V
+from distutils2.version import IrrationalVersionError
+from distutils2.version import suggest_normalized_version as suggest
 
-    def test_prerelease(self):
-        version = StrictVersion('1.2.3a1')
-        self.assertEquals(version.version, (1, 2, 3))
-        self.assertEquals(version.prerelease, ('a', 1))
-        self.assertEquals(str(version), '1.2.3a1')
+class VersionTestCase(unittest.TestCase):
 
-        version = StrictVersion('1.2.0')
-        self.assertEquals(str(version), '1.2')
+    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.post456.dev623'), '1.0.post456.dev623'))
 
-    def test_cmp_strict(self):
-        versions = (('1.5.1', '1.5.2b2', -1),
-                    ('161', '3.10a', ValueError),
-                    ('8.02', '8.02', 0),
-                    ('3.4j', '1996.07.12', ValueError),
-                    ('3.2.pl0', '3.1.1.6', ValueError),
-                    ('2g6', '11g', ValueError),
-                    ('0.9', '2.2', -1),
-                    ('1.2.1', '1.2', 1),
-                    ('1.1', '1.2.2', -1),
-                    ('1.2', '1.1', 1),
-                    ('1.2.1', '1.2.2', -1),
-                    ('1.2.2', '1.2', 1),
-                    ('1.2', '1.2.2', -1),
-                    ('0.4.0', '0.4', 0),
-                    ('1.13++', '5.5.kw', ValueError))
+    def test_basic_versions(self):
 
-        for v1, v2, wanted in versions:
-            try:
-                res = StrictVersion(v1).__cmp__(StrictVersion(v2))
-            except ValueError:
-                if wanted is ValueError:
-                    continue
-                else:
-                    raise AssertionError(("cmp(%s, %s) "
-                                          "shouldn't raise ValueError")
-                                            % (v1, v2))
-            self.assertEquals(res, wanted,
-                              'cmp(%s, %s) should be %s, got %s' %
-                              (v1, v2, wanted, res))
+        for v, s in self.versions:
+            self.assertEquals(str(v), s)
 
+    def test_from_parts(self):
 
-    def test_cmp(self):
-        versions = (('1.5.1', '1.5.2b2', -1),
-                    ('161', '3.10a', 1),
-                    ('8.02', '8.02', 0),
-                    ('3.4j', '1996.07.12', -1),
-                    ('3.2.pl0', '3.1.1.6', 1),
-                    ('2g6', '11g', -1),
-                    ('0.960923', '2.2beta29', -1),
-                    ('1.13++', '5.5.kw', -1))
+        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):
 
-        for v1, v2, wanted in versions:
-            res = LooseVersion(v1).__cmp__(LooseVersion(v2))
-            self.assertEquals(res, wanted,
-                              'cmp(%s, %s) should be %s, got %s' %
-                              (v1, v2, wanted, res))
+        irrational = ('1', '1.2a', '1.2.3b', '1.02', '1.2a03',
+                      '1.2a3.04', '1.2.dev.2', '1.2dev', '1.2.dev',
+                      '1.2.dev2.post2', '1.2.post2.dev3.post4')
+
+        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 NormalizedVersion 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.post456.dev623')
+        True
+
+        >>> V('1.0.post456.dev623') < V('1.0.post456')  < V('1.0.post1234')
+        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.dev7')
+        ...  < V('1.0.dev18')
+        ...  < V('1.0.dev456')
+        ...  < V('1.0.dev1234')
+        ...  < V('1.0')
+        ...  < V('1.0.post456.dev623')  # 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_normalized_version(self):
+
+        self.assertEquals(suggest('1.0'), '1.0')
+        self.assertEquals(suggest('1.0-alpha1'), '1.0a1')
+        self.assertEquals(suggest('1.0c2'), '1.0c2')
+        self.assertEquals(suggest('walla walla washington'), None)
+        self.assertEquals(suggest('2.4c1'), '2.4c1')
+
+        # 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.4pre1') , '2.4c1')
+        self.assertEquals(suggest('2.1-rc2'), '2.1c2')
+
+        # from pypi
+        self.assertEquals(suggest('0.1dev'), '0.1.dev0')
+        self.assertEquals(suggest('0.1.dev'), '0.1.dev0')
+
+        # we want to be able to parse Twisted
+        # development versions are like post releases in Twisted
+        self.assertEquals(suggest('9.0.0+r2363'), '9.0.0.post2363')
+
+        # pre-releases are using markers like "pre1"
+        self.assertEquals(suggest('9.0.0pre1'), '9.0.0c1')
+
+        # we want to be able to parse Tcl-TK
+        # they us "p1" "p2" for post releases
+        self.assertEquals(suggest('1.4p1'), '1.4.post1')
 
 def test_suite():
-    return unittest2.makeSuite(VersionTestCase)
+    #README = os.path.join(os.path.dirname(__file__), 'README.txt')
+    #suite = [doctest.DocFileSuite(README), unittest.makeSuite(VersionTestCase)]
+    suite = [unittest.makeSuite(VersionTestCase)]
+    return unittest.TestSuite(suite)
 
 if __name__ == "__main__":
-    unittest2.main(defaultTest="test_suite")
+    unittest.main(defaultTest="test_suite")
+

File src/distutils2/tests/test_versionpredicate.py

-"""Tests harness for distutils2.versionpredicate.
-
-"""
-
-import distutils2.versionpredicate
-import doctest
-
-def test_suite():
-    return doctest.DocTestSuite(distutils2.versionpredicate)

File src/distutils2/util.py

 from distutils2.errors import DistutilsPlatformError
 from distutils2.spawn import spawn, find_executable
 from distutils2 import log
-from distutils2.version import LooseVersion
 from distutils2.errors import DistutilsByteCompileError
 
 from distutils2._backport import sysconfig as _sysconfig
     result = pattern.search(out_string)
     if result is None:
         return None
-    return LooseVersion(result.group(1))
+    return result.group(1)
 
 def get_compiler_versions():
     """Returns a tuple providing the versions of gcc, ld and dllwrap
 
     For each command, if a command is not found, None is returned.
-    Otherwise a LooseVersion instance is returned.
+    Otherwise a string with the version is returned.
     """
     gcc = _find_exe_version('gcc -dumpversion')
     ld = _find_ld_version()

File src/distutils2/version.py

-#
-# distutils/version.py
-#
-# Implements multiple version numbering conventions for the
-# Python Module Distribution Utilities.
-#
-# $Id: version.py 70642 2009-03-28 00:48:48Z georg.brandl $
-#
+import sys
+import re
 
-"""Provides classes to represent module version numbers (one class for
-each style of version numbering).  There are currently two such classes
-implemented: StrictVersion and LooseVersion.
+class IrrationalVersionError(Exception):
+    """This is an irrational version."""
+    pass
 
-Every version number class implements the following interface:
-  * the 'parse' method takes a string and parses it to some internal
-    representation; if the string is an invalid version number,
-    'parse' raises a ValueError exception
-  * the class constructor takes an optional string argument which,
-    if supplied, is passed to 'parse'
-  * __str__ reconstructs the string that was passed to 'parse' (or
-    an equivalent string -- ie. one that will generate an equivalent
-    version number instance)
-  * __repr__ generates Python code to recreate the version number instance
-  * __cmp__ compares the current instance with either another instance
-    of the same class or a string (which will be parsed to an instance
-    of the same class, thus must follow the same rules)
-"""
+class HugeMajorVersionNumError(IrrationalVersionError):
+    """An irrational version because the major version number is huge
+    (often because a year or date was used).
 
-import string, re
-from types import StringType
+    See `error_on_huge_major_num` option in `NormalizedVersion` for details.
+    This guard can be disabled by setting that option False.
+    """
+    pass
 
-class Version:
-    """Abstract base class for version numbering classes.  Just provides
-    constructor (__init__) and reproducer (__repr__), because those
-    seem to be the same for all version numbering classes.
+# A marker used in the second and third parts of the `parts` tuple, for
+# versions that don't have those segments, to sort properly. An 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.post256.dev345    ((1,0), ('f',),  ('f', 'post', 256, 'dev', 345))
+#   1.0.post345           ((1,0), ('f',),  ('f', 'post', 345, 'f'))
+#                                   ^        ^                 ^
+#   'b' < 'f' ---------------------/         |                 |
+#                                            |                 |
+#   'dev' < 'f' < 'post' -------------------/                  |
+#                                                              |
+#   'dev' < 'f' ----------------------------------------------/
+# Other letters would do, but '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]|rc)       # 'a'=alpha, 'b'=beta, 'c'=release candidate
+                                   # 'rc'= alias for release candidate
+        (?P<prerelversion>\d+(?:\.\d+)*)
+    )?
+    (?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
+    $''', re.VERBOSE)
+
+class NormalizedVersion(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 NormalizedVersion instance from a version string.
 
-    def __init__ (self, vstring=None):
-        if vstring:
-            self.parse(vstring)
+        @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 `NormalizedVersion` 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)
 
-    def __repr__ (self):
-        return "%s ('%s')" % (self.__class__.__name__, str(self))
+    @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):
+        """Parses a string version into parts."""
+        match = VERSION_RE.search(s)
+        if not match:
+            raise IrrationalVersionError(s)
 
-# Interface for version-number classes -- must be implemented
-# by the following classes (the concrete ones -- Version should
-# be treated as an abstract class).
-#    __init__ (string) - create and take same action as 'parse'
-#                        (string parameter is optional)
-#    parse (string)    - convert a string representation to whatever
-#                        internal representation is appropriate for
-#                        this style of version numbering
-#    __str__ (self)    - convert back to a string; should be very similar
-#                        (if not identical to) the string supplied to parse
-#    __repr__ (self)   - generate Python code to recreate
-#                        the instance
-#    __cmp__ (self, other) - compare two version numbers ('other' may
-#                        be an unparsed version string, or another
-#                        instance of your version class)
+        groups = match.groupdict()
+        parts = []
 
+        # 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))
 
-class StrictVersion (Version):
+        # 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(FINAL_MARKER)
 
-    """Version numbering for anal retentives and software idealists.
-    Implements the standard interface for version number classes as
-    described above.  A version number consists of two or three
-    dot-separated numeric components, with an optional "pre-release" tag
-    on the end.  The pre-release tag consists of the letter 'a' or 'b'
-    followed by a number.  If the numeric components of two version
-    numbers are equal, then one with a pre-release tag will always
-    be deemed earlier (lesser) than one without.
+        # postdev
+        if groups.get('postdev'):
+            post = groups.get('post')
+            dev = groups.get('dev')
+            postdev = []
+            if post is not None:
+                postdev.extend([FINAL_MARKER[0], 'post', int(post)])
+                if dev is None:
+                    postdev.append(FINAL_MARKER[0])
+            if dev is not None:
+                postdev.extend(['dev', int(dev)])
+            parts.append(tuple(postdev))
+        else:
+            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.parts[0][0], s))
 
-    The following are valid version numbers (shown in the order that
-    would be obtained by sorting according to the supplied cmp function):
+    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.
 
-        0.4       0.4.0  (these two are equivalent)
-        0.4.1
-        0.5a1
-        0.5b3
-        0.5
-        0.9.6
-        1.0
-        1.0.4a3
-        1.0.4b1
-        1.0.4
+        @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
 
-    The following are examples of invalid version numbers:
+    def __str__(self):
+        return self.parts_to_str(self.parts)
 
-        1
-        2.7.2.2
-        1.3.a4
-        1.3pl1
-        1.3c4
+    @classmethod
+    def parts_to_str(cls, parts):
+        """Transforms a version expressed in tuple into its string
+        representation."""
+        # XXX This doesn't check for invalid tuples
+        main, prerel, postdev = 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 postdev and postdev is not FINAL_MARKER:
+            if postdev[0] == 'f':
+                postdev = postdev[1:]
+            i = 0
+            while i < len(postdev):
+                if i % 2 == 0:
+                    s += '.'
+                s += str(postdev[i])
+                i += 1
+        return s
 
-    The rationale for this version numbering system will be explained
-    in the distutils documentation.
+    def __repr__(self):
+        return "%s('%s')" % (self.__class__.__name__, self)
+
+    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, NormalizedVersion):
+            self._cannot_compare(other)
+        return self.parts == other.parts
+
+    def __lt__(self, other):
+        if not isinstance(other, NormalizedVersion):
+            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_normalized_version(s):
+    """Suggest a normalized version close to the given version string.
+
+    If you have a version string that isn't rational (i.e. NormalizedVersion
+    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 NormalizedVersion 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.
     """
+    try:
+        NormalizedVersion(s)
+        return s   # already rational
+    except IrrationalVersionError:
+        pass
 
-    version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
-                            re.VERBOSE)
+    rs = s.lower()
 
+    # part of this could use maketrans
+    for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
+                       ('beta', 'b'), ('rc', 'c'), ('-final', ''),
+                       ('-pre', 'c'),
+                       ('-release', ''), ('.release', ''), ('-stable', ''),
+                       ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
+                       ('final', '')):
+        rs = rs.replace(orig, repl)
 
-    def parse (self, vstring):
-        match = self.version_re.match(vstring)
-        if not match:
-            raise ValueError, "invalid version number '%s'" % vstring
+    # if something ends with dev or pre, we add a 0
+    rs = re.sub(r"pre$", r"pre0", rs)
+    rs = re.sub(r"dev$", r"dev0", rs)
 
-        (major, minor, patch, prerelease, prerelease_num) = \
-            match.group(1, 2, 4, 5, 6)
+    # if we have something like "b-2" or "a.2" at the end of the
+    # version, that is pobably beta, alpha, etc
+    # let's remove the dash or dot
+    rs = re.sub(r"([abc|rc])[\-\.](\d+)$", r"\1\2", rs)
 
-        if patch:
-            self.version = tuple(map(string.atoi, [major, minor, patch]))
-        else:
-            self.version = tuple(map(string.atoi, [major, minor]) + [0])
+    # 1.0-dev-r371 -> 1.0.dev371
+    # 0.1-dev-r79 -> 0.1.dev79
+    rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
 
-        if prerelease:
-            self.prerelease = (prerelease[0], string.atoi(prerelease_num))
-        else:
-            self.prerelease = None
+    # 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:]
 
-    def __str__ (self):
+    # 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)
 
-        if self.version[2] == 0:
-            vstring = string.join(map(str, self.version[0:2]), '.')
-        else:
-            vstring = string.join(map(str, self.version), '.')
+    # 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)
 
-        if self.prerelease:
-            vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
+    # the 'dev-rNNN' tag is a dev tag
+    rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
 
-        return vstring
+    # clean the - when used as a pre delimiter
+    rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
 
+    # a terminal "dev" or "devel" can be changed into ".dev0"
+    rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
 
-    def __cmp__ (self, other):
-        if isinstance(other, StringType):
-            other = StrictVersion(other)
+    # a terminal "dev" can be changed into ".dev0"
+    rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
 
-        compare = cmp(self.version, other.version)
-        if (compare == 0):              # have to compare prerelease
+    # a terminal "final" or "stable" can be removed
+    rs = re.sub(r"(final|stable)$", "", rs)
 
-            # case 1: neither has prerelease; they're equal
-            # case 2: self has prerelease, other doesn't; other is greater
-            # case 3: self doesn't have prerelease, other does: self is greater
-            # case 4: both have prerelease: must compare them!
+    # 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)
 
-            if (not self.prerelease and not other.prerelease):
-                return 0
-            elif (self.prerelease and not other.prerelease):
-                return -1
-            elif (not self.prerelease and other.prerelease):
-                return 1
-            elif (self.prerelease and other.prerelease):
-                return cmp(self.prerelease, other.prerelease)
+    # Clean 'r' instead of 'dev' usage:
+    #   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"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
 
-        else:                           # numeric versions don't match --
-            return compare              # prerelease stuff doesn't matter
+    # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
+    #   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|-c)(\d+)$", r"c\g<2>", rs)
 
 
-# end class StrictVersion
+    # Tcl/Tk uses "px" for their post release markers
+    rs = re.sub(r"p(\d+)$", r".post\1", rs)
 
+    try:
+        NormalizedVersion(rs)
+        return rs   # already rational
+    except IrrationalVersionError:
+        pass
+    return None
 
-# The rules according to Greg Stein:
-# 1) a version number has 1 or more numbers separated by a period or by
-#    sequences of letters. If only periods, then these are compared
-#    left-to-right to determine an ordering.
-# 2) sequences of letters are part of the tuple for comparison and are
-#    compared lexicographically
-# 3) recognize the numeric components may have leading zeroes
-#
-# The LooseVersion class below implements these rules: a version number
-# string is split up into a tuple of integer and string components, and
-# comparison is a simple tuple comparison.  This means that version
-# numbers behave in a predictable and obvious way, but a way that might
-# not necessarily be how people *want* version numbers to behave.  There
-# wouldn't be a problem if people could stick to purely numeric version
-# numbers: just split on period and compare the numbers as tuples.
-# However, people insist on putting letters into their version numbers;
-# the most common purpose seems to be:
-#   - indicating a "pre-release" version
-#     ('alpha', 'beta', 'a', 'b', 'pre', 'p')
-#   - indicating a post-release patch ('p', 'pl', 'patch')
-# but of course this can't cover all version number schemes, and there's
-# no way to know what a programmer means without asking him.
-#
-# The problem is what to do with letters (and other non-numeric
-# characters) in a version number.  The current implementation does the
-# obvious and predictable thing: keep them as strings and compare
-# lexically within a tuple comparison.  This has the desired effect if
-# an appended letter sequence implies something "post-release":
-# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
-#
-# However, if letters in a version number imply a pre-release version,
-# the "obvious" thing isn't correct.  Eg. you would expect that
-# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
-# implemented here, this just isn't so.
-#
-# Two possible solutions come to mind.  The first is to tie the
-# comparison algorithm to a particular set of semantic rules, as has
-# been done in the StrictVersion class above.  This works great as long
-# as everyone can go along with bondage and discipline.  Hopefully a
-# (large) subset of Python module programmers will agree that the
-# particular flavour of bondage and discipline provided by StrictVersion
-# provides enough benefit to be worth using, and will submit their
-# version numbering scheme to its domination.  The free-thinking
-# anarchists in the lot will never give in, though, and something needs
-# to be done to accommodate them.
-#
-# Perhaps a "moderately strict" version class could be implemented that
-# lets almost anything slide (syntactically), and makes some heuristic
-# assumptions about non-digits in version number strings.  This could
-# sink into special-case-hell, though; if I was as talented and
-# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
-# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
-# just as happy dealing with things like "2g6" and "1.13++".  I don't
-# think I'm smart enough to do it right though.
-#
-# In any case, I've coded the test suite for this module (see
-# ../test/test_version.py) specifically to fail on things like comparing
-# "1.2a2" and "1.2".  That's not because the *code* is doing anything
-# wrong, it's because the simple, obvious design doesn't match my
-# complicated, hairy expectations for real-world version numbers.  It
-# would be a snap to fix the test suite to say, "Yep, LooseVersion does
-# the Right Thing" (ie. the code matches the conception).  But I'd rather
-# have a conception that matches common notions about version numbers.
 
-class LooseVersion (Version):
+_PREDICATE = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)")
+_VERSIONS = re.compile(r"^\s*\((.*)\)\s*$")
+_SPLIT_CMP = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$")
 
-    """Version numbering for anarchists and software realists.
-    Implements the standard interface for version number classes as
-    described above.  A version number consists of a series of numbers,
-    separated by either periods or strings of letters.  When comparing
-    version numbers, the numeric components will be compared
-    numerically, and the alphabetic components lexically.  The following
-    are all valid version numbers, in no particular order:
+def _split_predicate(predicate):
+    match = _SPLIT_CMP(predicate)
+    if match is None:
+        raise ValueError('Bad package restriction syntax: %r' % version)
+    comp, version = res.groups()
+    return comp, NormalizedVersion(version)
 
-        1.5.1
-        1.5.2b2
-        161
-        3.10a
-        8.02
-        3.4j
-        1996.07.12
-        3.2.pl0
-        3.1.1.6
-        2g6
-        11g
-        0.960923
-        2.2beta29
-        1.13++
-        5.5.kw
-        2.0b1pl0
+class VersionPredicate(object):
+    """Defines a predicate: ProjectName (>ver1,ver2, ..)"""
+    def __init__(self, predicate):
+        predicate = predicate.strip()
+        match = _PREDICATE.match(predicate)
+        if match is None:
+            raise ValueError('Bad predicate "%s"' % predicate)
+        self.name, predicates = match.groups()
+        predicates = predicates.strip()
 
-    In fact, there is no such thing as an invalid version number under
-    this scheme; the rules for comparison are simple and predictable,
-    but may not always give the results you want (for some definition
-    of "want").
-    """
+        if predicates is not None:
+            predicates = match.groups()[0]
+            self.versions = [_split_predicate(pred)
+                             for pred in predicates.split(',')]
 
-    component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
-
-    def __init__ (self, vstring=None):
-        if vstring:
-            self.parse(vstring)
-
-
-    def parse (self, vstring):
-        # I've given up on thinking I can reconstruct the version string
-        # from the parsed tuple -- so I just store the string here for
-        # use by __str__
-        self.vstring = vstring
-        components = filter(lambda x: x and x != '.',
-                            self.component_re.split(vstring))
-        for i in range(len(components)):
-            try:
-                components[i] = int(components[i])
-            except ValueError:
-                pass
-
-        self.version = components
-
-
-    def __str__ (self):
-        return self.vstring
-
-
-    def __repr__ (self):
-        return "LooseVersion ('%s')" % str(self)
-
-
-    def __cmp__ (self, other):
-        if isinstance(other, StringType):
-            other = LooseVersion(other)
-
-        return cmp(self.version, other.version)
-
-
-# end class LooseVersion

File src/distutils2/versionpredicate.py

-"""Module for parsing and testing package version predicate strings.
-"""
-import re
-import distutils2.version
-import operator
-
-
-re_validPackage = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)")
-# (package) (rest)
-
-re_paren = re.compile(r"^\s*\((.*)\)\s*$") # (list) inside of parentheses
-re_splitComparison = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$")
-# (comp) (version)
-
-
-def splitUp(pred):
-    """Parse a single version comparison.
-
-    Return (comparison string, StrictVersion)
-    """
-    res = re_splitComparison.match(pred)
-    if not res:
-        raise ValueError("bad package restriction syntax: %r" % pred)
-    comp, verStr = res.groups()
-    return (comp, distutils2.version.StrictVersion(verStr))
-
-compmap = {"<": operator.lt, "<=": operator.le, "==": operator.eq,
-           ">": operator.gt, ">=": operator.ge, "!=": operator.ne}
-
-class VersionPredicate:
-    """Parse and test package version predicates.
-
-    >>> v = VersionPredicate('pyepat.abc (>1.0, <3333.3a1, !=1555.1b3)')
-
-    The `name` attribute provides the full dotted name that is given::
-
-    >>> v.name
-    'pyepat.abc'
-
-    The str() of a `VersionPredicate` provides a normalized
-    human-readable version of the expression::
-
-    >>> print v
-    pyepat.abc (> 1.0, < 3333.3a1, != 1555.1b3)
-
-    The `satisfied_by()` method can be used to determine with a given
-    version number is included in the set described by the version
-    restrictions::
-
-    >>> v.satisfied_by('1.1')
-    True
-    >>> v.satisfied_by('1.4')
-    True
-    >>> v.satisfied_by('1.0')
-    False
-    >>> v.satisfied_by('4444.4')
-    False
-    >>> v.satisfied_by('1555.1b3')
-    False
-
-    `VersionPredicate` is flexible in accepting extra whitespace::
-
-    >>> v = VersionPredicate(' pat( ==  0.1  )  ')
-    >>> v.name
-    'pat'
-    >>> v.satisfied_by('0.1')
-    True
-    >>> v.satisfied_by('0.2')
-    False
-
-    If any version numbers passed in do not conform to the
-    restrictions of `StrictVersion`, a `ValueError` is raised::
-
-    >>> v = VersionPredicate('p1.p2.p3.p4(>=1.0, <=1.3a1, !=1.2zb3)')
-    Traceback (most recent call last):
-      ...
-    ValueError: invalid version number '1.2zb3'
-
-    It the module or package name given does not conform to what's
-    allowed as a legal module or package name, `ValueError` is
-    raised::
-
-    >>> v = VersionPredicate('foo-bar')
-    Traceback (most recent call last):
-      ...
-    ValueError: expected parenthesized list: '-bar'
-
-    >>> v = VersionPredicate('foo bar (12.21)')
-    Traceback (most recent call last):
-      ...
-    ValueError: expected parenthesized list: 'bar (12.21)'
-
-    """
-
-    def __init__(self, versionPredicateStr):
-        """Parse a version predicate string.
-        """
-        # Fields:
-        #    name:  package name
-        #    pred:  list of (comparison string, StrictVersion)
-
-        versionPredicateStr = versionPredicateStr.strip()
-        if not versionPredicateStr:
-            raise ValueError("empty package restriction")
-        match = re_validPackage.match(versionPredicateStr)
-        if not match:
-            raise ValueError("bad package name in %r" % versionPredicateStr)
-        self.name, paren = match.groups()
-        paren = paren.strip()
-        if paren:
-            match = re_paren.match(paren)
-            if not match:
-                raise ValueError("expected parenthesized list: %r" % paren)
-            str = match.groups()[0]
-            self.pred = [splitUp(aPred) for aPred in str.split(",")]
-            if not self.pred:
-                raise ValueError("empty parenthesized list in %r"
-                                 % versionPredicateStr)
-        else:
-            self.pred = []
-
-    def __str__(self):
-        if self.pred:
-            seq = [cond + " " + str(ver) for cond, ver in self.pred]
-            return self.name + " (" + ", ".join(seq) + ")"
-        else:
-            return self.name
-
-    def satisfied_by(self, version):
-        """True if version is compatible with all the predicates in self.
-        The parameter version must be acceptable to the StrictVersion
-        constructor.  It may be either a string or StrictVersion.
-        """
-        for cond, ver in self.pred:
-            if not compmap[cond](version, ver):
-                return False
-        return True
-
-
-_provision_rx = None
-
-def split_provision(value):
-    """Return the name and optional version number of a provision.
-
-    The version number, if given, will be returned as a `StrictVersion`
-    instance, otherwise it will be `None`.
-
-    >>> split_provision('mypkg')
-    ('mypkg', None)
-    >>> split_provision(' mypkg( 1.2 ) ')
-    ('mypkg', StrictVersion ('1.2'))
-    """
-    global _provision_rx
-    if _provision_rx is None:
-        _provision_rx = re.compile(
-            "([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)(?:\s*\(\s*([^)\s]+)\s*\))?$")
-    value = value.strip()
-    m = _provision_rx.match(value)
-    if not m:
-        raise ValueError("illegal provides specification: %r" % value)
-    ver = m.group(2) or None
-    if ver:
-        ver = distutils2.version.StrictVersion(ver)
-    return m.group(1), ver