Commit fd4af829 by David Hotham Committed by GitHub

revert #5770, provide new fix (#6058)

Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
parent b5c46f57
...@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING ...@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
from poetry.mixology.assignment import Assignment from poetry.mixology.assignment import Assignment
from poetry.mixology.set_relation import SetRelation from poetry.mixology.set_relation import SetRelation
from poetry.mixology.term import Term
if TYPE_CHECKING: if TYPE_CHECKING:
...@@ -12,6 +11,7 @@ if TYPE_CHECKING: ...@@ -12,6 +11,7 @@ if TYPE_CHECKING:
from poetry.core.packages.package import Package from poetry.core.packages.package import Package
from poetry.mixology.incompatibility import Incompatibility from poetry.mixology.incompatibility import Incompatibility
from poetry.mixology.term import Term
class PartialSolution: class PartialSolution:
...@@ -146,15 +146,6 @@ class PartialSolution: ...@@ -146,15 +146,6 @@ class PartialSolution:
""" """
name = assignment.dependency.complete_name name = assignment.dependency.complete_name
old_positive = self._positive.get(name) old_positive = self._positive.get(name)
if old_positive is None and assignment.dependency.features:
old_positive_without_features = self._positive.get(
assignment.dependency.name
)
if old_positive_without_features is not None:
dep = old_positive_without_features.dependency.with_features(
assignment.dependency.features
)
old_positive = Term(dep, is_positive=True)
if old_positive is not None: if old_positive is not None:
value = old_positive.intersect(assignment) value = old_positive.intersect(assignment)
assert value is not None assert value is not None
......
...@@ -378,6 +378,12 @@ class VersionSolver: ...@@ -378,6 +378,12 @@ class VersionSolver:
# Prefer packages with as few remaining versions as possible, # Prefer packages with as few remaining versions as possible,
# so that if a conflict is necessary it's forced quickly. # so that if a conflict is necessary it's forced quickly.
def _get_min(dependency: Dependency) -> tuple[bool, int]: def _get_min(dependency: Dependency) -> tuple[bool, int]:
# Direct origin dependencies must be handled first: we don't want to resolve
# a regular dependency for some package only to find later that we had a
# direct-origin dependency.
if dependency.is_direct_origin():
return False, -1
if dependency.name in self._use_latest: if dependency.name in self._use_latest:
# If we're forced to use the latest version of a package, it effectively # If we're forced to use the latest version of a package, it effectively
# only has one version to choose from. # only has one version to choose from.
...@@ -387,16 +393,6 @@ class VersionSolver: ...@@ -387,16 +393,6 @@ class VersionSolver:
if locked: if locked:
return not dependency.marker.is_any(), 1 return not dependency.marker.is_any(), 1
# VCS, URL, File or Directory dependencies
# represent a single version
if (
dependency.is_vcs()
or dependency.is_url()
or dependency.is_file()
or dependency.is_directory()
):
return not dependency.marker.is_any(), 1
try: try:
return ( return (
not dependency.marker.is_any(), not dependency.marker.is_any(),
......
...@@ -141,6 +141,7 @@ class Provider: ...@@ -141,6 +141,7 @@ class Provider:
self._load_deferred = True self._load_deferred = True
self._source_root: Path | None = None self._source_root: Path | None = None
self._installed_packages = installed if installed is not None else [] self._installed_packages = installed if installed is not None else []
self._direct_origin_packages: dict[str, Package] = {}
@property @property
def pool(self) -> Pool: def pool(self) -> Pool:
...@@ -269,18 +270,32 @@ class Provider: ...@@ -269,18 +270,32 @@ class Provider:
return PackageCollection(dependency, [self._package]) return PackageCollection(dependency, [self._package])
if dependency.is_direct_origin(): if dependency.is_direct_origin():
packages = [self.search_for_direct_origin_dependency(dependency)] package = self.search_for_direct_origin_dependency(dependency)
self._direct_origin_packages[dependency.name] = package
return PackageCollection(dependency, [package])
else: # If we've previously found a direct-origin package that meets this dependency,
packages = self._pool.find_packages(dependency) # use it.
#
packages.sort( # We rely on the VersionSolver resolving direct-origin dependencies first.
key=lambda p: ( direct_origin_package = self._direct_origin_packages.get(dependency.name)
not p.is_prerelease() and not dependency.allows_prereleases(), if direct_origin_package is not None:
p.version, packages = (
), [direct_origin_package]
reverse=True, if dependency.constraint.allows(direct_origin_package.version)
else []
) )
return PackageCollection(dependency, packages)
packages = self._pool.find_packages(dependency)
packages.sort(
key=lambda p: (
not p.is_prerelease() and not dependency.allows_prereleases(),
p.version,
),
reverse=True,
)
if not packages: if not packages:
packages = self.search_for_installed_packages(dependency) packages = self.search_for_installed_packages(dependency)
......
...@@ -7,9 +7,12 @@ from typing import TYPE_CHECKING ...@@ -7,9 +7,12 @@ from typing import TYPE_CHECKING
import pytest import pytest
from cleo.io.null_io import NullIO from cleo.io.null_io import NullIO
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.directory_dependency import DirectoryDependency from poetry.core.packages.directory_dependency import DirectoryDependency
from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.file_dependency import FileDependency
from poetry.core.packages.package import Package
from poetry.core.packages.project_package import ProjectPackage from poetry.core.packages.project_package import ProjectPackage
from poetry.core.packages.url_dependency import URLDependency
from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.packages.vcs_dependency import VCSDependency
from poetry.factory import Factory from poetry.factory import Factory
...@@ -27,6 +30,9 @@ if TYPE_CHECKING: ...@@ -27,6 +30,9 @@ if TYPE_CHECKING:
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
SOME_URL = "https://example.com/path.tar.gz"
class MockEnv(BaseMockEnv): class MockEnv(BaseMockEnv):
def run(self, bin: str, *args: str) -> None: def run(self, bin: str, *args: str) -> None:
raise EnvCommandError(CalledProcessError(1, "python", output="")) raise EnvCommandError(CalledProcessError(1, "python", output=""))
...@@ -55,6 +61,108 @@ def provider(root: ProjectPackage, pool: Pool) -> Provider: ...@@ -55,6 +61,108 @@ def provider(root: ProjectPackage, pool: Pool) -> Provider:
return Provider(root, pool, NullIO()) return Provider(root, pool, NullIO())
@pytest.mark.parametrize(
"dependency, expected",
[
(Dependency("foo", "<2"), [Package("foo", "1")]),
(Dependency("foo", "<2", extras=["bar"]), [Package("foo", "1")]),
(Dependency("foo", ">=1"), [Package("foo", "2"), Package("foo", "1")]),
(
Dependency("foo", ">=1a"),
[
Package("foo", "3a"),
Package("foo", "2"),
Package("foo", "2a"),
Package("foo", "1"),
],
),
(
Dependency("foo", ">=1", allows_prereleases=True),
[
Package("foo", "3a"),
Package("foo", "2"),
Package("foo", "2a"),
Package("foo", "1"),
],
),
],
)
def test_search_for(
provider: Provider,
repository: Repository,
dependency: Dependency,
expected: list[Package],
) -> None:
foo1 = Package("foo", "1")
foo2a = Package("foo", "2a")
foo2 = Package("foo", "2")
foo3a = Package("foo", "3a")
repository.add_package(foo1)
repository.add_package(foo2a)
repository.add_package(foo2)
repository.add_package(foo3a)
assert provider.search_for(dependency) == expected
@pytest.mark.parametrize(
"dependency, direct_origin_dependency, expected_before, expected_after",
[
(
Dependency("foo", ">=1"),
URLDependency("foo", SOME_URL),
[Package("foo", "3")],
[Package("foo", "2a", source_type="url", source_url=SOME_URL)],
),
(
Dependency("foo", ">=2"),
URLDependency("foo", SOME_URL),
[Package("foo", "3")],
[],
),
(
Dependency("foo", ">=1", extras=["bar"]),
URLDependency("foo", SOME_URL),
[Package("foo", "3")],
[Package("foo", "2a", source_type="url", source_url=SOME_URL)],
),
(
Dependency("foo", ">=1"),
URLDependency("foo", SOME_URL, extras=["baz"]),
[Package("foo", "3")],
[Package("foo", "2a", source_type="url", source_url=SOME_URL)],
),
(
Dependency("foo", ">=1", extras=["bar"]),
URLDependency("foo", SOME_URL, extras=["baz"]),
[Package("foo", "3")],
[Package("foo", "2a", source_type="url", source_url=SOME_URL)],
),
],
)
def test_search_for_direct_origin_and_extras(
provider: Provider,
repository: Repository,
mocker: MockerFixture,
dependency: Dependency,
direct_origin_dependency: Dependency,
expected_before: list[Package],
expected_after: list[Package],
) -> None:
foo2a_direct_origin = Package("foo", "2a", source_type="url", source_url=SOME_URL)
mocker.patch(
"poetry.puzzle.provider.Provider.search_for_direct_origin_dependency",
return_value=foo2a_direct_origin,
)
foo2a = Package("foo", "2a")
foo3 = Package("foo", "3")
repository.add_package(foo2a)
repository.add_package(foo3)
assert provider.search_for(dependency) == expected_before
assert provider.search_for(direct_origin_dependency) == [foo2a_direct_origin]
assert provider.search_for(dependency) == expected_after
@pytest.mark.parametrize("value", [True, False]) @pytest.mark.parametrize("value", [True, False])
def test_search_for_vcs_retains_develop_flag(provider: Provider, value: bool): def test_search_for_vcs_retains_develop_flag(provider: Provider, value: bool):
dependency = VCSDependency( dependency = VCSDependency(
......
...@@ -3596,3 +3596,55 @@ def test_solver_direct_origin_dependency_with_extras_requested_by_other_package( ...@@ -3596,3 +3596,55 @@ def test_solver_direct_origin_dependency_with_extras_requested_by_other_package(
assert op.package.version.text == "0.1.2" assert op.package.version.text == "0.1.2"
assert op.package.source_type == "directory" assert op.package.source_type == "directory"
assert op.package.source_url == path assert op.package.source_url == path
def test_solver_incompatible_dependency_with_and_without_extras(
solver: Solver, repo: Repository, package: ProjectPackage
):
"""
The solver first encounters a requirement for google-auth and then later an
incompatible requirement for google-auth[aiohttp].
Testcase derived from https://github.com/python-poetry/poetry/issues/6054.
"""
# Incompatible requirements from foo and bar2.
foo = get_package("foo", "1.0.0")
foo.add_dependency(Factory.create_dependency("google-auth", {"version": "^1"}))
bar = get_package("bar", "1.0.0")
bar2 = get_package("bar", "2.0.0")
bar2.add_dependency(
Factory.create_dependency(
"google-auth", {"version": "^2", "extras": ["aiohttp"]}
)
)
baz = get_package("baz", "1.0.0") # required by google-auth[aiohttp]
google_auth = get_package("google-auth", "1.2.3")
google_auth.extras = {"aiohttp": [get_dependency("baz", "^1.0")]}
google_auth2 = get_package("google-auth", "2.3.4")
google_auth2.extras = {"aiohttp": [get_dependency("baz", "^1.0")]}
repo.add_package(foo)
repo.add_package(bar)
repo.add_package(bar2)
repo.add_package(baz)
repo.add_package(google_auth)
repo.add_package(google_auth2)
package.add_dependency(Factory.create_dependency("foo", ">=1"))
package.add_dependency(Factory.create_dependency("bar", ">=1"))
transaction = solver.solve()
check_solver_result(
transaction,
[
{"job": "install", "package": google_auth},
{"job": "install", "package": bar},
{"job": "install", "package": foo},
],
)
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