Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix direct preference in PipProvider.get_preference #13244

Merged
merged 9 commits into from
Mar 27, 2025
2 changes: 2 additions & 0 deletions news/13244.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
While resolving dependencies prefer if any of the known requirements are
"direct", e.g. points to an explicit URL.
23 changes: 18 additions & 5 deletions src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
Iterable,
Iterator,
Mapping,
Optional,
Sequence,
Tuple,
TypeVar,
Union,
)

from pip._vendor.resolvelib.providers import AbstractProvider

from pip._internal.req.req_install import InstallRequirement

from .base import Candidate, Constraint, Requirement
from .candidates import REQUIRES_PYTHON_IDENTIFIER
from .factory import Factory
from .requirements import ExplicitRequirement

if TYPE_CHECKING:
from pip._vendor.resolvelib.providers import Preference
Expand Down Expand Up @@ -185,19 +190,27 @@ def get_preference(
else:
has_information = True

if has_information:
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
candidate, ireqs = zip(*lookups)
if not has_information:
direct = False
ireqs: Tuple[Optional[InstallRequirement], ...] = ()
else:
candidate, ireqs = None, ()
# Go through the information and for each requirement,
# check if it's explicit (e.g., a direct link) and get the
# InstallRequirement (the second element) from get_candidate_lookup()
directs, ireqs = zip(
*(
(isinstance(r, ExplicitRequirement), r.get_candidate_lookup()[1])
for r, _ in information[identifier]
)
)
direct = any(directs)

operators: list[tuple[str, str]] = [
(specifier.operator, specifier.version)
for specifier_set in (ireq.specifier for ireq in ireqs if ireq)
for specifier in specifier_set
]

direct = candidate is not None
pinned = any(((op[:2] == "==") and ("*" not in ver)) for op, ver in operators)
upper_bounded = any(
((op in ("<", "<=", "~=")) or (op == "==" and "*" in ver))
Expand Down
47 changes: 36 additions & 11 deletions tests/unit/resolution_resolvelib/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
from pip._internal.resolution.resolvelib.candidates import REQUIRES_PYTHON_IDENTIFIER
from pip._internal.resolution.resolvelib.factory import Factory
from pip._internal.resolution.resolvelib.provider import PipProvider
from pip._internal.resolution.resolvelib.requirements import SpecifierRequirement
from pip._internal.resolution.resolvelib.requirements import (
ExplicitRequirement,
SpecifierRequirement,
)

if TYPE_CHECKING:
from pip._vendor.resolvelib.providers import Preference
Expand All @@ -20,6 +23,12 @@
PreferenceInformation = RequirementInformation[Requirement, Candidate]


class FakeCandidate(Candidate):
"""A minimal fake candidate for testing purposes."""

def __init__(self, *args: object, **kwargs: object) -> None: ...


def build_req_info(
name: str, parent: Optional[Candidate] = None
) -> "PreferenceInformation":
Expand All @@ -33,6 +42,14 @@ def build_req_info(
return requirement_information


def build_explicit_req_info(
url: str, parent: Optional[Candidate] = None
) -> "PreferenceInformation":
"""Build a direct requirement using a minimal FakeCandidate."""
direct_requirement = ExplicitRequirement(FakeCandidate(url))
return RequirementInformation(requirement=direct_requirement, parent=parent)


@pytest.mark.parametrize(
"identifier, information, backtrack_causes, user_requested, expected",
[
Expand All @@ -42,47 +59,55 @@ def build_req_info(
{"pinned-package": [build_req_info("pinned-package==1.0")]},
[],
{},
(False, False, True, math.inf, False, "pinned-package"),
(True, False, True, math.inf, False, "pinned-package"),
),
# Star-specified package, i.e. with "*"
(
"star-specified-package",
{"star-specified-package": [build_req_info("star-specified-package==1.*")]},
[],
{},
(False, True, False, math.inf, False, "star-specified-package"),
(True, True, False, math.inf, False, "star-specified-package"),
),
# Package that caused backtracking
(
"backtrack-package",
{"backtrack-package": [build_req_info("backtrack-package")]},
[build_req_info("backtrack-package")],
{},
(False, True, True, math.inf, True, "backtrack-package"),
(True, True, True, math.inf, True, "backtrack-package"),
),
# Root package requested by user
(
"root-package",
{"root-package": [build_req_info("root-package")]},
[],
{"root-package": 1},
(False, True, True, 1, True, "root-package"),
(True, True, True, 1, True, "root-package"),
),
# Unfree package (with specifier operator)
(
"unfree-package",
{"unfree-package": [build_req_info("unfree-package!=1")]},
[],
{},
(False, True, True, math.inf, False, "unfree-package"),
(True, True, True, math.inf, False, "unfree-package"),
),
# Free package (no operator)
(
"free-package",
{"free-package": [build_req_info("free-package")]},
[],
{},
(False, True, True, math.inf, True, "free-package"),
(True, True, True, math.inf, True, "free-package"),
),
# Test case for "direct" preference (explicit URL)
(
"direct-package",
{"direct-package": [build_explicit_req_info("direct-package")]},
[],
{},
(False, True, True, math.inf, True, "direct-package"),
),
# Upper bounded with <= operator
(
Expand All @@ -94,15 +119,15 @@ def build_req_info(
},
[],
{},
(False, True, False, math.inf, False, "upper-bound-lte-package"),
(True, True, False, math.inf, False, "upper-bound-lte-package"),
),
# Upper bounded with < operator
(
"upper-bound-lt-package",
{"upper-bound-lt-package": [build_req_info("upper-bound-lt-package<2.0")]},
[],
{},
(False, True, False, math.inf, False, "upper-bound-lt-package"),
(True, True, False, math.inf, False, "upper-bound-lt-package"),
),
# Upper bounded with ~= operator
(
Expand All @@ -114,15 +139,15 @@ def build_req_info(
},
[],
{},
(False, True, False, math.inf, False, "upper-bound-compatible-package"),
(True, True, False, math.inf, False, "upper-bound-compatible-package"),
),
# Not upper bounded, using only >= operator
(
"lower-bound-package",
{"lower-bound-package": [build_req_info("lower-bound-package>=1.0")]},
[],
{},
(False, True, True, math.inf, False, "lower-bound-package"),
(True, True, True, math.inf, False, "lower-bound-package"),
),
],
)
Expand Down