Commit c77ffbda by Riccardo Albertazzi Committed by GitHub

fix: explicit source dependency is not satisfied by direct origin (#7973)

Co-authored-by: David Hotham <david.hotham@blueyonder.co.uk>
Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
parent f3f71ea2
...@@ -322,6 +322,29 @@ The constraints **must** have different requirements (like `python`) ...@@ -322,6 +322,29 @@ The constraints **must** have different requirements (like `python`)
otherwise it will cause an error when resolving dependencies. otherwise it will cause an error when resolving dependencies.
{{% /note %}} {{% /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 ## Expanded dependency specification syntax
In the case of more complex dependency specifications, you may find that you In the case of more complex dependency specifications, you may find that you
......
...@@ -280,12 +280,8 @@ class Provider: ...@@ -280,12 +280,8 @@ class Provider:
# #
# We rely on the VersionSolver resolving direct-origin dependencies first. # We rely on the VersionSolver resolving direct-origin dependencies first.
direct_origin_package = self._direct_origin_packages.get(dependency.name) direct_origin_package = self._direct_origin_packages.get(dependency.name)
if direct_origin_package is not None: if direct_origin_package and direct_origin_package.satisfies(dependency):
packages = ( packages = [direct_origin_package]
[direct_origin_package]
if dependency.constraint.allows(direct_origin_package.version)
else []
)
return PackageCollection(dependency, packages) return PackageCollection(dependency, packages)
packages = self._pool.find_packages(dependency) packages = self._pool.find_packages(dependency)
......
...@@ -2659,3 +2659,74 @@ def test_installer_distinguishes_locked_packages_by_source( ...@@ -2659,3 +2659,74 @@ def test_installer_distinguishes_locked_packages_by_source(
source_url=source_url, source_url=source_url,
source_reference=source_reference, 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")]
...@@ -113,7 +113,7 @@ def test_search_for( ...@@ -113,7 +113,7 @@ def test_search_for(
Dependency("foo", ">=2"), Dependency("foo", ">=2"),
URLDependency("foo", SOME_URL), URLDependency("foo", SOME_URL),
[Package("foo", "3")], [Package("foo", "3")],
[], [Package("foo", "3")],
), ),
( (
Dependency("foo", ">=1", extras=["bar"]), Dependency("foo", ">=1", extras=["bar"]),
...@@ -722,3 +722,38 @@ def test_complete_package_fetches_optional_vcs_dependency_only_if_requested( ...@@ -722,3 +722,38 @@ def test_complete_package_fetches_optional_vcs_dependency_only_if_requested(
spy.assert_called() spy.assert_called()
else: else:
spy.assert_not_called() 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]
...@@ -3437,6 +3437,69 @@ def test_solver_cannot_choose_another_version_for_url_dependencies( ...@@ -3437,6 +3437,69 @@ def test_solver_cannot_choose_another_version_for_url_dependencies(
solver.solve() solver.solve()
@pytest.mark.parametrize("explicit_source", [True, False])
def test_solver_cannot_choose_url_dependency_for_explicit_source(
solver: Solver,
repo: Repository,
package: ProjectPackage,
explicit_source: bool,
) -> None:
"""A direct origin dependency cannot satisfy a version dependency with an explicit
source. (It can satisfy a version dependency without an explicit source.)
"""
package.add_dependency(
Factory.create_dependency(
"demo",
{
"markers": "sys_platform != 'darwin'",
"url": "https://foo.bar/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" if explicit_source else None,
},
)
)
package_pendulum = get_package("pendulum", "1.4.4")
package_demo = get_package("demo", "0.1.0")
package_demo_url = Package(
"demo",
"0.1.0",
source_type="url",
source_url="https://foo.bar/distributions/demo-0.1.0-py2.py3-none-any.whl",
)
# The url demo dependency depends on pendulum.
repo.add_package(package_pendulum)
repo.add_package(package_demo)
transaction = solver.solve()
if explicit_source:
# direct origin cannot satisfy explicit source
# -> package_demo MUST be included
expected = [
{"job": "install", "package": package_pendulum},
{"job": "install", "package": package_demo_url},
{"job": "install", "package": package_demo},
]
else:
# direct origin can satisfy dependency without source
# -> package_demo NEED NOT (but could) be included
expected = [
{"job": "install", "package": package_pendulum},
{"job": "install", "package": package_demo_url},
]
check_solver_result(transaction, expected)
def test_solver_should_not_update_same_version_packages_if_installed_has_no_source_type( def test_solver_should_not_update_same_version_packages_if_installed_has_no_source_type(
package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO
) -> None: ) -> None:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment