Skip to content

Commit

Permalink
fix: explicit source dependency is not satisfied by direct origin (#7973
Browse files Browse the repository at this point in the history
)

Co-authored-by: David Hotham <david.hotham@blueyonder.co.uk>
Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
(cherry picked from commit c77ffbd)
  • Loading branch information
ralbertazzi authored and radoering committed May 26, 2023
1 parent 1df43f1 commit a1b5f33
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 7 deletions.
23 changes: 23 additions & 0 deletions docs/dependency-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions src/poetry/puzzle/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
71 changes: 71 additions & 0 deletions tests/installation/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
37 changes: 36 additions & 1 deletion tests/puzzle/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down Expand Up @@ -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]
63 changes: 63 additions & 0 deletions tests/puzzle/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit a1b5f33

Please sign in to comment.