Skip to content

Commit 7de044d

Browse files
dimblebyradoering
authored andcommitted
revert python-poetry#5770, provide new fix (python-poetry#6058)
Co-authored-by: Randy Döring <[email protected]>
1 parent cdbb59a commit 7de044d

File tree

5 files changed

+192
-30
lines changed

5 files changed

+192
-30
lines changed

src/poetry/mixology/partial_solution.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
from poetry.mixology.assignment import Assignment
66
from poetry.mixology.set_relation import SetRelation
7-
from poetry.mixology.term import Term
87

98

109
if TYPE_CHECKING:
1110
from poetry.core.packages.dependency import Dependency
1211
from poetry.core.packages.package import Package
1312

1413
from poetry.mixology.incompatibility import Incompatibility
14+
from poetry.mixology.term import Term
1515

1616

1717
class PartialSolution:
@@ -146,15 +146,6 @@ def _register(self, assignment: Assignment) -> None:
146146
"""
147147
name = assignment.dependency.complete_name
148148
old_positive = self._positive.get(name)
149-
if old_positive is None and assignment.dependency.features:
150-
old_positive_without_features = self._positive.get(
151-
assignment.dependency.name
152-
)
153-
if old_positive_without_features is not None:
154-
dep = old_positive_without_features.dependency.with_features(
155-
assignment.dependency.features
156-
)
157-
old_positive = Term(dep, is_positive=True)
158149
if old_positive is not None:
159150
value = old_positive.intersect(assignment)
160151
assert value is not None

src/poetry/mixology/version_solver.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,12 @@ def _choose_package_version(self) -> str | None:
378378
# Prefer packages with as few remaining versions as possible,
379379
# so that if a conflict is necessary it's forced quickly.
380380
def _get_min(dependency: Dependency) -> tuple[bool, int]:
381+
# Direct origin dependencies must be handled first: we don't want to resolve
382+
# a regular dependency for some package only to find later that we had a
383+
# direct-origin dependency.
384+
if dependency.is_direct_origin():
385+
return False, -1
386+
381387
if dependency.name in self._use_latest:
382388
# If we're forced to use the latest version of a package, it effectively
383389
# only has one version to choose from.
@@ -387,16 +393,6 @@ def _get_min(dependency: Dependency) -> tuple[bool, int]:
387393
if locked:
388394
return not dependency.marker.is_any(), 1
389395

390-
# VCS, URL, File or Directory dependencies
391-
# represent a single version
392-
if (
393-
dependency.is_vcs()
394-
or dependency.is_url()
395-
or dependency.is_file()
396-
or dependency.is_directory()
397-
):
398-
return not dependency.marker.is_any(), 1
399-
400396
try:
401397
return (
402398
not dependency.marker.is_any(),

src/poetry/puzzle/provider.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def __init__(
141141
self._load_deferred = True
142142
self._source_root: Path | None = None
143143
self._installed_packages = installed if installed is not None else []
144+
self._direct_origin_packages: dict[str, Package] = {}
144145

145146
@property
146147
def pool(self) -> Pool:
@@ -269,18 +270,32 @@ def search_for(self, dependency: Dependency) -> list[DependencyPackage]:
269270
return PackageCollection(dependency, [self._package])
270271

271272
if dependency.is_direct_origin():
272-
packages = [self.search_for_direct_origin_dependency(dependency)]
273+
package = self.search_for_direct_origin_dependency(dependency)
274+
self._direct_origin_packages[dependency.name] = package
275+
return PackageCollection(dependency, [package])
273276

274-
else:
275-
packages = self._pool.find_packages(dependency)
276-
277-
packages.sort(
278-
key=lambda p: (
279-
not p.is_prerelease() and not dependency.allows_prereleases(),
280-
p.version,
281-
),
282-
reverse=True,
277+
# If we've previously found a direct-origin package that meets this dependency,
278+
# use it.
279+
#
280+
# We rely on the VersionSolver resolving direct-origin dependencies first.
281+
direct_origin_package = self._direct_origin_packages.get(dependency.name)
282+
if direct_origin_package is not None:
283+
packages = (
284+
[direct_origin_package]
285+
if dependency.constraint.allows(direct_origin_package.version)
286+
else []
283287
)
288+
return PackageCollection(dependency, packages)
289+
290+
packages = self._pool.find_packages(dependency)
291+
292+
packages.sort(
293+
key=lambda p: (
294+
not p.is_prerelease() and not dependency.allows_prereleases(),
295+
p.version,
296+
),
297+
reverse=True,
298+
)
284299

285300
if not packages:
286301
packages = self.search_for_installed_packages(dependency)

tests/puzzle/test_provider.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
import pytest
88

99
from cleo.io.null_io import NullIO
10+
from poetry.core.packages.dependency import Dependency
1011
from poetry.core.packages.directory_dependency import DirectoryDependency
1112
from poetry.core.packages.file_dependency import FileDependency
13+
from poetry.core.packages.package import Package
1214
from poetry.core.packages.project_package import ProjectPackage
15+
from poetry.core.packages.url_dependency import URLDependency
1316
from poetry.core.packages.vcs_dependency import VCSDependency
1417

1518
from poetry.factory import Factory
@@ -27,6 +30,9 @@
2730
from pytest_mock import MockerFixture
2831

2932

33+
SOME_URL = "https://example.com/path.tar.gz"
34+
35+
3036
class MockEnv(BaseMockEnv):
3137
def run(self, bin: str, *args: str) -> None:
3238
raise EnvCommandError(CalledProcessError(1, "python", output=""))
@@ -55,6 +61,108 @@ def provider(root: ProjectPackage, pool: Pool) -> Provider:
5561
return Provider(root, pool, NullIO())
5662

5763

64+
@pytest.mark.parametrize(
65+
"dependency, expected",
66+
[
67+
(Dependency("foo", "<2"), [Package("foo", "1")]),
68+
(Dependency("foo", "<2", extras=["bar"]), [Package("foo", "1")]),
69+
(Dependency("foo", ">=1"), [Package("foo", "2"), Package("foo", "1")]),
70+
(
71+
Dependency("foo", ">=1a"),
72+
[
73+
Package("foo", "3a"),
74+
Package("foo", "2"),
75+
Package("foo", "2a"),
76+
Package("foo", "1"),
77+
],
78+
),
79+
(
80+
Dependency("foo", ">=1", allows_prereleases=True),
81+
[
82+
Package("foo", "3a"),
83+
Package("foo", "2"),
84+
Package("foo", "2a"),
85+
Package("foo", "1"),
86+
],
87+
),
88+
],
89+
)
90+
def test_search_for(
91+
provider: Provider,
92+
repository: Repository,
93+
dependency: Dependency,
94+
expected: list[Package],
95+
) -> None:
96+
foo1 = Package("foo", "1")
97+
foo2a = Package("foo", "2a")
98+
foo2 = Package("foo", "2")
99+
foo3a = Package("foo", "3a")
100+
repository.add_package(foo1)
101+
repository.add_package(foo2a)
102+
repository.add_package(foo2)
103+
repository.add_package(foo3a)
104+
assert provider.search_for(dependency) == expected
105+
106+
107+
@pytest.mark.parametrize(
108+
"dependency, direct_origin_dependency, expected_before, expected_after",
109+
[
110+
(
111+
Dependency("foo", ">=1"),
112+
URLDependency("foo", SOME_URL),
113+
[Package("foo", "3")],
114+
[Package("foo", "2a", source_type="url", source_url=SOME_URL)],
115+
),
116+
(
117+
Dependency("foo", ">=2"),
118+
URLDependency("foo", SOME_URL),
119+
[Package("foo", "3")],
120+
[],
121+
),
122+
(
123+
Dependency("foo", ">=1", extras=["bar"]),
124+
URLDependency("foo", SOME_URL),
125+
[Package("foo", "3")],
126+
[Package("foo", "2a", source_type="url", source_url=SOME_URL)],
127+
),
128+
(
129+
Dependency("foo", ">=1"),
130+
URLDependency("foo", SOME_URL, extras=["baz"]),
131+
[Package("foo", "3")],
132+
[Package("foo", "2a", source_type="url", source_url=SOME_URL)],
133+
),
134+
(
135+
Dependency("foo", ">=1", extras=["bar"]),
136+
URLDependency("foo", SOME_URL, extras=["baz"]),
137+
[Package("foo", "3")],
138+
[Package("foo", "2a", source_type="url", source_url=SOME_URL)],
139+
),
140+
],
141+
)
142+
def test_search_for_direct_origin_and_extras(
143+
provider: Provider,
144+
repository: Repository,
145+
mocker: MockerFixture,
146+
dependency: Dependency,
147+
direct_origin_dependency: Dependency,
148+
expected_before: list[Package],
149+
expected_after: list[Package],
150+
) -> None:
151+
foo2a_direct_origin = Package("foo", "2a", source_type="url", source_url=SOME_URL)
152+
mocker.patch(
153+
"poetry.puzzle.provider.Provider.search_for_direct_origin_dependency",
154+
return_value=foo2a_direct_origin,
155+
)
156+
foo2a = Package("foo", "2a")
157+
foo3 = Package("foo", "3")
158+
repository.add_package(foo2a)
159+
repository.add_package(foo3)
160+
161+
assert provider.search_for(dependency) == expected_before
162+
assert provider.search_for(direct_origin_dependency) == [foo2a_direct_origin]
163+
assert provider.search_for(dependency) == expected_after
164+
165+
58166
@pytest.mark.parametrize("value", [True, False])
59167
def test_search_for_vcs_retains_develop_flag(provider: Provider, value: bool):
60168
dependency = VCSDependency(

tests/puzzle/test_solver.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3596,3 +3596,55 @@ def test_solver_direct_origin_dependency_with_extras_requested_by_other_package(
35963596
assert op.package.version.text == "0.1.2"
35973597
assert op.package.source_type == "directory"
35983598
assert op.package.source_url == path
3599+
3600+
3601+
def test_solver_incompatible_dependency_with_and_without_extras(
3602+
solver: Solver, repo: Repository, package: ProjectPackage
3603+
):
3604+
"""
3605+
The solver first encounters a requirement for google-auth and then later an
3606+
incompatible requirement for google-auth[aiohttp].
3607+
3608+
Testcase derived from https://github.com/python-poetry/poetry/issues/6054.
3609+
"""
3610+
# Incompatible requirements from foo and bar2.
3611+
foo = get_package("foo", "1.0.0")
3612+
foo.add_dependency(Factory.create_dependency("google-auth", {"version": "^1"}))
3613+
3614+
bar = get_package("bar", "1.0.0")
3615+
3616+
bar2 = get_package("bar", "2.0.0")
3617+
bar2.add_dependency(
3618+
Factory.create_dependency(
3619+
"google-auth", {"version": "^2", "extras": ["aiohttp"]}
3620+
)
3621+
)
3622+
3623+
baz = get_package("baz", "1.0.0") # required by google-auth[aiohttp]
3624+
3625+
google_auth = get_package("google-auth", "1.2.3")
3626+
google_auth.extras = {"aiohttp": [get_dependency("baz", "^1.0")]}
3627+
3628+
google_auth2 = get_package("google-auth", "2.3.4")
3629+
google_auth2.extras = {"aiohttp": [get_dependency("baz", "^1.0")]}
3630+
3631+
repo.add_package(foo)
3632+
repo.add_package(bar)
3633+
repo.add_package(bar2)
3634+
repo.add_package(baz)
3635+
repo.add_package(google_auth)
3636+
repo.add_package(google_auth2)
3637+
3638+
package.add_dependency(Factory.create_dependency("foo", ">=1"))
3639+
package.add_dependency(Factory.create_dependency("bar", ">=1"))
3640+
3641+
transaction = solver.solve()
3642+
3643+
check_solver_result(
3644+
transaction,
3645+
[
3646+
{"job": "install", "package": google_auth},
3647+
{"job": "install", "package": bar},
3648+
{"job": "install", "package": foo},
3649+
],
3650+
)

0 commit comments

Comments
 (0)