Refactor _auto_deps.py and __init__.py, adding more robust checking of dependency...
authordavid-sarah <david-sarah@jacaranda.org>
Fri, 21 Jan 2011 05:36:10 +0000 (21:36 -0800)
committerdavid-sarah <david-sarah@jacaranda.org>
Fri, 21 Jan 2011 05:36:10 +0000 (21:36 -0800)
src/allmydata/__init__.py
src/allmydata/_auto_deps.py
src/allmydata/test/test_version.py [new file with mode: 0644]

index 886fd4e0becad2cd6d500aa42605945ab6017c67..b0078d08117bea8f08d87d073d67715e0a4c15e0 100644 (file)
@@ -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 <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:
-        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
index b784b8bc515f978b3886d032a5328878f6c3911a..f1fbb3477d1a0c17483fbc901081d1c13b44701f 100644 (file)
-# 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 <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,))
+# 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 (file)
index 0000000..8bbc3f6
--- /dev/null
@@ -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')