From: Daira Hopwood Date: Tue, 20 Jan 2015 18:52:02 +0000 (-0800) Subject: dependecy specs: tolerate new PEP440 semantics too X-Git-Tag: allmydata-tahoe-1.10.1a1~84 X-Git-Url: https://git.rkrishnan.org/specifications/banana.xhtml?a=commitdiff_plain;h=ef455df990c5585e8755d1d791f4dac0ef954956;p=tahoe-lafs%2Ftahoe-lafs.git dependecy specs: tolerate new PEP440 semantics too The latest setuptools (version 8) changed the way dependency specifications ("I can handle libfoo version 2 or 3, but not 4") are interpreted. The new version follows PEP440, which is simpler but somewhat less expressive. Tahoe's _auto_deps.py now uses dep-specs which are correctly parsed by both old and new setuptools. Fixes ticket:2354. * Restrict the requirements in _auto_deps.py to work with either the old or PEP 440 semantics. * Update check_requirement and tests to take account of changes for PEP 440 compatibility. * Fix an error message. * Remove a superfluous TODO. --- diff --git a/src/allmydata/__init__.py b/src/allmydata/__init__.py index e7697e65..e07f73c2 100644 --- a/src/allmydata/__init__.py +++ b/src/allmydata/__init__.py @@ -217,11 +217,10 @@ def get_package_versions_and_locations(): def check_requirement(req, vers_and_locs): - # TODO: check [] options - # We support only disjunctions of <=, >=, and == + # We support only conjunctions of <=, >=, and != reqlist = req.split(',') - name = reqlist[0].split('<=')[0].split('>=')[0].split('==')[0].strip(' ').split('[')[0] + name = reqlist[0].split('<=')[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: @@ -234,33 +233,38 @@ def check_requirement(req, vers_and_locs): return actualver = normalized_version(actual, what="actual version %r of %s from %r" % (actual, name, location)) + if not match_requirement(req, reqlist, actualver): + 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) + + +def match_requirement(req, reqlist, actualver): for r in reqlist: s = r.split('<=') if len(s) == 2: required = s[1].strip(' ') - if actualver <= normalized_version(required, what="required maximum version %r in %r" % (required, req)): - return # maximum requirement met + if not (actualver <= normalized_version(required, what="required maximum version %r in %r" % (required, req))): + return False # maximum requirement not met else: s = r.split('>=') if len(s) == 2: required = s[1].strip(' ') - if actualver >= normalized_version(required, what="required minimum version %r in %r" % (required, req)): - return # minimum requirement met + if not (actualver >= normalized_version(required, what="required minimum version %r in %r" % (required, req))): + return False # minimum requirement not met else: - s = r.split('==') + s = r.split('!=') if len(s) == 2: required = s[1].strip(' ') - if actualver == normalized_version(required, what="required exact version %r in %r" % (required, req)): - return # exact requirement met + if not (actualver != normalized_version(required, what="excluded version %r in %r" % (required, req))): + return False # not-equal requirement not 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) + return True _vers_and_locs_list = get_package_versions_and_locations() diff --git a/src/allmydata/_auto_deps.py b/src/allmydata/_auto_deps.py index 5052ab85..b3f2f2e8 100644 --- a/src/allmydata/_auto_deps.py +++ b/src/allmydata/_auto_deps.py @@ -4,6 +4,17 @@ # 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. +# The semantics for requirement specs changed incompatibly in setuptools 8, +# which now follows PEP 440. The requirements used in this file must be valid +# under both the old and new semantics. That can be achieved by limiting +# requirement specs to one of the following forms: +# +# * >= X, <= Y where X < Y +# * >= X, != Y, != Z, ... where X < Y < Z... +# +# (In addition, check_requirement in allmydata/__init__.py only supports +# >=, <= and != operators.) + install_requires = [ # We require newer versions of setuptools (actually # zetuptoolz) to build, but can handle older versions to run. @@ -16,7 +27,7 @@ install_requires = [ # zope.interface >= 3.6.0 is required for Twisted >= 12.1.0. # zope.interface 3.6.3 and 3.6.4 are incompatible with Nevow (#1435). - "zope.interface == 3.6.0, == 3.6.1, == 3.6.2, >= 3.6.5", + "zope.interface >= 3.6.0, != 3.6.3, != 3.6.4", # * foolscap < 0.5.1 had a performance bug which spent O(N**2) CPU for # transferring large mutable files of size N. @@ -28,7 +39,7 @@ install_requires = [ # Needed for SFTP. # pycrypto 2.2 doesn't work due to # pycrypto 2.4 doesn't work due to - "pycrypto == 2.1.0, == 2.3, >= 2.4.1", + "pycrypto >= 2.1.0, != 2.2, != 2.4", # , 0.8.0 provides "call" "mock >= 0.8.0", @@ -102,7 +113,7 @@ if sys.platform == "win32": # * We don't want Twisted >= 12.3.0 to avoid a dependency of its endpoints # code on pywin32. # - "Twisted == 11.0.0, == 11.1.0, == 12.0.0, == 12.1.0, == 12.2.0", + "Twisted >= 11.0.0, <= 12.2.0", # * We need Nevow >= 0.9.33 to avoid a bug in Nevow's setup.py # which imported twisted at setup time. @@ -110,7 +121,7 @@ if sys.platform == "win32": # which conflicts with the Twisted requirement above. # # - "Nevow == 0.9.33, == 0.10", + "Nevow >= 0.9.33, <= 0.10", # pyasn1 is needed by twisted.conch in Twisted >= 9.0. "pyasn1 >= 0.0.8a", @@ -204,7 +215,7 @@ if _can_use_pyOpenSSL_0_14: ] else: install_requires += [ - "pyOpenSSL == 0.13, == 0.13.1", + "pyOpenSSL >= 0.13, <= 0.13.1", ] diff --git a/src/allmydata/test/test_version.py b/src/allmydata/test/test_version.py index 296db062..fc642777 100644 --- a/src/allmydata/test/test_version.py +++ b/src/allmydata/test/test_version.py @@ -1,4 +1,6 @@ +from pkg_resources import Requirement + from twisted.trial import unittest from allmydata import check_requirement, cross_check, PackagingError @@ -9,39 +11,49 @@ from allmydata.util.verlib import NormalizedVersion as V, \ class CheckRequirement(unittest.TestCase): def test_check_requirement(self): - check_requirement("setuptools >= 0.6c6", {"setuptools": ("0.6", "", None)}) - check_requirement("setuptools >= 0.6c6", {"setuptools": ("0.6", "", "distribute")}) - check_requirement("pycrypto == 2.0.1, == 2.1, >= 2.3", {"pycrypto": ("2.1.0", "", None)}) - check_requirement("pycrypto == 2.0.1, == 2.1, >= 2.3", {"pycrypto": ("2.4.0", "", None)}) - check_requirement("zope.interface <= 3.6.2, >= 3.6.6", {"zope.interface": ("3.6.1", "", None)}) - check_requirement("zope.interface <= 3.6.2, >= 3.6.6", {"zope.interface": ("3.6.6", "", None)}) + self._check_success("setuptools >= 0.6c6", {"setuptools": ("0.6", "", None)}) + self._check_success("setuptools >= 0.6c6", {"setuptools": ("0.6", "", "distribute")}) + self._check_success("pycrypto >= 2.1.0, != 2.2, != 2.4", {"pycrypto": ("2.1.0", "", None)}) + self._check_success("pycrypto >= 2.1.0, != 2.2, != 2.4", {"pycrypto": ("2.3.0", "", None)}) + self._check_success("pycrypto >= 2.1.0, != 2.2, != 2.4", {"pycrypto": ("2.4.1", "", None)}) + self._check_success("Twisted >= 11.0.0, <= 12.2.0", {"Twisted": ("11.0.0", "", None)}) + self._check_success("Twisted >= 11.0.0, <= 12.2.0", {"Twisted": ("12.2.0", "", None)}) - check_requirement("zope.interface", {"zope.interface": ("unknown", "", None)}) - check_requirement("mock", {"mock": ("0.6.0", "", None)}) - check_requirement("foo >= 1.0", {"foo": ("1.0", "", None), "bar": ("2.0", "", None)}) + self._check_success("zope.interface", {"zope.interface": ("unknown", "", None)}) + self._check_success("mock", {"mock": ("0.6.0", "", None)}) + self._check_success("foo >= 1.0", {"foo": ("1.0", "", None), "bar": ("2.0", "", None)}) - check_requirement("foolscap[secure_connections] >= 0.6.0", {"foolscap": ("0.7.0", "", None)}) + self._check_success("foolscap[secure_connections] >= 0.6.0", {"foolscap": ("0.7.0", "", None)}) try: - check_requirement("foolscap[secure_connections] >= 0.6.0", {"foolscap": ("0.6.1+", "", None)}) + self._check_success("foolscap[secure_connections] >= 0.6.0", {"foolscap": ("0.6.1+", "", None)}) # succeeding is ok except PackagingError, e: self.failUnlessIn("could not parse", str(e)) - self.failUnlessRaises(PackagingError, check_requirement, - "foolscap[secure_connections] >= 0.6.0", {"foolscap": ("0.5.1", "", None)}) - self.failUnlessRaises(PackagingError, check_requirement, - "pycrypto == 2.0.1, == 2.1, >= 2.3", {"pycrypto": ("2.2.0", "", None)}) - self.failUnlessRaises(PackagingError, check_requirement, - "zope.interface <= 3.6.2, >= 3.6.6", {"zope.interface": ("3.6.4", "", None)}) - self.failUnlessRaises(PackagingError, check_requirement, - "foo >= 1.0", {}) - self.failUnlessRaises(PackagingError, check_requirement, - "foo >= 1.0", {"foo": ("irrational", "", None)}) + self._check_failure("foolscap[secure_connections] >= 0.6.0", {"foolscap": ("0.5.1", "", None)}) + self._check_failure("pycrypto >= 2.1.0, != 2.2, != 2.4", {"pycrypto": ("2.2.0", "", None)}) + self._check_failure("pycrypto >= 2.1.0, != 2.2, != 2.4", {"pycrypto": ("2.0.0", "", None)}) + self._check_failure("Twisted >= 11.0.0, <= 12.2.0", {"Twisted": ("10.2.0", "", None)}) + self._check_failure("Twisted >= 11.0.0, <= 12.2.0", {"Twisted": ("13.0.0", "", None)}) + self._check_failure("foo >= 1.0", {}) + self._check_failure("foo >= 1.0", {"foo": ("irrational", "", None)}) self.failUnlessRaises(ImportError, check_requirement, "foo >= 1.0", {"foo": (None, None, "foomodule")}) + def _check_success(self, req, vers_and_locs): + check_requirement(req, vers_and_locs) + + for pkg, ver in vers_and_locs.items(): + self.failUnless(ver[0] in Requirement.parse(req), str((ver, req))) + + def _check_failure(self, req, vers_and_locs): + self.failUnlessRaises(PackagingError, check_requirement, req, vers_and_locs) + + for pkg, ver in vers_and_locs.items(): + self.failIf(ver[0] in Requirement.parse(req), str((ver, req))) + def test_cross_check_ticket_1355(self): # The bug in #1355 is triggered when a version string from either pkg_resources or import # is not parseable at all by normalized_version.