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.
This commit is contained in:
Daira Hopwood 2015-01-20 10:52:02 -08:00 committed by Brian Warner
parent f77f358dc1
commit ef455df990
3 changed files with 69 additions and 42 deletions

View File

@ -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()

View File

@ -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 <https://bugs.launchpad.net/pycrypto/+bug/620253>
# pycrypto 2.4 doesn't work due to <https://bugs.launchpad.net/pycrypto/+bug/881130>
"pycrypto == 2.1.0, == 2.3, >= 2.4.1",
"pycrypto >= 2.1.0, != 2.2, != 2.4",
# <http://www.voidspace.org.uk/python/mock/>, 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. <https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2028>
#
"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.
# <https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2291>
#
"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",
]

View File

@ -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.