Refactor _auto_deps.py and __init__.py, adding more robust checking of dependency versions, and not trusting pkg_resources to get the versions right. refs #1258, #1287

This commit is contained in:
david-sarah
2011-01-20 21:36:10 -08:00
parent 06e0d13451
commit 29336a0916
3 changed files with 449 additions and 211 deletions

View File

@ -4,56 +4,12 @@ Decentralized storage grid.
community web site: U{http://tahoe-lafs.org/} community web site: U{http://tahoe-lafs.org/}
""" """
# We want to call require_auto_deps() before other imports, because the setuptools class PackagingError(EnvironmentError):
# docs claim that if a distribution is installed with --multi-version, it might not """
# be importable until after pkg_resources.require() has been called for it. We don't Raised when there is an error in packaging of Tahoe-LAFS or its
# have an example of this happening at this time. It is possible that require() isn't dependencies which makes it impossible to proceed safely.
# actually needed because we set __requires__ in the generated startup script, but """
# that would be an undocumented property of the setuptools implementation. pass
from allmydata import _auto_deps
_auto_deps.require_auto_deps()
# This is just to suppress DeprecationWarnings from nevow and twisted.
# See http://allmydata.org/trac/tahoe/ticket/859 and
# http://divmod.org/trac/ticket/2994 .
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning,
message="the sha module is deprecated; use the hashlib module instead",
append=True)
warnings.filterwarnings("ignore", category=DeprecationWarning,
message="object.__new__\(\) takes no parameters",
append=True)
warnings.filterwarnings("ignore", category=DeprecationWarning,
message="The popen2 module is deprecated. Use the subprocess module.",
append=True)
warnings.filterwarnings("ignore", category=DeprecationWarning,
message="the md5 module is deprecated; use hashlib instead",
append=True)
warnings.filterwarnings("ignore", category=DeprecationWarning,
message="twisted.web.error.NoResource is deprecated since Twisted 9.0. See twisted.web.resource.NoResource.",
append=True)
try:
import nevow
from twisted.persisted import sob
from twisted.python import filepath
hush_pyflakes = (nevow, sob, filepath)
del hush_pyflakes
finally:
warnings.filters.pop()
warnings.filters.pop()
warnings.filters.pop()
warnings.filters.pop()
# Don't pop the filter for the sha module warning because it is also generated
# by pycrypto (which we don't want to import unless needed).
# warnings.filters.pop()
# This warning is generated by twisted, PyRex, and possibly other packages,
# but can happen at any time, not only when they are imported. See
# http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1129 .
warnings.filterwarnings("ignore", category=DeprecationWarning,
message="BaseException.message has been deprecated as of Python 2.6",
append=True)
__version__ = "unknown" __version__ = "unknown"
try: try:
@ -175,88 +131,245 @@ def get_platform():
else: else:
return platform.platform() return platform.platform()
def get_package_versions_from_setuptools():
import pkg_resources
return dict([(p.project_name, (p.version, p.location)) for p in pkg_resources.require(__appname__)])
def package_dir(srcfile): from allmydata.util import verlib
return os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(srcfile)))) def normalized_version(verstr):
return verlib.NormalizedVersion(verlib.suggest_normalized_version(verstr))
def get_package_versions_and_locations(): def get_package_versions_and_locations():
# because there are a few dependencies that are outside setuptools's ken import warnings
# (Python and platform, and sqlite3 if you are on Python >= 2.5), and from _auto_deps import package_imports, deprecation_messages, deprecation_imports
# because setuptools might fail to find something even though import
# finds it: def package_dir(srcfile):
import OpenSSL, allmydata, foolscap.api, nevow, platform, pycryptopp, setuptools, simplejson, twisted, zfec, zope.interface return os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(srcfile))))
pysqlitever = None
pysqlitefile = None # pkg_resources.require returns the distribution that pkg_resources attempted to put
sqlitever = None # on sys.path, which can differ from the one that we actually import due to #1258,
# or any other bug that causes sys.path to be set up incorrectly. Therefore we
# must import the packages in order to check their versions and paths.
# This warning is generated by twisted, PyRex, and possibly other packages,
# but can happen at any time, not only when they are imported. See
# http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1129 .
warnings.filterwarnings("ignore", category=DeprecationWarning,
message="BaseException.message has been deprecated as of Python 2.6",
append=True)
# This is to suppress various DeprecationWarnings that occur when modules are imported.
# See http://allmydata.org/trac/tahoe/ticket/859 and http://divmod.org/trac/ticket/2994 .
for msg in deprecation_messages:
warnings.filterwarnings("ignore", category=DeprecationWarning, message=msg, append=True)
try: try:
import sqlite3 for modulename in deprecation_imports:
except ImportError: try:
try: __import__(modulename)
from pysqlite2 import dbapi2 except ImportError:
except ImportError: pass
pass finally:
for ign in deprecation_messages:
warnings.filters.pop()
packages = []
def get_version(module, attr):
return str(getattr(module, attr, 'unknown'))
for pkgname, modulename in [(__appname__, 'allmydata')] + package_imports:
if modulename:
try:
__import__(modulename)
module = sys.modules[modulename]
except ImportError:
packages.append((pkgname, (None, modulename)))
else:
if 'sqlite' in pkgname:
packages.append( (pkgname, (get_version(module, 'version'), package_dir(module.__file__))) )
packages.append( ('sqlite', (get_version(module, 'sqlite_version'), package_dir(module.__file__))) )
else:
packages.append( (pkgname, (get_version(module, '__version__'), package_dir(module.__file__))) )
elif pkgname == 'python':
packages.append( (pkgname, (platform.python_version(), sys.executable)) )
elif pkgname == 'platform':
packages.append( (pkgname, (get_platform(), None)) )
return packages
def check_requirement(req, vers_and_locs):
# TODO: check [] options
# We support only disjunctions of >= and ==
reqlist = req.split(',')
name = reqlist[0].split('>=')[0].split('==')[0].strip(' ').split('[')[0]
if name not in vers_and_locs:
raise PackagingError("no version info for %s" % (name,))
if req.strip(' ') == name:
return
(actual, location) = vers_and_locs[name]
if actual is None:
raise ImportError("could not import %r for requirement %r" % (location, req))
if actual == 'unknown':
return
actualver = normalized_version(actual)
for r in reqlist:
s = r.split('>=')
if len(s) == 2:
required = s[1].strip(' ')
if actualver >= normalized_version(required):
return # minimum requirement met
else: else:
pysqlitever = dbapi2.version s = r.split('==')
pysqlitefile = package_dir(dbapi2.__file__) if len(s) == 2:
sqlitever = dbapi2.sqlite_version required = s[1].strip(' ')
else: if actualver == normalized_version(required):
pysqlitever = sqlite3.version return # exact requirement met
pysqlitefile = package_dir(sqlite3.__file__) else:
sqlitever = sqlite3.sqlite_version raise PackagingError("no version info or could not understand requirement %r" % (req,))
d1 = { msg = ("We require %s, but could only find version %s.\n" % (req, actual))
'pyOpenSSL': (OpenSSL.__version__, package_dir(OpenSSL.__file__)), if location and location != 'unknown':
__appname__: (allmydata.__version__, package_dir(allmydata.__file__)), msg += "The version we found is from %r.\n" % (location,)
'foolscap': (foolscap.api.__version__, package_dir(foolscap.__file__)), msg += ("To resolve this problem, uninstall that version, either using your\n"
'Nevow': (nevow.__version__, package_dir(nevow.__file__)), "operating system's package manager or by moving aside the directory.")
'pycryptopp': (pycryptopp.__version__, package_dir(pycryptopp.__file__)), raise PackagingError(msg)
'setuptools': (setuptools.__version__, package_dir(setuptools.__file__)),
'simplejson': (simplejson.__version__, package_dir(simplejson.__file__)),
'pysqlite': (pysqlitever, pysqlitefile), _vers_and_locs_list = get_package_versions_and_locations()
'sqlite': (sqlitever, 'unknown'),
'zope.interface': ('unknown', package_dir(zope.interface.__file__)),
'Twisted': (twisted.__version__, package_dir(twisted.__file__)), def cross_check_pkg_resources_versus_import():
'zfec': (zfec.__version__, package_dir(zfec.__file__)), """This function returns a list of errors due to any failed cross-checks."""
'python': (platform.python_version(), sys.executable),
'platform': (get_platform(), None),
}
# But we prefer to get all the dependencies as known by setuptools:
import pkg_resources import pkg_resources
try: from _auto_deps import install_requires
d2 = get_package_versions_from_setuptools()
except pkg_resources.DistributionNotFound: errors = []
# See docstring in _auto_deps.require_auto_deps() to explain why it makes sense to ignore this exception. not_pkg_resourceable = set(['sqlite', 'sqlite3', 'python', 'platform', __appname__.lower()])
pass not_import_versionable = set(['zope.interface', 'mock', 'pyasn1'])
else: ignorable = set(['argparse', 'pyutil', 'zbase32'])
d1.update(d2)
pkg_resources_vers_and_locs = dict([(p.project_name.lower(), (str(p.version), p.location))
for p in pkg_resources.require(install_requires)])
for name, (imp_ver, imp_loc) in _vers_and_locs_list:
name = name.lower()
if name not in not_pkg_resourceable:
if name not in pkg_resources_vers_and_locs:
errors.append("Warning: dependency %s (version %s imported from %r) was not found by pkg_resources."
% (name, imp_ver, imp_loc))
pr_ver, pr_loc = pkg_resources_vers_and_locs[name]
try:
pr_normver = normalized_version(pr_ver)
except Exception, e:
errors.append("Warning: version number %s found for dependency %s by pkg_resources could not be parsed. "
"The version found by import was %s from %r. "
"pkg_resources thought it should be found at %r. "
"The exception was %s: %s"
% (pr_ver, name, imp_ver, imp_loc, pr_loc, e.__class__.name, e))
else:
if imp_ver == 'unknown':
if name not in not_import_versionable:
errors.append("Warning: unexpectedly could not find a version number for dependency %s imported from %r. "
"pkg_resources thought it should be version %s at %r."
% (name, imp_loc, pr_ver, pr_loc))
else:
try:
imp_normver = normalized_version(imp_ver)
except Exception, e:
errors.append("Warning: version number %s found for dependency %s (imported from %r) could not be parsed. "
"pkg_resources thought it should be version %s at %r. "
"The exception was %s: %s"
% (imp_ver, name, imp_loc, pr_ver, pr_loc, e.__class__.name, e))
else:
if pr_ver == 'unknown' or (pr_normver != imp_normver):
if not os.path.normpath(os.path.realpath(pr_loc)) == os.path.normpath(os.path.realpath(imp_loc)):
errors.append("Warning: dependency %s found to have version number %s (normalized to %s, from %r) "
"by pkg_resources, but version %s (normalized to %s, from %r) by import."
% (name, pr_ver, str(pr_normver), pr_loc, imp_ver, str(imp_normver), imp_loc))
imported_packages = set([p.lower() for (p, _) in _vers_and_locs_list])
for pr_name, (pr_ver, pr_loc) in pkg_resources_vers_and_locs.iteritems():
if pr_name not in imported_packages and pr_name not in ignorable:
errors.append("Warning: dependency %s (version %s) found by pkg_resources not found by import."
% (pr_name, pr_ver))
return errors
def get_error_string(errors):
from allmydata._auto_deps import install_requires
return ("\n%s\n\n"
"For debugging purposes, the PYTHONPATH was\n"
" %r\n"
"install_requires was\n"
" %r\n"
"sys.path after importing pkg_resources was\n"
" %s\n"
% ("\n".join(errors), os.environ.get('PYTHONPATH'), install_requires, (os.pathsep+"\n ").join(sys.path)) )
def check_all_requirements():
"""This function returns a list of errors due to any failed checks."""
from allmydata._auto_deps import install_requires
errors = []
# we require 2.4.4 on non-UCS-2, non-Redhat builds to avoid <http://www.python.org/news/security/PSF-2006-001/>
# we require 2.4.3 on non-UCS-2 Redhat, because 2.4.3 is common on Redhat-based distros and will have patched the above bug
# we require at least 2.4.2 in any case to avoid a bug in the base64 module: <http://bugs.python.org/issue1171487>
if sys.maxunicode == 65535:
if sys.version_info < (2, 4, 2) or sys.version_info[0] > 2:
errors.append("Tahoe-LAFS current requires Python v2.4.2 or greater "
"for a UCS-2 build (but less than v3), not %r" %
(sys.version_info,))
elif platform.platform().lower().find('redhat') >= 0:
if sys.version_info < (2, 4, 3) or sys.version_info[0] > 2:
errors.append("Tahoe-LAFS current requires Python v2.4.3 or greater "
"on Redhat-based distributions (but less than v3), not %r" %
(sys.version_info,))
else:
if sys.version_info < (2, 4, 4) or sys.version_info[0] > 2:
errors.append("Tahoe-LAFS current requires Python v2.4.4 or greater "
"for a non-UCS-2 build (but less than v3), not %r" %
(sys.version_info,))
vers_and_locs = dict(_vers_and_locs_list)
for requirement in install_requires:
try:
check_requirement(requirement, vers_and_locs)
except Exception, e:
errors.append("%s: %s" % (e.__class__.__name__, e))
if errors:
raise PackagingError(get_error_string(errors))
check_all_requirements()
return d1
def get_package_versions(): def get_package_versions():
return dict([(k, v) for k, (v, l) in get_package_versions_and_locations().iteritems()]) return dict([(k, v) for k, (v, l) in _vers_and_locs_list])
def get_package_locations(): def get_package_locations():
return dict([(k, l) for k, (v, l) in get_package_versions_and_locations().iteritems()]) return dict([(k, l) for k, (v, l) in _vers_and_locs_list])
def get_package_versions_string(show_paths=False): def get_package_versions_string(show_paths=False):
vers_and_locs = get_package_versions_and_locations()
res = [] res = []
for p in [__appname__, "foolscap", "pycryptopp", "zfec", "Twisted", "Nevow", "zope.interface", "python", "platform"]: for p, (v, loc) in _vers_and_locs_list:
(ver, loc) = vers_and_locs.get(p, ('UNKNOWN', 'UNKNOWN'))
info = str(p) + ": " + str(ver)
if show_paths:
info = info + " (%s)" % str(loc)
res.append(info)
if vers_and_locs.has_key(p):
del vers_and_locs[p]
for p, (v, loc) in vers_and_locs.iteritems():
info = str(p) + ": " + str(v) info = str(p) + ": " + str(v)
if show_paths: if show_paths:
info = info + " (%s)" % str(loc) info = info + " (%s)" % str(loc)
res.append(info) res.append(info)
return ', '.join(res)
output = ",\n".join(res) + "\n"
if not hasattr(sys, 'frozen'):
errors = cross_check_pkg_resources_versus_import()
if errors:
output += get_error_string(errors)
return output

