[Distutils] ORed requirements Proof of Concept

Matt Good matt at matt-good.net
Sun Dec 17 02:34:11 CET 2006


Based on the recent discussions regarding setuptools requirements where
more than one package may satisfy a requirement.  Setuptools doesn't
have a way to say "my package requires either A or B".  I've created an
inital patch to the setuptools trunk for specifying this as:

install_requires = [
    "A | B",
]

Requirements with multiple candidates like "A | B" will be scanned first
for an already installed version, and if one isn't found it will try to
install each candidate in order until one is successful.

This patch changes the values returned by
pkg_resources.parse_requirements.  The items yielded by the method will
still be Requirement objects for standard requirements, but it will
yield a tuple of Requirement objects if there are multiple candidates.

It also does not yet handle this scenario ideally:
install_requires = [
    'A | B',
],
extras_require = {
    'foo': 'B',
}

If the user wants to install the entry point "foo", setuptools should
detect that 'B' will be installed which will satisfy the installation
requirement 'A | B'.  Right now it will still install 'A' to satisfy the
installation requirement, then also install 'B' to satisfy the 'foo'
extra.

-- Matt Good



Index: setuptools/package_index.py
===================================================================
--- setuptools/package_index.py	(revision 53031)
+++ setuptools/package_index.py	(working copy)
@@ -468,6 +468,13 @@
         set, development and system eggs (i.e., those using the ``.egg-info``
         format) will be ignored.
         """
+        if isinstance(requirement, tuple):
+            for req in requirement:
+                dist = self.fetch_distribution(req, tmpdir, force_scan, source,
+                                               develop_ok)
+                if dist is not None:
+                    return dist
+            return None
 
         # process a Requirement
         self.info("Searching for %s", requirement)
Index: setuptools/command/easy_install.py
===================================================================
--- setuptools/command/easy_install.py	(revision 53031)
+++ setuptools/command/easy_install.py	(working copy)
@@ -414,7 +414,7 @@
         if not self.editable: self.install_site_py()
 
         try:
-            if not isinstance(spec,Requirement):
+            if not isinstance(spec,(Requirement,tuple)):
                 if URL_SCHEME(spec):
                     # It's a url, download it to tmpdir and process
                     self.not_editable(spec)
Index: pkg_resources.py
===================================================================
--- pkg_resources.py	(revision 53031)
+++ pkg_resources.py	(working copy)
@@ -14,6 +14,7 @@
 """
 
 import sys, os, zipimport, time, re, imp, new, pkgutil  # XXX
+from distutils.errors import DistutilsError
 from sets import ImmutableSet
 from os import utime, rename, unlink    # capture these to bypass sandboxing
 from os import open as os_open
@@ -465,20 +466,20 @@
         processed = {}  # set of processed requirements
         best = {}  # key -> dist
         to_activate = []
+        env = [env]
 
-        while requirements:
-            req = requirements.pop(0)   # process dependencies breadth-first
+        def process(req, installer):
             if req in processed:
                 # Ignore cyclic or redundant dependencies
-                continue
+                return
             dist = best.get(req.key)
             if dist is None:
                 # Find the best distribution and add it to the map
                 dist = self.by_key.get(req.key)
                 if dist is None:
-                    if env is None:
-                        env = Environment(self.entries)
-                    dist = best[req.key] = env.best_match(req, self, installer)
+                    if env[0] is None:
+                        env[0] = Environment(self.entries)
+                    dist = best[req.key] = env[0].best_match(req, self, installer)
                     if dist is None:
                         raise DistributionNotFound(req)  # XXX put more info here
                 to_activate.append(dist)
@@ -488,6 +489,27 @@
             requirements.extend(dist.requires(req.extras)[::-1])
             processed[req] = True
 
+        while requirements:
+            req = requirements.pop(0)   # process dependencies breadth-first
+            if isinstance(req, tuple):
+                found_match = False
+                # first try without installer to check pre-installed packages
+                for inst in (None, installer):
+                    for r in req:
+                        try:
+                            process(r, inst)
+                            found_match = True
+                            break
+                        except (DistributionNotFound, VersionConflict,
+                                DistutilsError), e:
+                            pass
+                    if found_match:
+                        break
+                if not found_match:
+                    raise e
+            else:
+                process(req, installer)
+
         return to_activate    # return list of distros to activate
 
     def find_plugins(self,
@@ -1645,6 +1667,8 @@
             s = s.strip()
             if s and not s.startswith('#'):     # skip blank lines/comments
                 yield s
+    elif isinstance(strs,tuple):
+        yield ' | '.join(strs)
     else:
         for ss in strs:
             for s in yield_lines(ss):
@@ -2212,7 +2236,7 @@
         if match: p = match.end()   # skip the terminator, if any
         return line, p, items
 
-    for line in lines:
+    def parse_one(line):
         match = DISTRO(line)
         if not match:
             raise ValueError("Missing distribution spec", line)
@@ -2229,9 +2253,16 @@
 
         line, p, specs = scan_list(VERSION,LINE_END,line,p,(1,2),"version spec")
         specs = [(op,safe_version(val)) for op,val in specs]
-        yield Requirement(project_name, specs, extras)
+        return Requirement(project_name, specs, extras)
 
+    for line in lines:
+        req = [parse_one(i) for i in line.split('|')]
+        if len(req)==1:
+            yield req[0]
+        else:
+            yield tuple(req)
 
+
 def _sort_dists(dists):
     tmp = [(dist.hashcmp,dist) for dist in dists]
     tmp.sort()



More information about the Distutils-SIG mailing list