diff --git a/docs/dependency-specification.md b/docs/dependency-specification.md index 991d6e78510..f14fd780e77 100644 --- a/docs/dependency-specification.md +++ b/docs/dependency-specification.md @@ -322,6 +322,29 @@ The constraints **must** have different requirements (like `python`) otherwise it will cause an error when resolving dependencies. {{% /note %}} +### Combining git / url / path dependencies with source repositories + +Direct origin (`git`/ `url`/ `path`) dependencies can satisfy the requirement of a dependency that +doesn't explicitly specify a source, even when mutually exclusive markers are used. For instance +in the following example the url package will also be a valid solution for the second requirement: +```toml +foo = [ + { platform = "darwin", url = "https://example.com/example-1.0-py3-none-any.whl" }, + { platform = "linux", version = "^1.0" }, +] +``` + +Sometimes you may instead want to use a direct origin dependency for specific conditions +(i.e. a compiled package that is not available on PyPI for a certain platform/architecture) while +falling back on source repositories in other cases. In this case you should explicitly ask for your +dependency to be satisfied by another `source`. For example: +```toml +foo = [ + { platform = "darwin", url = "https://example.com/foo-1.0.0-py3-none-macosx_11_0_arm64.whl" }, + { platform = "linux", version = "^1.0", source = "pypi" }, +] +``` + ## Expanded dependency specification syntax In the case of more complex dependency specifications, you may find that you diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index 3c3b738b832..ea1eb8d9559 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -280,12 +280,8 @@ def search_for(self, dependency: Dependency) -> list[DependencyPackage]: # # We rely on the VersionSolver resolving direct-origin dependencies first. direct_origin_package = self._direct_origin_packages.get(dependency.name) - if direct_origin_package is not None: - packages = ( - [direct_origin_package] - if dependency.constraint.allows(direct_origin_package.version) - else [] - ) + if direct_origin_package and direct_origin_package.satisfies(dependency): + packages = [direct_origin_package] return PackageCollection(dependency, packages) packages = self._pool.find_packages(dependency) diff --git a/tests/puzzle/test_provider.py b/tests/puzzle/test_provider.py index 64f86babadf..17fea250346 100644 --- a/tests/puzzle/test_provider.py +++ b/tests/puzzle/test_provider.py @@ -722,3 +722,46 @@ def test_complete_package_fetches_optional_vcs_dependency_only_if_requested( spy.assert_called() else: spy.assert_not_called() + + +def test_source_dependency_is_satisfied_by_direct_origin( + provider: Provider, repository: Repository +) -> None: + direct_origin_package = Package("foo", "1.1", source_type="url") + repository.add_package(Package("foo", "1.0")) + provider._direct_origin_packages = {"foo": direct_origin_package} + dep = Dependency("foo", ">=1") + result = provider.search_for(dep) + + assert len(result) == 1 + assert result[0].package == direct_origin_package + + +def test_explicit_source_dependency_is_not_satisfied_by_direct_origin( + provider: Provider, repository: Repository +) -> None: + repo_package = Package("foo", "1.0") + repository.add_package(repo_package) + provider._direct_origin_packages = {"foo": Package("foo", "1.1", source_type="url")} + dep = Dependency("foo", ">=1") + dep.source_name = repository.name + + result = provider.search_for(dep) + + assert len(result) == 1 + assert result[0].package == repo_package + + +def test_source_dependency_is_not_satisfied_by_incompatible_direct_origin( + provider: Provider, repository: Repository +) -> None: + repo_package = Package("foo", "2.0") + repository.add_package(repo_package) + provider._direct_origin_packages = {"foo": Package("foo", "1.0", source_type="url")} + dep = Dependency("foo", ">=2") + dep.source_name = repository.name + + result = provider.search_for(dep) + + assert len(result) == 1 + assert result[0].package == repo_package