diff --git a/pex/resolver.py b/pex/resolver.py index 1e842837a..d9867565d 100644 --- a/pex/resolver.py +++ b/pex/resolver.py @@ -203,10 +203,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) @@ -222,7 +220,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 26498ec8e..5dad6f41f 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -82,6 +82,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')