Skip to content

Commit 7352dd5

Browse files
authored
Merge pull request #13273 from notatallshaw/prefer-requirements-with-upper-bounds
Prefer requirements with upper bounds
2 parents f82e892 + 1e68d9c commit 7352dd5

File tree

4 files changed

+86
-28
lines changed

4 files changed

+86
-28
lines changed

docs/html/topics/more-dependency-resolution.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,22 @@ Pip's current implementation of the provider implements
160160

161161
* If Requires-Python is present only consider that
162162
* If there are causes of resolution conflict (backtrack causes) then
163-
only consider them until there are no longer any resolution conflicts
164-
165-
Pip's current implementation of the provider implements `get_preference` as
166-
follows:
167-
168-
* Prefer if any of the known requirements is "direct", e.g. points to an
169-
explicit URL.
170-
* If equal, prefer if any requirement is "pinned", i.e. contains
171-
operator ``===`` or ``==``.
172-
* Order user-specified requirements by the order they are specified.
173-
* If equal, prefers "non-free" requirements, i.e. contains at least one
174-
operator, such as ``>=`` or ``<``.
175-
* If equal, order alphabetically for consistency (helps debuggability).
163+
only consider them until there are no longer any resolution conflicts
164+
165+
Pip's current implementation of the provider implements `get_preference`
166+
for known requirements with the following preferences in the following order:
167+
168+
* Any requirement that is "direct", e.g., points to an explicit URL.
169+
* Any requirement that is "pinned", i.e., contains the operator ``===``
170+
or ``==`` without a wildcard.
171+
* Any requirement that imposes an upper version limit, i.e., contains the
172+
operator ``<``, ``<=``, ``~=``, or ``==`` with a wildcard. Because
173+
pip prioritizes the latest version, preferring explicit upper bounds
174+
can rule out infeasible candidates sooner. This does not imply that
175+
upper bounds are good practice; they can make dependency management
176+
and resolution harder.
177+
* Order user-specified requirements as they are specified, placing
178+
other requirements afterward.
179+
* Any "non-free" requirement, i.e., one that contains at least one
180+
operator, such as ``>=`` or ``!=``.
181+
* Alphabetical order for consistency (aids debuggability).

