Skip to content

Commit 415083c

Browse files
authored
Fix direct preference in PipProvider.get_preference
2 parents 2846077 + 87b1334 commit 415083c

File tree

3 files changed

+56
-16
lines changed

3 files changed

+56
-16
lines changed

news/13244.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
While resolving dependencies prefer if any of the known requirements are
2+
"direct", e.g. points to an explicit URL.

src/pip/_internal/resolution/resolvelib/provider.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@
66
Iterable,
77
Iterator,
88
Mapping,
9+
Optional,
910
Sequence,
11+
Tuple,
1012
TypeVar,
1113
Union,
1214
)
1315

1416
from pip._vendor.resolvelib.providers import AbstractProvider
1517

18+
from pip._internal.req.req_install import InstallRequirement
19+
1620
from .base import Candidate, Constraint, Requirement
1721
from .candidates import REQUIRES_PYTHON_IDENTIFIER
1822
from .factory import Factory
23+
from .requirements import ExplicitRequirement
1924

2025
if TYPE_CHECKING:
2126
from pip._vendor.resolvelib.providers import Preference
@@ -185,19 +190,27 @@ def get_preference(
185190
else:
186191
has_information = True
187192

188-
if has_information:
189-
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
190-
candidate, ireqs = zip(*lookups)
193+
if not has_information:
194+
direct = False
195+
ireqs: Tuple[Optional[InstallRequirement], ...] = ()
191196
else:
192-
candidate, ireqs = None, ()
197+
# Go through the information and for each requirement,
198+
# check if it's explicit (e.g., a direct link) and get the
199+
# InstallRequirement (the second element) from get_candidate_lookup()
200+
directs, ireqs = zip(
201+
*(
202+
(isinstance(r, ExplicitRequirement), r.get_candidate_lookup()[1])
203+
for r, _ in information[identifier]
204+
)
205+
)
206+
direct = any(directs)
193207

194208
operators: list[tuple[str, str]] = [
195209
(specifier.operator, specifier.version)
196210
for specifier_set in (ireq.specifier for ireq in ireqs if ireq)
197211
for specifier in specifier_set
198212
]
199213

200-
direct = candidate is not None
201214
pinned = any(((op[:2] == "==") and ("*" not in ver)) for op, ver in operators)
202215
upper_bounded = any(
203216
((op in ("<", "<=", "~=")) or (op == "==" and "*" in ver))

tests/unit/resolution_resolvelib/test_provider.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
from pip._internal.resolution.resolvelib.candidates import REQUIRES_PYTHON_IDENTIFIER
1111
from pip._internal.resolution.resolvelib.factory import Factory
1212
from pip._internal.resolution.resolvelib.provider import PipProvider
13-
from pip._internal.resolution.resolvelib.requirements import SpecifierRequirement
13+
from pip._internal.resolution.resolvelib.requirements import (
14+
ExplicitRequirement,
15+
SpecifierRequirement,
16+
)
1417

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

2225

26+
class FakeCandidate(Candidate):
27+
"""A minimal fake candidate for testing purposes."""
28+
29+
def __init__(self, *args: object, **kwargs: object) -> None: ...
30+
31+
2332
def build_req_info(
2433
name: str, parent: Optional[Candidate] = None
2534
) -> "PreferenceInformation":
@@ -33,6 +42,14 @@ def build_req_info(
3342
return requirement_information
3443

3544

45+
def build_explicit_req_info(
46+
url: str, parent: Optional[Candidate] = None
47+
) -> "PreferenceInformation":
48+
"""Build a direct requirement using a minimal FakeCandidate."""
49+
direct_requirement = ExplicitRequirement(FakeCandidate(url))
50+
return RequirementInformation(requirement=direct_requirement, parent=parent)
51+
52+
3653
@pytest.mark.parametrize(
3754
"identifier, information, backtrack_causes, user_requested, expected",
3855
[
@@ -42,47 +59,55 @@ def build_req_info(
4259
{"pinned-package": [build_req_info("pinned-package==1.0")]},
4360
[],
4461
{},
45-
(False, False, True, math.inf, False, "pinned-package"),
62+
(True, False, True, math.inf, False, "pinned-package"),
4663
),
4764
# Star-specified package, i.e. with "*"
4865
(
4966
"star-specified-package",
5067
{"star-specified-package": [build_req_info("star-specified-package==1.*")]},
5168
[],
5269
{},
53-
(False, True, False, math.inf, False, "star-specified-package"),
70+
(True, True, False, math.inf, False, "star-specified-package"),
5471
),
5572
# Package that caused backtracking
5673
(
5774
"backtrack-package",
5875
{"backtrack-package": [build_req_info("backtrack-package")]},
5976
[build_req_info("backtrack-package")],
6077
{},
61-
(False, True, True, math.inf, True, "backtrack-package"),
78+
(True, True, True, math.inf, True, "backtrack-package"),
6279
),
6380
# Root package requested by user
6481
(
6582
"root-package",
6683
{"root-package": [build_req_info("root-package")]},
6784
[],
6885
{"root-package": 1},
69-
(False, True, True, 1, True, "root-package"),
86+
(True, True, True, 1, True, "root-package"),
7087
),
7188
# Unfree package (with specifier operator)
7289
(
7390
"unfree-package",
7491
{"unfree-package": [build_req_info("unfree-package!=1")]},
7592
[],
7693
{},
77-
(False, True, True, math.inf, False, "unfree-package"),
94+
(True, True, True, math.inf, False, "unfree-package"),
7895
),
7996
# Free package (no operator)
8097
(
8198
"free-package",
8299
{"free-package": [build_req_info("free-package")]},
83100
[],
84101
{},
85-
(False, True, True, math.inf, True, "free-package"),
102+
(True, True, True, math.inf, True, "free-package"),
103+
),
104+
# Test case for "direct" preference (explicit URL)
105+
(
106+
"direct-package",
107+
{"direct-package": [build_explicit_req_info("direct-package")]},
108+
[],
109+
{},
110+
(False, True, True, math.inf, True, "direct-package"),
86111
),
87112
# Upper bounded with <= operator
88113
(
@@ -94,15 +119,15 @@ def build_req_info(
94119
},
95120
[],
96121
{},
97-
(False, True, False, math.inf, False, "upper-bound-lte-package"),
122+
(True, True, False, math.inf, False, "upper-bound-lte-package"),
98123
),
99124
# Upper bounded with < operator
100125
(
101126
"upper-bound-lt-package",
102127
{"upper-bound-lt-package": [build_req_info("upper-bound-lt-package<2.0")]},
103128
[],
104129
{},
105-
(False, True, False, math.inf, False, "upper-bound-lt-package"),
130+
(True, True, False, math.inf, False, "upper-bound-lt-package"),
106131
),
107132
# Upper bounded with ~= operator
108133
(
@@ -114,15 +139,15 @@ def build_req_info(
114139
},
115140
[],
116141
{},
117-
(False, True, False, math.inf, False, "upper-bound-compatible-package"),
142+
(True, True, False, math.inf, False, "upper-bound-compatible-package"),
118143
),
119144
# Not upper bounded, using only >= operator
120145
(
121146
"lower-bound-package",
122147
{"lower-bound-package": [build_req_info("lower-bound-package>=1.0")]},
123148
[],
124149
{},
125-
(False, True, True, math.inf, False, "lower-bound-package"),
150+
(True, True, True, math.inf, False, "lower-bound-package"),
126151
),
127152
],
128153
)

0 commit comments

Comments
 (0)