From: david-sarah Date: Fri, 21 Jan 2011 05:36:10 +0000 (-0800) Subject: Refactor _auto_deps.py and __init__.py, adding more robust checking of dependency... X-Git-Tag: allmydata-tahoe-1.8.2b1~26 X-Git-Url: https://git.rkrishnan.org/%5B/%5D%20/uri/somewhere?a=commitdiff_plain;h=29336a09163cd3d5b75a284bfb817ad435df3b5c;p=tahoe-lafs%2Ftahoe-lafs.git 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 --- diff --git a/src/allmydata/__init__.py b/src/allmydata/__init__.py index 886fd4e0..b0078d08 100644 --- a/src/allmydata/__init__.py +++ b/src/allmydata/__init__.py @@ -4,56 +4,12 @@ Decentralized storage grid. community web site: U{http://tahoe-lafs.org/} """ -# We want to call require_auto_deps() before other imports, because the setuptools -# 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 -# have an example of this happening at this time. It is possible that require() isn't -# actually needed because we set __requires__ in the generated startup script, but -# that would be an undocumented property of the setuptools implementation. - -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) +class PackagingError(EnvironmentError): + """ + Raised when there is an error in packaging of Tahoe-LAFS or its + dependencies which makes it impossible to proceed safely. + """ + pass __version__ = "unknown" try: @@ -175,88 +131,245 @@ def get_platform(): else: 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): - return os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(srcfile)))) +from allmydata.util import verlib +def normalized_version(verstr): + return verlib.NormalizedVersion(verlib.suggest_normalized_version(verstr)) + def get_package_versions_and_locations(): - # because there are a few dependencies that are outside setuptools's ken - # (Python and platform, and sqlite3 if you are on Python >= 2.5), and - # because setuptools might fail to find something even though import - # finds it: - import OpenSSL, allmydata, foolscap.api, nevow, platform, pycryptopp, setuptools, simplejson, twisted, zfec, zope.interface - pysqlitever = None - pysqlitefile = None - sqlitever = None + import warnings + from _auto_deps import package_imports, deprecation_messages, deprecation_imports + + def package_dir(srcfile): + return os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(srcfile)))) + + # pkg_resources.require returns the distribution that pkg_resources attempted to put + # 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: - import sqlite3 - except ImportError: - try: - from pysqlite2 import dbapi2 - except ImportError: - pass + for modulename in deprecation_imports: + try: + __import__(modulename) + except ImportError: + 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: - pysqlitever = dbapi2.version - pysqlitefile = package_dir(dbapi2.__file__) - sqlitever = dbapi2.sqlite_version - else: - pysqlitever = sqlite3.version - pysqlitefile = package_dir(sqlite3.__file__) - sqlitever = sqlite3.sqlite_version - - d1 = { - 'pyOpenSSL': (OpenSSL.__version__, package_dir(OpenSSL.__file__)), - __appname__: (allmydata.__version__, package_dir(allmydata.__file__)), - 'foolscap': (foolscap.api.__version__, package_dir(foolscap.__file__)), - 'Nevow': (nevow.__version__, package_dir(nevow.__file__)), - 'pycryptopp': (pycryptopp.__version__, package_dir(pycryptopp.__file__)), - 'setuptools': (setuptools.__version__, package_dir(setuptools.__file__)), - 'simplejson': (simplejson.__version__, package_dir(simplejson.__file__)), - 'pysqlite': (pysqlitever, pysqlitefile), - 'sqlite': (sqlitever, 'unknown'), - 'zope.interface': ('unknown', package_dir(zope.interface.__file__)), - 'Twisted': (twisted.__version__, package_dir(twisted.__file__)), - 'zfec': (zfec.__version__, package_dir(zfec.__file__)), - 'python': (platform.python_version(), sys.executable), - 'platform': (get_platform(), None), - } - - # But we prefer to get all the dependencies as known by setuptools: + s = r.split('==') + if len(s) == 2: + required = s[1].strip(' ') + if actualver == normalized_version(required): + return # exact requirement met + else: + raise PackagingError("no version info or could not understand requirement %r" % (req,)) + + msg = ("We require %s, but could only find version %s.\n" % (req, actual)) + if location and location != 'unknown': + msg += "The version we found is from %r.\n" % (location,) + msg += ("To resolve this problem, uninstall that version, either using your\n" + "operating system's package manager or by moving aside the directory.") + raise PackagingError(msg) + + +_vers_and_locs_list = get_package_versions_and_locations() + + +def cross_check_pkg_resources_versus_import(): + """This function returns a list of errors due to any failed cross-checks.""" + import pkg_resources - try: - d2 = get_package_versions_from_setuptools() - except pkg_resources.DistributionNotFound: - # See docstring in _auto_deps.require_auto_deps() to explain why it makes sense to ignore this exception. - pass + from _auto_deps import install_requires + + errors = [] + not_pkg_resourceable = set(['sqlite', 'sqlite3', 'python', 'platform', __appname__.lower()]) + not_import_versionable = set(['zope.interface', 'mock', 'pyasn1']) + ignorable = set(['argparse', 'pyutil', 'zbase32']) + + 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 + # 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: + 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: - d1.update(d2) + 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(): - 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(): - 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): - vers_and_locs = get_package_versions_and_locations() res = [] - for p in [__appname__, "foolscap", "pycryptopp", "zfec", "Twisted", "Nevow", "zope.interface", "python", "platform"]: - (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(): + for p, (v, loc) in _vers_and_locs_list: info = str(p) + ": " + str(v) if show_paths: info = info + " (%s)" % str(loc) 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 diff --git a/src/allmydata/_auto_deps.py b/src/allmydata/_auto_deps.py index b784b8bc..f1fbb347 100644 --- a/src/allmydata/_auto_deps.py +++ b/src/allmydata/_auto_deps.py @@ -1,111 +1,105 @@ -# Note: do not import any module from Tahoe-LAFS itself in this -# file. Also please avoid importing modules from other packages than -# the Python Standard Library if at all possible (exception: we rely -# on importing pkg_resources, which is provided by setuptools, -# zetuptoolz, distribute, and perhaps in the future distutils2, for -# the require_auto_deps() function.) - -install_requires=[ - # we require newer versions of setuptools (actually - # 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", - - "zope.interface", - "Twisted >= 2.4.0", - - # foolscap < 0.5.1 had a performance bug which spent - # O(N**2) CPU for transferring large mutable files - # of size N. - # foolscap < 0.6 is incompatible with Twisted 10.2.0. - # foolscap 0.6.1 quiets a DeprecationWarning. - "foolscap[secure_connections] >= 0.6.1", - "Nevow >= 0.6.0", - - # Needed for SFTP. pyasn1 is needed by twisted.conch in Twisted >= 9.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/ - "mock", - - # Will be needed to test web apps, but not yet. See #1001. - #"windmill >= 1.3", - ] - -import platform -if platform.machine().lower() in ['i386', 'x86_64', 'amd64', 'x86', '']: - # 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") - - -# Sqlite comes built into Python >= 2.5, and is provided by the "pysqlite" -# distribution for Python 2.4. -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 - install_requires=[] -del sys # clean up namespace - -def require_python_version(): - import sys, platform - - # we require 2.4.4 on non-UCS-2, non-Redhat builds to avoid - # 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: - 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,)) +# Note: please minimize imports in this file. In particular, do not import +# any module from Tahoe-LAFS or its dependencies, and do not import any +# modules at all at global level. That includes setuptools and pkg_resources. +# It is ok to import modules from the Python Standard Library if they are +# always available, or the import is protected by try...except ImportError. + +install_requires = [ + "zfec >= 1.1.0", + + # Feisty has simplejson 1.4 + "simplejson >= 1.4", + + "zope.interface", + + "Twisted >= 2.4.0", + + # foolscap < 0.5.1 had a performance bug which spent + # O(N**2) CPU for transferring large mutable files + # of size N. + # foolscap < 0.6 is incompatible with Twisted 10.2.0. + # foolscap 0.6.1 quiets a DeprecationWarning. + "foolscap[secure_connections] >= 0.6.1", + + "Nevow >= 0.6.0", + + # Needed for SFTP. pyasn1 is needed by twisted.conch in Twisted >= 9.0. + # 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", + + # http://www.voidspace.org.uk/python/mock/ + "mock", + + # Will be needed to test web apps, but not yet. See #1001. + #"windmill >= 1.3", +] + +# 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'), +] + +def require_more(): + import platform, sys + + if platform.machine().lower() in ['i386', 'x86_64', 'amd64', 'x86', '']: + # 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: - if sys.version_info < (2, 4, 4) or sys.version_info[0] > 2: - raise NotImplementedError("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,)) - -def require_auto_deps(): - """ - The purpose of this function is to raise a pkg_resources exception if any of the - requirements can't be imported. This is just to give earlier and more explicit error - messages, as opposed to waiting until the source code tries to import some module from one - of these packages and gets an ImportError. This function gets called from - src/allmydata/__init__.py . - """ - require_python_version() - - import pkg_resources - for requirement in install_requires: - try: - pkg_resources.require(requirement) - except pkg_resources.DistributionNotFound: - # there is no .egg-info present for this requirement, which - # either means that it isn't installed, or it is installed in a - # way that pkg_resources can't find it (but regular python - # might). There are several older Linux distributions which - # provide our dependencies just fine, but they don't ship - # .egg-info files. Note that if there *is* an .egg-info file, - # but it shows a too-old version, then we'll get a - # VersionConflict error instead of DistributionNotFound. - pass + # 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 difference to users of pycryptopp). + install_requires.append("pycryptopp >= 0.5.14") + + # Sqlite comes built into Python >= 2.5, and is provided by the "pysqlite" + # distribution for Python 2.4. + try: + import sqlite3 + sqlite3 # hush pyflakes + package_imports.append(('sqlite3', 'sqlite3')) + except ImportError: + # pysqlite v2.0.5 was shipped in Ubuntu 6.06 LTS "dapper" and Nexenta NCP 1. + install_requires.append("pysqlite >= 2.0.5") + package_imports.append(('pysqlite', 'pysqlite.dbapi2')) + + if not hasattr(sys, 'frozen'): + # we require newer versions of setuptools (actually + # zetuptoolz) to build, but can handle older versions to run + install_requires.append("setuptools >= 0.6c6") + package_imports.append(('setuptools', 'setuptools')) + +require_more() + +deprecation_messages = [ + "the sha module is deprecated; use the hashlib module instead", + "object.__new__\(\) takes no parameters", + "The popen2 module is deprecated. Use the subprocess module.", + "the md5 module is deprecated; use hashlib instead", + "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', +] diff --git a/src/allmydata/test/test_version.py b/src/allmydata/test/test_version.py new file mode 100644 index 00000000..8bbc3f6b --- /dev/null +++ b/src/allmydata/test/test_version.py @@ -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')