news/13273.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved heuristics for determining the order of dependency resolution.

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,20 @@ def get_preference(
161161
162162
Currently pip considers the following in order:
163163
164-
* Prefer if any of the known requirements is "direct", e.g. points to an
165-
explicit URL.
166-
* If equal, prefer if any requirement is "pinned", i.e. contains
167-
operator ``===`` or ``==``.
168-
* Order user-specified requirements by the order they are specified.
169-
* If equal, prefers "non-free" requirements, i.e. contains at least one
170-
operator, such as ``>=`` or ``<``.
171-
* If equal, order alphabetically for consistency (helps debuggability).
164+
* Any requirement that is "direct", e.g., points to an explicit URL.
165+
* Any requirement that is "pinned", i.e., contains the operator ``===``
166+
or ``==`` without a wildcard.
167+
* Any requirement that imposes an upper version limit, i.e., contains the
168+
operator ``<``, ``<=``, ``~=``, or ``==`` with a wildcard. Because
169+
pip prioritizes the latest version, preferring explicit upper bounds
170+
can rule out infeasible candidates sooner. This does not imply that
171+
upper bounds are good practice; they can make dependency management
172+
and resolution harder.
173+
* Order user-specified requirements as they are specified, placing
174+
other requirements afterward.
175+
* Any "non-free" requirement, i.e., one that contains at least one
176+
operator, such as ``>=`` or ``!=``.
177+
* Alphabetical order for consistency (aids debuggability).
172178
"""
173179
try:
174180
next(iter(information[identifier]))
@@ -193,12 +199,17 @@ def get_preference(
193199

194200
direct = candidate is not None
195201
pinned = any(((op[:2] == "==") and ("*" not in ver)) for op, ver in operators)
202+
upper_bounded = any(
203+
((op in ("<", "<=", "~=")) or (op == "==" and "*" in ver))
204+
for op, ver in operators
205+
)
196206
unfree = bool(operators)
197207
requested_order = self._user_requested.get(identifier, math.inf)
198208

199209
return (
200210
not direct,
201211
not pinned,
212+
not upper_bounded,
202213
requested_order,
203214
not unfree,
204215
identifier,

tests/unit/resolution_resolvelib/test_provider.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,47 +42,87 @@ def build_req_info(
4242
{"pinned-package": [build_req_info("pinned-package==1.0")]},
4343
[],
4444
{},
45-
(False, False, math.inf, False, "pinned-package"),
45+
(False, False, True, math.inf, False, "pinned-package"),
4646
),
4747
# Star-specified package, i.e. with "*"
4848
(
4949
"star-specified-package",
5050
{"star-specified-package": [build_req_info("star-specified-package==1.*")]},
5151
[],
5252
{},
53-
(False, True, math.inf, False, "star-specified-package"),
53+
(False, True, False, math.inf, False, "star-specified-package"),
5454
),
5555
# Package that caused backtracking
5656
(
5757
"backtrack-package",
5858
{"backtrack-package": [build_req_info("backtrack-package")]},
5959
[build_req_info("backtrack-package")],
6060
{},
61-
(False, True, math.inf, True, "backtrack-package"),
61+
(False, True, True, math.inf, True, "backtrack-package"),
6262
),
6363
# Root package requested by user
6464
(
6565
"root-package",
6666
{"root-package": [build_req_info("root-package")]},
6767
[],
6868
{"root-package": 1},
69-
(False, True, 1, True, "root-package"),
69+
(False, True, True, 1, True, "root-package"),
7070
),
7171
# Unfree package (with specifier operator)
7272
(
7373
"unfree-package",
74-
{"unfree-package": [build_req_info("unfree-package<1")]},
74+
{"unfree-package": [build_req_info("unfree-package!=1")]},
7575
[],
7676
{},
77-
(False, True, math.inf, False, "unfree-package"),
77+
(False, True, True, math.inf, False, "unfree-package"),
7878
),
7979
# Free package (no operator)
8080
(
8181
"free-package",
8282
{"free-package": [build_req_info("free-package")]},
8383
[],
8484
{},
85-
(False, True, math.inf, True, "free-package"),
85+
(False, True, True, math.inf, True, "free-package"),
86+
),
87+
# Upper bounded with <= operator
88+
(
89+
"upper-bound-lte-package",
90+
{
91+
"upper-bound-lte-package": [
92+
build_req_info("upper-bound-lte-package<=2.0")
93+
]
94+
},
95+
[],
96+
{},
97+
(False, True, False, math.inf, False, "upper-bound-lte-package"),
98+
),
99+
# Upper bounded with < operator
100+
(
101+
"upper-bound-lt-package",
102+
{"upper-bound-lt-package": [build_req_info("upper-bound-lt-package<2.0")]},
103+
[],
104+
{},
105+
(False, True, False, math.inf, False, "upper-bound-lt-package"),
106+
),
107+
# Upper bounded with ~= operator
108+
(
109+
"upper-bound-compatible-package",
110+
{
111+
"upper-bound-compatible-package": [
112+
build_req_info("upper-bound-compatible-package~=1.0")
113+
]
114+
},
115+
[],
116+
{},
117+
(False, True, False, math.inf, False, "upper-bound-compatible-package"),
118+
),
119+
# Not upper bounded, using only >= operator
120+
(
121+
"lower-bound-package",
122+
{"lower-bound-package": [build_req_info("lower-bound-package>=1.0")]},
123+
[],
124+
{},
125+
(False, True, True, math.inf, False, "lower-bound-package"),
86126
),
87127
],
88128
)

0 commit comments

Comments
 (0)