summaryrefslogtreecommitdiff
path: root/src/distutils2
diff options
context:
space:
mode:
authorTarek Ziad? <tarek@ziade.org>2010-02-25 01:03:36 -0500
committerTarek Ziad? <tarek@ziade.org>2010-02-25 01:03:36 -0500
commite313d42f46446c76a3e60267e9df29e51ff92722 (patch)
treeb992406719a94b423f358fe4aa852bbf2142f413 /src/distutils2
parent53bb51a9472ec16e8dce38db9097969d1e5b9141 (diff)
downloaddisutils2-e313d42f46446c76a3e60267e9df29e51ff92722.tar.gz
includeed the new version tool -- ready to be used in DistributionMetadata
Diffstat (limited to 'src/distutils2')
-rw-r--r--src/distutils2/tests/LONG_DESC.txt44
-rw-r--r--src/distutils2/tests/test_pypi_versions.py120
-rw-r--r--src/distutils2/tests/test_util.py1
-rw-r--r--src/distutils2/tests/test_version.py195
-rw-r--r--src/distutils2/tests/test_versionpredicate.py9
-rw-r--r--src/distutils2/util.py5
-rw-r--r--src/distutils2/version.py632
-rw-r--r--src/distutils2/versionpredicate.py164
8 files changed, 637 insertions, 533 deletions
diff --git a/src/distutils2/tests/LONG_DESC.txt b/src/distutils2/tests/LONG_DESC.txt
new file mode 100644
index 0000000..2b4358a
--- /dev/null
+++ b/src/distutils2/tests/LONG_DESC.txt
@@ -0,0 +1,44 @@
+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
diff --git a/src/distutils2/tests/test_pypi_versions.py b/src/distutils2/tests/test_pypi_versions.py
new file mode 100644
index 0000000..eb5634c
--- /dev/null
+++ b/src/distutils2/tests/test_pypi_versions.py
@@ -0,0 +1,120 @@
+#
+## 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())
+
diff --git a/src/distutils2/tests/test_util.py b/src/distutils2/tests/test_util.py
index e7e77d6..b146b15 100644
--- a/src/distutils2/tests/test_util.py
+++ b/src/distutils2/tests/test_util.py
@@ -14,7 +14,6 @@ from distutils2.util import (convert_path, change_root,
byte_compile)
from distutils2 import util
from distutils2.tests import support
-from distutils2.version import LooseVersion
class FakePopen(object):
test_class = None
diff --git a/src/distutils2/tests/test_version.py b/src/distutils2/tests/test_version.py
index 41278bd..6a57979 100644
--- a/src/distutils2/tests/test_version.py
+++ b/src/distutils2/tests/test_version.py
@@ -1,70 +1,135 @@
"""Tests for distutils.version."""
-import unittest2
-from distutils2.version import LooseVersion
-from distutils2.version import StrictVersion
-
-class VersionTestCase(unittest2.TestCase):
-
- 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')
-
- version = StrictVersion('1.2.0')
- self.assertEquals(str(version), '1.2')
-
- 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))
-
- 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))
-
-
- 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 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))
+import unittest
+import doctest
+import os
+
+from distutils2.version import NormalizedVersion as V
+from distutils2.version import IrrationalVersionError
+from distutils2.version import suggest_normalized_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.post456.dev623'), '1.0.post456.dev623'))
+
+ 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', '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")
+
diff --git a/src/distutils2/tests/test_versionpredicate.py b/src/distutils2/tests/test_versionpredicate.py
deleted file mode 100644
index 50c59fa..0000000
--- a/src/distutils2/tests/test_versionpredicate.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""Tests harness for distutils2.versionpredicate.
-
-"""
-
-import distutils2.versionpredicate
-import doctest
-
-def test_suite():
- return doctest.DocTestSuite(distutils2.versionpredicate)
diff --git a/src/distutils2/util.py b/src/distutils2/util.py
index 5110697..6353814 100644
--- a/src/distutils2/util.py
+++ b/src/distutils2/util.py
@@ -11,7 +11,6 @@ import sys, os, string, re
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
@@ -484,13 +483,13 @@ def _find_exe_version(cmd, pattern=_RE_VERSION):
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()
diff --git a/src/distutils2/version.py b/src/distutils2/version.py
index 5efe144..3704260 100644
--- a/src/distutils2/version.py
+++ b/src/distutils2/version.py
@@ -1,299 +1,349 @@
-#
-# 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 $
-#
-
-"""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.
-
-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)
-"""
-
-import string, re
-from types import StringType
-
-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.
- """
-
- def __init__ (self, vstring=None):
- if vstring:
- self.parse(vstring)
-
- def __repr__ (self):
- return "%s ('%s')" % (self.__class__.__name__, str(self))
-
-
-# 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)
-
-
-class StrictVersion (Version):
-
- """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.
-
- The following are valid version numbers (shown in the order that
- would be obtained by sorting according to the supplied cmp function):
-
- 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
-
- The following are examples of invalid version numbers:
-
- 1
- 2.7.2.2
- 1.3.a4
- 1.3pl1
- 1.3c4
-
- The rationale for this version numbering system will be explained
- in the distutils documentation.
- """
+import sys
+import re
- version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
- re.VERBOSE)
+class IrrationalVersionError(Exception):
+ """This is an irrational version."""
+ pass
+class HugeMajorVersionNumError(IrrationalVersionError):
+ """An irrational version because the major version number is huge
+ (often because a year or date was used).
- def parse (self, vstring):
- match = self.version_re.match(vstring)
+ See `error_on_huge_major_num` option in `NormalizedVersion` 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. 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.
+
+ @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)
+
+ @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 ValueError, "invalid version number '%s'" % vstring
-
- (major, minor, patch, prerelease, prerelease_num) = \
- match.group(1, 2, 4, 5, 6)
-
- if patch:
- self.version = tuple(map(string.atoi, [major, minor, patch]))
+ raise IrrationalVersionError(s)
+
+ 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))
+
+ # 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:
- self.version = tuple(map(string.atoi, [major, minor]) + [0])
-
- if prerelease:
- self.prerelease = (prerelease[0], string.atoi(prerelease_num))
- else:
- self.prerelease = None
-
-
- def __str__ (self):
-
- if self.version[2] == 0:
- vstring = string.join(map(str, self.version[0:2]), '.')
+ parts.append(FINAL_MARKER)
+
+ # 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:
- vstring = string.join(map(str, self.version), '.')
-
- if self.prerelease:
- vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
-
- return vstring
-
-
- def __cmp__ (self, other):
- if isinstance(other, StringType):
- other = StrictVersion(other)
-
- compare = cmp(self.version, other.version)
- if (compare == 0): # have to compare prerelease
-
- # 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!
-
- 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)
-
- else: # numeric versions don't match --
- return compare # prerelease stuff doesn't matter
-
-
-# end class StrictVersion
-
-
-# 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):
-
- """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:
-
- 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
-
- 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").
+ 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))
+
+ 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):
+ return self.parts_to_str(self.parts)
+
+ @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
+
+ 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
+
+ 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)
+
+ # 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)
+
+ # 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)
+
+ # 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)
+
+ # 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)
+
+ # the 'dev-rNNN' tag is a dev tag
+ rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
+
+ # 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)
+
+ # a terminal "dev" can be changed into ".dev0"
+ rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
+
+ # a terminal "final" or "stable" can be removed
+ rs = re.sub(r"(final|stable)$", "", 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.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)
+
+ # 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)
+
+
+ # 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
+
+
+_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*$")
+
+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)
+
+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()
+
+ 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
diff --git a/src/distutils2/versionpredicate.py b/src/distutils2/versionpredicate.py
deleted file mode 100644
index 6c8b361..0000000
--- a/src/distutils2/versionpredicate.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""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