View File

@ -1,111 +1,105 @@
# Note: do not import any module from Tahoe-LAFS itself in this # Note: please minimize imports in this file. In particular, do not import
# file. Also please avoid importing modules from other packages than # any module from Tahoe-LAFS or its dependencies, and do not import any
# the Python Standard Library if at all possible (exception: we rely # modules at all at global level. That includes setuptools and pkg_resources.
# on importing pkg_resources, which is provided by setuptools, # It is ok to import modules from the Python Standard Library if they are
# zetuptoolz, distribute, and perhaps in the future distutils2, for # always available, or the import is protected by try...except ImportError.
# the require_auto_deps() function.)
install_requires=[ install_requires = [
# we require newer versions of setuptools (actually "zfec >= 1.1.0",
# zetuptoolz) to build, but can handle older versions to run
"setuptools >= 0.6c6",
"zfec >= 1.1.0", # Feisty has simplejson 1.4
"simplejson >= 1.4",
# Feisty has simplejson 1.4 "zope.interface",
"simplejson >= 1.4",
"zope.interface", "Twisted >= 2.4.0",
"Twisted >= 2.4.0",
# foolscap < 0.5.1 had a performance bug which spent # foolscap < 0.5.1 had a performance bug which spent
# O(N**2) CPU for transferring large mutable files # O(N**2) CPU for transferring large mutable files
# of size N. # of size N.
# foolscap < 0.6 is incompatible with Twisted 10.2.0. # foolscap < 0.6 is incompatible with Twisted 10.2.0.
# foolscap 0.6.1 quiets a DeprecationWarning. # foolscap 0.6.1 quiets a DeprecationWarning.
"foolscap[secure_connections] >= 0.6.1", "foolscap[secure_connections] >= 0.6.1",
"Nevow >= 0.6.0",
# Needed for SFTP. pyasn1 is needed by twisted.conch in Twisted >= 9.0. "Nevow >= 0.6.0",
# pycrypto 2.2 doesn't work due to https://bugs.launchpad.net/pycrypto/+bug/620253
"pycrypto == 2.0.1, == 2.1, >= 2.3",
"pyasn1 >= 0.0.8a",
# http://www.voidspace.org.uk/python/mock/ # Needed for SFTP. pyasn1 is needed by twisted.conch in Twisted >= 9.0.
"mock", # pycrypto 2.2 doesn't work due to https://bugs.launchpad.net/pycrypto/+bug/620253
"pycrypto == 2.0.1, == 2.1.0, >= 2.3",
"pyasn1 >= 0.0.8a",
# Will be needed to test web apps, but not yet. See #1001. # http://www.voidspace.org.uk/python/mock/
#"windmill >= 1.3", "mock",
]
import platform # Will be needed to test web apps, but not yet. See #1001.
if platform.machine().lower() in ['i386', 'x86_64', 'amd64', 'x86', '']: #"windmill >= 1.3",
# pycryptopp v0.5.20 fixes bugs in SHA-256 and AES on x86 or amd64 ]
# (from Crypto++ revisions 470, 471, 480, 492). The '' is there
# in case platform.machine is broken and this is actually an x86
# or amd64 machine.
install_requires.append("pycryptopp >= 0.5.20")
else:
# pycryptopp v0.5.13 had a new bundled version of Crypto++
# (v5.6.0) and a new bundled version of setuptools (although that
# shouldn't make any different to users of pycryptopp).
install_requires.append("pycryptopp >= 0.5.14")
# Includes some indirect dependencies, but does not include allmydata.
# These are in the order they should be listed by --version, etc.
package_imports = [
# package name module name
('foolscap', 'foolscap'),
('pycryptopp', 'pycryptopp'),
('zfec', 'zfec'),
('Twisted', 'twisted'),
('Nevow', 'nevow'),
('zope.interface', 'zope.interface'),
('python', None),
('platform', None),
('pyOpenSSL', 'OpenSSL'),
('simplejson', 'simplejson'),
('pycrypto', 'Crypto'),
('pyasn1', 'pyasn1'),
('mock', 'mock'),
]
# Sqlite comes built into Python >= 2.5, and is provided by the "pysqlite" def require_more():
# distribution for Python 2.4. import platform, sys
import sys
if sys.version_info < (2, 5):
# pysqlite v2.0.5 was shipped in Ubuntu 6.06 LTS "dapper" and Nexenta NCP 1.
install_requires.append("pysqlite >= 2.0.5")
if hasattr(sys, 'frozen'): # for py2exe if platform.machine().lower() in ['i386', 'x86_64', 'amd64', 'x86', '']:
install_requires=[] # pycryptopp v0.5.20 fixes bugs in SHA-256 and AES on x86 or amd64
del sys # clean up namespace # (from Crypto++ revisions 470, 471, 480, 492). The '' is there
# in case platform.machine is broken and this is actually an x86
def require_python_version(): # or amd64 machine.
import sys, platform install_requires.append("pycryptopp >= 0.5.20")
# we require 2.4.4 on non-UCS-2, non-Redhat builds to avoid <http://www.python.org/news/security/PSF-2006-001/>
# we require 2.4.3 on non-UCS-2 Redhat, because 2.4.3 is common on Redhat-based distros and will have patched the above bug
# we require at least 2.4.2 in any case to avoid a bug in the base64 module: <http://bugs.python.org/issue1171487>
if sys.maxunicode == 65535:
if sys.version_info < (2, 4, 2) or sys.version_info[0] > 2:
raise NotImplementedError("Tahoe-LAFS current requires Python v2.4.2 or greater "
"for a UCS-2 build (but less than v3), not %r" %
(sys.version_info,))
elif platform.platform().lower().find('redhat') >= 0:
if sys.version_info < (2, 4, 3) or sys.version_info[0] > 2:
raise NotImplementedError("Tahoe-LAFS current requires Python v2.4.3 or greater "
"on Redhat-based distributions (but less than v3), not %r" %
(sys.version_info,))
else: else:
if sys.version_info < (2, 4, 4) or sys.version_info[0] > 2: # pycryptopp v0.5.13 had a new bundled version of Crypto++
raise NotImplementedError("Tahoe-LAFS current requires Python v2.4.4 or greater " # (v5.6.0) and a new bundled version of setuptools (although that
"for a non-UCS-2 build (but less than v3), not %r" % # shouldn't make any difference to users of pycryptopp).
(sys.version_info,)) install_requires.append("pycryptopp >= 0.5.14")
def require_auto_deps(): # Sqlite comes built into Python >= 2.5, and is provided by the "pysqlite"
""" # distribution for Python 2.4.
The purpose of this function is to raise a pkg_resources exception if any of the try:
requirements can't be imported. This is just to give earlier and more explicit error import sqlite3
messages, as opposed to waiting until the source code tries to import some module from one sqlite3 # hush pyflakes
of these packages and gets an ImportError. This function gets called from package_imports.append(('sqlite3', 'sqlite3'))
src/allmydata/__init__.py . except ImportError:
""" # pysqlite v2.0.5 was shipped in Ubuntu 6.06 LTS "dapper" and Nexenta NCP 1.
require_python_version() install_requires.append("pysqlite >= 2.0.5")
package_imports.append(('pysqlite', 'pysqlite.dbapi2'))
import pkg_resources if not hasattr(sys, 'frozen'):
for requirement in install_requires: # we require newer versions of setuptools (actually
try: # zetuptoolz) to build, but can handle older versions to run
pkg_resources.require(requirement) install_requires.append("setuptools >= 0.6c6")
except pkg_resources.DistributionNotFound: package_imports.append(('setuptools', 'setuptools'))
# there is no .egg-info present for this requirement, which
# either means that it isn't installed, or it is installed in a require_more()
# way that pkg_resources can't find it (but regular python
# might). There are several older Linux distributions which deprecation_messages = [
# provide our dependencies just fine, but they don't ship "the sha module is deprecated; use the hashlib module instead",
# .egg-info files. Note that if there *is* an .egg-info file, "object.__new__\(\) takes no parameters",
# but it shows a too-old version, then we'll get a "The popen2 module is deprecated. Use the subprocess module.",
# VersionConflict error instead of DistributionNotFound. "the md5 module is deprecated; use hashlib instead",
pass "twisted.web.error.NoResource is deprecated since Twisted 9.0. See twisted.web.resource.NoResource.",
"the sets module is deprecated",
]
deprecation_imports = [
'nevow',
'twisted.persisted.sob',
'twisted.python.filepath',
'Crypto.Hash.SHA',
]

