Repository: conda/conda

Issue:
Don't bail when a dependency can't be found
When a dependency for a package can't be found, conda bails completely, but this can happen e.g., just for some old builds of something. So we should just exclude any package like this from the solver. 



Discussion:
It can also happen if someone has a package on their binstar but not all the dependencies for it. 


Resolving patch:
diff --git a/conda/resolve.py b/conda/resolve.py
--- a/conda/resolve.py
+++ b/conda/resolve.py
@@ -30,6 +30,11 @@ def normalized_version(version):
         return version
 
 
+class NoPackagesFound(RuntimeError):
+    def __init__(self, msg, pkg):
+        super(NoPackagesFound, self).__init__(msg)
+        self.pkg = pkg
+
 const_pat = re.compile(r'([=<>!]{1,2})(\S+)$')
 def ver_eval(version, constraint):
     """
@@ -243,7 +248,7 @@ def track_features(self, fn):
     def get_pkgs(self, ms, max_only=False):
         pkgs = [Package(fn, self.index[fn]) for fn in self.find_matches(ms)]
         if not pkgs:
-            raise RuntimeError("No packages found matching: %s" % ms)
+            raise NoPackagesFound("No packages found matching: %s" % ms, ms.spec)
         if max_only:
             maxpkg = max(pkgs)
             ret = []
@@ -262,7 +267,7 @@ def get_pkgs(self, ms, max_only=False):
     def get_max_dists(self, ms):
         pkgs = self.get_pkgs(ms, max_only=True)
         if not pkgs:
-            raise RuntimeError("No packages found matching: %s" % ms)
+            raise NoPackagesFound("No packages found matching: %s" % ms, ms.spec)
         for pkg in pkgs:
             yield pkg.fn
 
@@ -371,11 +376,22 @@ def generate_version_eq(self, v, dists, include0=False):
     def get_dists(self, specs, max_only=False):
         dists = {}
         for spec in specs:
+            found = False
+            notfound = []
             for pkg in self.get_pkgs(MatchSpec(spec), max_only=max_only):
                 if pkg.fn in dists:
+                    found = True
                     continue
-                dists.update(self.all_deps(pkg.fn, max_only=max_only))
-                dists[pkg.fn] = pkg
+                try:
+                    dists.update(self.all_deps(pkg.fn, max_only=max_only))
+                except NoPackagesFound as e:
+                    # Ignore any package that has nonexisting dependencies.
+                    notfound.append(e.pkg)
+                else:
+                    dists[pkg.fn] = pkg
+                    found = True
+            if not found:
+                raise NoPackagesFound("Could not find some dependencies for %s: %s" % (spec, ', '.join(notfound)), None)
 
         return dists
 
@@ -387,25 +403,31 @@ def solve2(self, specs, features, guess=True, alg='sorter', returnall=False):
         # complicated cases that the pseudo-boolean solver does, but it's also
         # much faster when it does work.
 
-        dists = self.get_dists(specs, max_only=True)
-
-        v = {} # map fn to variable number
-        w = {} # map variable number to fn
-        i = -1 # in case the loop doesn't run
-        for i, fn in enumerate(sorted(dists)):
-            v[fn] = i + 1
-            w[i + 1] = fn
-        m = i + 1
-
-        dotlog.debug("Solving using max dists only")
-        clauses = self.gen_clauses(v, dists, specs, features)
-        solutions = min_sat(clauses)
-
-        if len(solutions) == 1:
-            ret = [w[lit] for lit in solutions.pop(0) if 0 < lit]
-            if returnall:
-                return [ret]
-            return ret
+        try:
+            dists = self.get_dists(specs, max_only=True)
+        except NoPackagesFound:
+            # Handle packages that are not included because some dependencies
+            # couldn't be found.
+            pass
+        else:
+            v = {} # map fn to variable number
+            w = {} # map variable number to fn
+            i = -1 # in case the loop doesn't run
+            for i, fn in enumerate(sorted(dists)):
+                v[fn] = i + 1
+                w[i + 1] = fn
+            m = i + 1
+
+            dotlog.debug("Solving using max dists only")
+            clauses = self.gen_clauses(v, dists, specs, features)
+            solutions = min_sat(clauses)
+
+
+            if len(solutions) == 1:
+                ret = [w[lit] for lit in solutions.pop(0) if 0 < lit]
+                if returnall:
+                    return [ret]
+                return ret
 
         dists = self.get_dists(specs)
 


Test patch:
diff --git a/tests/test_resolve.py b/tests/test_resolve.py
--- a/tests/test_resolve.py
+++ b/tests/test_resolve.py
@@ -3,13 +3,15 @@
 import unittest
 from os.path import dirname, join
 
-from conda.resolve import ver_eval, VersionSpec, MatchSpec, Package, Resolve
+from conda.resolve import ver_eval, VersionSpec, MatchSpec, Package, Resolve, NoPackagesFound
 
 from .helpers import raises
 
 
 with open(join(dirname(__file__), 'index.json')) as fi:
-    r = Resolve(json.load(fi))
+    index = json.load(fi)
+
+r = Resolve(index)
 
 f_mkl = set(['mkl'])
 
@@ -672,9 +674,183 @@ def test_unsat():
 def test_nonexistent():
     r.msd_cache = {}
 
-    assert raises(RuntimeError, lambda: r.solve(['notarealpackage 2.0*']), 'No packages found')
+    assert raises(NoPackagesFound, lambda: r.solve(['notarealpackage 2.0*']), 'No packages found')
     # This exact version of NumPy does not exist
-    assert raises(RuntimeError, lambda: r.solve(['numpy 1.5']), 'No packages found')
+    assert raises(NoPackagesFound, lambda: r.solve(['numpy 1.5']), 'No packages found')
+
+def test_nonexistent_deps():
+    index2 = index.copy()
+    index2['mypackage-1.0-py33_0.tar.bz2'] = {
+        'build': 'py33_0',
+        'build_number': 0,
+        'depends': ['nose', 'python 3.3*', 'notarealpackage 2.0*'],
+        'name': 'mypackage',
+        'requires': ['nose 1.2.1', 'python 3.3'],
+        'version': '1.0',
+    }
+    index2['mypackage-1.1-py33_0.tar.bz2'] = {
+        'build': 'py33_0',
+        'build_number': 0,
+        'depends': ['nose', 'python 3.3*'],
+        'name': 'mypackage',
+        'requires': ['nose 1.2.1', 'python 3.3'],
+        'version': '1.1',
+    }
+    r = Resolve(index2)
+
+    assert set(r.find_matches(MatchSpec('mypackage'))) == {
+        'mypackage-1.0-py33_0.tar.bz2',
+        'mypackage-1.1-py33_0.tar.bz2',
+    }
+    assert set(r.get_dists(['mypackage']).keys()) == {
+        'mypackage-1.1-py33_0.tar.bz2',
+        'nose-1.1.2-py26_0.tar.bz2',
+        'nose-1.1.2-py27_0.tar.bz2',
+        'nose-1.1.2-py33_0.tar.bz2',
+        'nose-1.2.1-py26_0.tar.bz2',
+        'nose-1.2.1-py27_0.tar.bz2',
+        'nose-1.2.1-py33_0.tar.bz2',
+        'nose-1.3.0-py26_0.tar.bz2',
+        'nose-1.3.0-py27_0.tar.bz2',
+        'nose-1.3.0-py33_0.tar.bz2',
+        'openssl-1.0.1c-0.tar.bz2',
+        'python-2.6.8-1.tar.bz2',
+        'python-2.6.8-2.tar.bz2',
+        'python-2.6.8-3.tar.bz2',
+        'python-2.6.8-4.tar.bz2',
+        'python-2.6.8-5.tar.bz2',
+        'python-2.6.8-6.tar.bz2',
+        'python-2.7.3-2.tar.bz2',
+        'python-2.7.3-3.tar.bz2',
+        'python-2.7.3-4.tar.bz2',
+        'python-2.7.3-5.tar.bz2',
+        'python-2.7.3-6.tar.bz2',
+        'python-2.7.3-7.tar.bz2',
+        'python-2.7.4-0.tar.bz2',
+        'python-2.7.5-0.tar.bz2',
+        'python-3.3.0-2.tar.bz2',
+        'python-3.3.0-3.tar.bz2',
+        'python-3.3.0-4.tar.bz2',
+        'python-3.3.0-pro0.tar.bz2',
+        'python-3.3.0-pro1.tar.bz2',
+        'python-3.3.1-0.tar.bz2',
+        'python-3.3.2-0.tar.bz2',
+        'readline-6.2-0.tar.bz2',
+        'sqlite-3.7.13-0.tar.bz2',
+        'system-5.8-0.tar.bz2',
+        'system-5.8-1.tar.bz2',
+        'tk-8.5.13-0.tar.bz2',
+        'zlib-1.2.7-0.tar.bz2',
+    }
+
+    assert set(r.get_dists(['mypackage'], max_only=True).keys()) == {
+        'mypackage-1.1-py33_0.tar.bz2',
+        'nose-1.3.0-py26_0.tar.bz2',
+        'nose-1.3.0-py27_0.tar.bz2',
+        'nose-1.3.0-py33_0.tar.bz2',
+        'openssl-1.0.1c-0.tar.bz2',
+        'python-2.6.8-6.tar.bz2',
+        'python-2.7.5-0.tar.bz2',
+        'python-3.3.2-0.tar.bz2',
+        'readline-6.2-0.tar.bz2',
+        'sqlite-3.7.13-0.tar.bz2',
+        'system-5.8-1.tar.bz2',
+        'tk-8.5.13-0.tar.bz2',
+        'zlib-1.2.7-0.tar.bz2',
+    }
+
+    assert r.solve(['mypackage']) == r.solve(['mypackage 1.1']) == [
+        'mypackage-1.1-py33_0.tar.bz2',
+        'nose-1.3.0-py33_0.tar.bz2',
+        'openssl-1.0.1c-0.tar.bz2',
+        'python-3.3.2-0.tar.bz2',
+        'readline-6.2-0.tar.bz2',
+        'sqlite-3.7.13-0.tar.bz2',
+        'system-5.8-1.tar.bz2',
+        'tk-8.5.13-0.tar.bz2',
+        'zlib-1.2.7-0.tar.bz2',
+    ]
+    assert raises(RuntimeError, lambda: r.solve(['mypackage 1.0']))
+
+    # This time, the latest version is messed up
+    index3 = index.copy()
+    index3['mypackage-1.1-py33_0.tar.bz2'] = {
+        'build': 'py33_0',
+        'build_number': 0,
+        'depends': ['nose', 'python 3.3*', 'notarealpackage 2.0*'],
+        'name': 'mypackage',
+        'requires': ['nose 1.2.1', 'python 3.3'],
+        'version': '1.1',
+    }
+    index3['mypackage-1.0-py33_0.tar.bz2'] = {
+        'build': 'py33_0',
+        'build_number': 0,
+        'depends': ['nose', 'python 3.3*'],
+        'name': 'mypackage',
+        'requires': ['nose 1.2.1', 'python 3.3'],
+        'version': '1.0',
+    }
+    r = Resolve(index3)
+
+    assert set(r.find_matches(MatchSpec('mypackage'))) == {
+        'mypackage-1.0-py33_0.tar.bz2',
+        'mypackage-1.1-py33_0.tar.bz2',
+        }
+    assert set(r.get_dists(['mypackage']).keys()) == {
+        'mypackage-1.0-py33_0.tar.bz2',
+        'nose-1.1.2-py26_0.tar.bz2',
+        'nose-1.1.2-py27_0.tar.bz2',
+        'nose-1.1.2-py33_0.tar.bz2',
+        'nose-1.2.1-py26_0.tar.bz2',
+        'nose-1.2.1-py27_0.tar.bz2',
+        'nose-1.2.1-py33_0.tar.bz2',
+        'nose-1.3.0-py26_0.tar.bz2',
+        'nose-1.3.0-py27_0.tar.bz2',
+        'nose-1.3.0-py33_0.tar.bz2',
+        'openssl-1.0.1c-0.tar.bz2',
+        'python-2.6.8-1.tar.bz2',
+        'python-2.6.8-2.tar.bz2',
+        'python-2.6.8-3.tar.bz2',
+        'python-2.6.8-4.tar.bz2',
+        'python-2.6.8-5.tar.bz2',
+        'python-2.6.8-6.tar.bz2',
+        'python-2.7.3-2.tar.bz2',
+        'python-2.7.3-3.tar.bz2',
+        'python-2.7.3-4.tar.bz2',
+        'python-2.7.3-5.tar.bz2',
+        'python-2.7.3-6.tar.bz2',
+        'python-2.7.3-7.tar.bz2',
+        'python-2.7.4-0.tar.bz2',
+        'python-2.7.5-0.tar.bz2',
+        'python-3.3.0-2.tar.bz2',
+        'python-3.3.0-3.tar.bz2',
+        'python-3.3.0-4.tar.bz2',
+        'python-3.3.0-pro0.tar.bz2',
+        'python-3.3.0-pro1.tar.bz2',
+        'python-3.3.1-0.tar.bz2',
+        'python-3.3.2-0.tar.bz2',
+        'readline-6.2-0.tar.bz2',
+        'sqlite-3.7.13-0.tar.bz2',
+        'system-5.8-0.tar.bz2',
+        'system-5.8-1.tar.bz2',
+        'tk-8.5.13-0.tar.bz2',
+        'zlib-1.2.7-0.tar.bz2',
+    }
+
+    assert raises(RuntimeError, lambda: r.get_dists(['mypackage'], max_only=True))
+
+    assert r.solve(['mypackage']) == r.solve(['mypackage 1.0']) == [
+        'mypackage-1.0-py33_0.tar.bz2',
+        'nose-1.3.0-py33_0.tar.bz2',
+        'openssl-1.0.1c-0.tar.bz2',
+        'python-3.3.2-0.tar.bz2',
+        'readline-6.2-0.tar.bz2',
+        'sqlite-3.7.13-0.tar.bz2',
+        'system-5.8-1.tar.bz2',
+        'tk-8.5.13-0.tar.bz2',
+        'zlib-1.2.7-0.tar.bz2',
+    ]
+    assert raises(NoPackagesFound, lambda: r.solve(['mypackage 1.1']))
 
 def test_package_ordering():
     sympy_071 = Package('sympy-0.7.1-py27_0.tar.bz2', r.index['sympy-0.7.1-py27_0.tar.bz2'])