From 256236e77d95e9ec9fb5cdf06803ec8f86e083c3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 28 Feb 2017 09:38:08 -0500 Subject: [PATCH] Fix Ambiguous Resolvable bug in transitive dependency resolution If an inexact dependency is specified, and a transitive dependency with either a pin or a tighter bound is found later, Ambiguous Resolvable can be raised. This is because the latest package is added to processed_packages and then by the time the transitive dep runs through the while resolvables loop, it's version doesn't match the one found in processed_packages and Ambiguous Resolvable is raised. This patch fixes it by 1) not raising an error if the packages don't match and 2) after all resolvables are processed, loop back through distributions and recreate the correct list of distributions from resolvable_set.packages(). Fixes: #366 --- pex/resolver.py | 23 ++++++++++++++++++----- tests/test_resolver.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/pex/resolver.py b/pex/resolver.py index feb4980f4..24b402d71 100644 --- a/pex/resolver.py +++ b/pex/resolver.py @@ -200,10 +200,8 @@ def resolve(self, resolvables, resolvable_set=None): assert len(packages) > 0, 'ResolvableSet.packages(%s) should not be empty' % resolvable package = next(iter(packages)) if resolvable.name in processed_packages: - # TODO implement backtracking? - if package != processed_packages[resolvable.name]: - raise self.Error('Ambiguous resolvable: %s' % resolvable) - continue + if package == processed_packages[resolvable.name]: + continue if package not in distributions: dist = self.build(package, resolvable.options) built_package = Package.from_href(dist.location) @@ -219,7 +217,22 @@ def resolve(self, resolvables, resolvable_set=None): distribution.requires(extras=resolvable_set.extras(resolvable.name))) resolvable_set = resolvable_set.replace_built(built_packages) - return list(distributions.values()) + # We may have built multiple distributions depending upon if we found transitive dependencies + # for the same package. But ultimately, resolvable_set.packages() contains the correct version + # for all packages. So loop through it and only return the package version in + # resolvable_set.packages() that is found in distributions. + dists = [] + # No point in proceeding if distributions is empty + if not distributions: + return dists + + for resolvable, packages, parent, constraint_only in resolvable_set.packages(): + if constraint_only: + continue + assert len(packages) > 0, 'ResolvableSet.packages(%s) should not be empty' % resolvable + package = next(iter(packages)) + dists.append(distributions[package]) + return dists class CachingResolver(Resolver): diff --git a/tests/test_resolver.py b/tests/test_resolver.py index a2dc8a07b..5eda68363 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -72,6 +72,24 @@ def test_cached_dependency_pinned_unpinned_resolution_multi_run(): assert dists[0].version == '1.1.0' +def test_ambiguous_transitive_resolvable(): + # If an unbounded or larger bounded resolvable is resolved first, and a + # transitive resolvable is resolved later in another round, Error(Ambiguous resolvable) can be + # raised because foo pulls in foo-2.0.0 and bar->foo==1.0.0 pulls in foo-1.0.0. + foo1_0 = make_sdist(name='foo', version='1.0.0') + foo2_0 = make_sdist(name='foo', version='2.0.0') + bar1_0 = make_sdist(name='bar', version='1.0.0', install_reqs=['foo==1.0.0']) + with temporary_dir() as td: + for sdist in (foo1_0, foo2_0, bar1_0): + safe_copy(sdist, os.path.join(td, os.path.basename(sdist))) + fetchers = [Fetcher([td])] + with temporary_dir() as cd: + dists = resolve(['foo', 'bar'], fetchers=fetchers, cache=cd, + cache_ttl=1000) + assert len(dists) == 2 + assert dists[0].version == '1.0.0' + + def test_resolve_prereleases(): stable_dep = make_sdist(name='dep', version='2.0.0') prerelease_dep = make_sdist(name='dep', version='3.0.0rc3')