From ef455df990c5585e8755d1d791f4dac0ef954956 Mon Sep 17 00:00:00 2001
From: Daira Hopwood <daira@jacaranda.org>
Date: Tue, 20 Jan 2015 10:52:02 -0800
Subject: [PATCH] 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.
---
 src/allmydata/__init__.py          | 36 +++++++++++---------
 src/allmydata/_auto_deps.py        | 21 +++++++++---
 src/allmydata/test/test_version.py | 54 ++++++++++++++++++------------
 3 files changed, 69 insertions(+), 42 deletions(-)

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 <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",
     ]
 
 
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.
-- 
2.45.2