View File

@ -0,0 +1,131 @@
from twisted.trial import unittest
from allmydata import check_requirement, PackagingError
from allmydata.util.verlib import NormalizedVersion as V, \
IrrationalVersionError, \
suggest_normalized_version as suggest
class CheckRequirement(unittest.TestCase):
def test_check_requirement(self):
check_requirement("setuptools >= 0.6c6", {"setuptools": ("0.6", "")})
check_requirement("pycrypto == 2.0.1, == 2.1, >= 2.3", {"pycrypto": ("2.1.0", "")})
check_requirement("pycrypto == 2.0.1, == 2.1, >= 2.3", {"pycrypto": ("2.4.0", "")})
check_requirement("zope.interface", {"zope.interface": ("unknown", "")})
check_requirement("mock", {"mock": ("0.6.0", "")})
check_requirement("foo >= 1.0", {"foo": ("1.0", ""), "bar": ("2.0", "")})
check_requirement("foolscap[secure_connections] >= 0.6.0", {"foolscap": ("0.7.0", "")})
self.failUnlessRaises(PackagingError, check_requirement,
"foolscap[secure_connections] >= 0.6.0", {"foolscap": ("0.5.1", "")})
self.failUnlessRaises(PackagingError, check_requirement,
"pycrypto == 2.0.1, == 2.1, >= 2.3", {"pycrypto": ("2.2.0", "")})
self.failUnlessRaises(PackagingError, check_requirement,
"foo >= 1.0", {})
# based on https://bitbucket.org/tarek/distutilsversion/src/17df9a7d96ef/test_verlib.py
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.failUnlessEqual(str(v), s)
def test_from_parts(self):
for v, s in self.versions:
parts = v.parts
v2 = V.from_parts(*parts)
self.failUnlessEqual(v, v2)
self.failUnlessEqual(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.failUnlessRaises(IrrationalVersionError, V, s)
def test_comparison(self):
self.failUnlessRaises(TypeError, lambda: V('1.2.0') == '1.2')
self.failUnlessEqual(V('1.2.0'), V('1.2'))
self.failIfEqual(V('1.2.0'), V('1.2.3'))
self.failUnless(V('1.2.0') < V('1.2.3'))
self.failUnless(V('1.0') > V('1.0b2'))
self.failUnless(V('1.0') > V('1.0c2') > V('1.0c1') > V('1.0b2') > V('1.0b1')
> V('1.0a2') > V('1.0a1'))
self.failUnless(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'))
self.failUnless(V('1.0') < V('1.0.post456.dev623'))
self.failUnless(V('1.0.post456.dev623') < V('1.0.post456') < V('1.0.post1234'))
self.failUnless(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')
< V('1.0c2.dev456')
< V('1.0c2')
< 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'))
def test_suggest_normalized_version(self):
self.failUnlessEqual(suggest('1.0'), '1.0')
self.failUnlessEqual(suggest('1.0-alpha1'), '1.0a1')
self.failUnlessEqual(suggest('1.0c2'), '1.0c2')
self.failUnlessEqual(suggest('walla walla washington'), None)
self.failUnlessEqual(suggest('2.4c1'), '2.4c1')
# from setuptools
self.failUnlessEqual(suggest('0.4a1.r10'), '0.4a1.post10')
self.failUnlessEqual(suggest('0.7a1dev-r66608'), '0.7a1.dev66608')
self.failUnlessEqual(suggest('0.6a9.dev-r41475'), '0.6a9.dev41475')
self.failUnlessEqual(suggest('2.4preview1'), '2.4c1')
self.failUnlessEqual(suggest('2.4pre1') , '2.4c1')
self.failUnlessEqual(suggest('2.1-rc2'), '2.1c2')
# from pypi
self.failUnlessEqual(suggest('0.1dev'), '0.1.dev0')
self.failUnlessEqual(suggest('0.1.dev'), '0.1.dev0')
# we want to be able to parse Twisted
# development versions are like post releases in Twisted
self.failUnlessEqual(suggest('9.0.0+r2363'), '9.0.0.post2363')
# pre-releases are using markers like "pre1"
self.failUnlessEqual(suggest('9.0.0pre1'), '9.0.0c1')
# we want to be able to parse Tcl-TK
# they us "p1" "p2" for post releases
self.failUnlessEqual(suggest('1.4p1'), '1.4.post1')
# from darcsver
self.failUnlessEqual(suggest('1.8.1-r4956'), '1.8.1.post4956')
# zetuptoolz
self.failUnlessEqual(suggest('0.6c16dev3'), '0.6c16.dev3')