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/installation/test_installer.py b/tests/installation/test_installer.py index 01a16088fbc..d3017464754 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -2659,3 +2659,74 @@ def test_installer_distinguishes_locked_packages_by_source( source_url=source_url, source_reference=source_reference, ) + + +@pytest.mark.parametrize("env_platform", ["darwin", "linux"]) +def test_explicit_source_dependency_with_direct_origin_dependency( + pool: RepositoryPool, + locker: Locker, + installed: CustomInstalledRepository, + config: Config, + repo: Repository, + package: ProjectPackage, + env_platform: str, +) -> None: + """ + A dependency with explicit source should not be satisfied by + a direct origin dependency even if there is a version match. + """ + package.add_dependency( + Factory.create_dependency( + "demo", + { + "markers": "sys_platform != 'darwin'", + "url": "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl", + }, + ) + ) + package.add_dependency( + Factory.create_dependency( + "demo", + { + "version": "0.1.0", + "markers": "sys_platform == 'darwin'", + "source": "repo", + }, + ) + ) + # The url demo dependency depends on pendulum. + repo.add_package(get_package("pendulum", "1.4.4")) + repo.add_package(get_package("demo", "0.1.0")) + + installer = Installer( + NullIO(), + MockEnv(platform=env_platform), + package, + locker, + pool, + config, + installed=installed, + executor=Executor( + MockEnv(platform=env_platform), + pool, + config, + NullIO(), + ), + ) + + result = installer.run() + + assert result == 0 + assert isinstance(installer.executor, Executor) + if env_platform == "linux": + assert installer.executor.installations == [ + Package("pendulum", "1.4.4"), + Package( + "demo", + "0.1.0", + source_type="url", + source_url="https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl", + ), + ] + else: + assert installer.executor.installations == [Package("demo", "0.1.0")] diff --git a/tests/puzzle/test_provider.py b/tests/puzzle/test_provider.py index 64f86babadf..eec324a2b4a 100644 --- a/tests/puzzle/test_provider.py +++ b/tests/puzzle/test_provider.py @@ -113,7 +113,7 @@ def test_search_for( Dependency("foo", ">=2"), URLDependency("foo", SOME_URL), [Package("foo", "3")], - [], + [Package("foo", "3")], ), ( Dependency("foo", ">=1", extras=["bar"]), @@ -722,3 +722,38 @@ 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") + + assert provider.search_for(dep) == [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 + + assert provider.search_for(dep) == [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 + + assert provider.search_for(dep) == [repo_package]