Skip to content

Commit 5170554

Browse files
ralbertazzigithub-actions[bot]
authored andcommitted
fix: explicit source dependency is not satisfied by direct origin (#7973)
Co-authored-by: David Hotham <[email protected]> Co-authored-by: Randy Döring <[email protected]> (cherry picked from commit c77ffbd)
1 parent 1df43f1 commit 5170554

File tree

5 files changed

+195
-7
lines changed

5 files changed

+195
-7
lines changed

docs/dependency-specification.md

+23
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,29 @@ The constraints **must** have different requirements (like `python`)
322322
otherwise it will cause an error when resolving dependencies.
323323
{{% /note %}}
324324

325+
### Combining git / url / path dependencies with source repositories
326+
327+
Direct origin (`git`/ `url`/ `path`) dependencies can satisfy the requirement of a dependency that
328+
doesn't explicitly specify a source, even when mutually exclusive markers are used. For instance
329+
in the following example the url package will also be a valid solution for the second requirement:
330+
```toml
331+
foo = [
332+
{ platform = "darwin", url = "https://example.com/example-1.0-py3-none-any.whl" },
333+
{ platform = "linux", version = "^1.0" },
334+
]
335+
```
336+
337+
Sometimes you may instead want to use a direct origin dependency for specific conditions
338+
(i.e. a compiled package that is not available on PyPI for a certain platform/architecture) while
339+
falling back on source repositories in other cases. In this case you should explicitly ask for your
340+
dependency to be satisfied by another `source`. For example:
341+
```toml
342+
foo = [
343+
{ platform = "darwin", url = "https://example.com/foo-1.0.0-py3-none-macosx_11_0_arm64.whl" },
344+
{ platform = "linux", version = "^1.0", source = "pypi" },
345+
]
346+
```
347+
325348
## Expanded dependency specification syntax
326349

327350
In the case of more complex dependency specifications, you may find that you

src/poetry/puzzle/provider.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -280,12 +280,8 @@ def search_for(self, dependency: Dependency) -> list[DependencyPackage]:
280280
#
281281
# We rely on the VersionSolver resolving direct-origin dependencies first.
282282
direct_origin_package = self._direct_origin_packages.get(dependency.name)
283-
if direct_origin_package is not None:
284-
packages = (
285-
[direct_origin_package]
286-
if dependency.constraint.allows(direct_origin_package.version)
287-
else []
288-
)
283+
if direct_origin_package and direct_origin_package.satisfies(dependency):
284+
packages = [direct_origin_package]
289285
return PackageCollection(dependency, packages)
290286

291287
packages = self._pool.find_packages(dependency)

tests/installation/test_installer.py

+71
Original file line numberDiff line numberDiff line change
@@ -2659,3 +2659,74 @@ def test_installer_distinguishes_locked_packages_by_source(
26592659
source_url=source_url,
26602660
source_reference=source_reference,
26612661
)
2662+
2663+
2664+
@pytest.mark.parametrize("env_platform", ["darwin", "linux"])
2665+
def test_explicit_source_dependency_with_direct_origin_dependency(
2666+
pool: RepositoryPool,
2667+
locker: Locker,
2668+
installed: CustomInstalledRepository,
2669+
config: Config,
2670+
repo: Repository,
2671+
package: ProjectPackage,
2672+
env_platform: str,
2673+
) -> None:
2674+
"""
2675+
A dependency with explicit source should not be satisfied by
2676+
a direct origin dependency even if there is a version match.
2677+
"""
2678+
package.add_dependency(
2679+
Factory.create_dependency(
2680+
"demo",
2681+
{
2682+
"markers": "sys_platform != 'darwin'",
2683+
"url": "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl",
2684+
},
2685+
)
2686+
)
2687+
package.add_dependency(
2688+
Factory.create_dependency(
2689+
"demo",
2690+
{
2691+
"version": "0.1.0",
2692+
"markers": "sys_platform == 'darwin'",
2693+
"source": "repo",
2694+
},
2695+
)
2696+
)
2697+
# The url demo dependency depends on pendulum.
2698+
repo.add_package(get_package("pendulum", "1.4.4"))
2699+
repo.add_package(get_package("demo", "0.1.0"))
2700+
2701+
installer = Installer(
2702+
NullIO(),
2703+
MockEnv(platform=env_platform),
2704+
package,
2705+
locker,
2706+
pool,
2707+
config,
2708+
installed=installed,
2709+
executor=Executor(
2710+
MockEnv(platform=env_platform),
2711+
pool,
2712+
config,
2713+
NullIO(),
2714+
),
2715+
)
2716+
2717+
result = installer.run()
2718+
2719+
assert result == 0
2720+
assert isinstance(installer.executor, Executor)
2721+
if env_platform == "linux":
2722+
assert installer.executor.installations == [
2723+
Package("pendulum", "1.4.4"),
2724+
Package(
2725+
"demo",
2726+
"0.1.0",
2727+
source_type="url",
2728+
source_url="https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl",
2729+
),
2730+
]
2731+
else:
2732+
assert installer.executor.installations == [Package("demo", "0.1.0")]

tests/puzzle/test_provider.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def test_search_for(
113113
Dependency("foo", ">=2"),
114114
URLDependency("foo", SOME_URL),
115115
[Package("foo", "3")],
116-
[],
116+
[Package("foo", "3")],
117117
),
118118
(
119119
Dependency("foo", ">=1", extras=["bar"]),
@@ -722,3 +722,38 @@ def test_complete_package_fetches_optional_vcs_dependency_only_if_requested(
722722
spy.assert_called()
723723
else:
724724
spy.assert_not_called()
725+
726+
727+
def test_source_dependency_is_satisfied_by_direct_origin(
728+
provider: Provider, repository: Repository
729+
) -> None:
730+
direct_origin_package = Package("foo", "1.1", source_type="url")
731+
repository.add_package(Package("foo", "1.0"))
732+
provider._direct_origin_packages = {"foo": direct_origin_package}
733+
dep = Dependency("foo", ">=1")
734+
735+
assert provider.search_for(dep) == [direct_origin_package]
736+
737+
738+
def test_explicit_source_dependency_is_not_satisfied_by_direct_origin(
739+
provider: Provider, repository: Repository
740+
) -> None:
741+
repo_package = Package("foo", "1.0")
742+
repository.add_package(repo_package)
743+
provider._direct_origin_packages = {"foo": Package("foo", "1.1", source_type="url")}
744+
dep = Dependency("foo", ">=1")
745+
dep.source_name = repository.name
746+
747+
assert provider.search_for(dep) == [repo_package]
748+
749+
750+
def test_source_dependency_is_not_satisfied_by_incompatible_direct_origin(
751+
provider: Provider, repository: Repository
752+
) -> None:
753+
repo_package = Package("foo", "2.0")
754+
repository.add_package(repo_package)
755+
provider._direct_origin_packages = {"foo": Package("foo", "1.0", source_type="url")}
756+
dep = Dependency("foo", ">=2")
757+
dep.source_name = repository.name
758+
759+
assert provider.search_for(dep) == [repo_package]

tests/puzzle/test_solver.py

+63
Original file line numberDiff line numberDiff line change
@@ -3437,6 +3437,69 @@ def test_solver_cannot_choose_another_version_for_url_dependencies(
34373437
solver.solve()
34383438

34393439

3440+
@pytest.mark.parametrize("explicit_source", [True, False])
3441+
def test_solver_cannot_choose_url_dependency_for_explicit_source(
3442+
solver: Solver,
3443+
repo: Repository,
3444+
package: ProjectPackage,
3445+
explicit_source: bool,
3446+
) -> None:
3447+
"""A direct origin dependency cannot satisfy a version dependency with an explicit
3448+
source. (It can satisfy a version dependency without an explicit source.)
3449+
"""
3450+
package.add_dependency(
3451+
Factory.create_dependency(
3452+
"demo",
3453+
{
3454+
"markers": "sys_platform != 'darwin'",
3455+
"url": "https://foo.bar/distributions/demo-0.1.0-py2.py3-none-any.whl",
3456+
},
3457+
)
3458+
)
3459+
package.add_dependency(
3460+
Factory.create_dependency(
3461+
"demo",
3462+
{
3463+
"version": "0.1.0",
3464+
"markers": "sys_platform == 'darwin'",
3465+
"source": "repo" if explicit_source else None,
3466+
},
3467+
)
3468+
)
3469+
3470+
package_pendulum = get_package("pendulum", "1.4.4")
3471+
package_demo = get_package("demo", "0.1.0")
3472+
package_demo_url = Package(
3473+
"demo",
3474+
"0.1.0",
3475+
source_type="url",
3476+
source_url="https://foo.bar/distributions/demo-0.1.0-py2.py3-none-any.whl",
3477+
)
3478+
# The url demo dependency depends on pendulum.
3479+
repo.add_package(package_pendulum)
3480+
repo.add_package(package_demo)
3481+
3482+
transaction = solver.solve()
3483+
3484+
if explicit_source:
3485+
# direct origin cannot satisfy explicit source
3486+
# -> package_demo MUST be included
3487+
expected = [
3488+
{"job": "install", "package": package_pendulum},
3489+
{"job": "install", "package": package_demo_url},
3490+
{"job": "install", "package": package_demo},
3491+
]
3492+
else:
3493+
# direct origin can satisfy dependency without source
3494+
# -> package_demo NEED NOT (but could) be included
3495+
expected = [
3496+
{"job": "install", "package": package_pendulum},
3497+
{"job": "install", "package": package_demo_url},
3498+
]
3499+
3500+
check_solver_result(transaction, expected)
3501+
3502+
34403503
def test_solver_should_not_update_same_version_packages_if_installed_has_no_source_type(
34413504
package: ProjectPackage, repo: Repository, pool: RepositoryPool, io: NullIO
34423505
) -> None:

0 commit comments

Comments
 (0)