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`)
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
......
......@@ -280,12 +280,8 @@ class Provider:
#
# 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)
......
......@@ -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")]
......@@ -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]
......@@ -3437,6 +3437,69 @@ def test_solver_cannot_choose_another_version_for_url_dependencies(
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(
package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO
) -> 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