Skip to content

Prefer requirements with upper bounds #13273

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions docs/html/topics/more-dependency-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,22 @@ Pip's current implementation of the provider implements

* If Requires-Python is present only consider that
* If there are causes of resolution conflict (backtrack causes) then
only consider them until there are no longer any resolution conflicts

Pip's current implementation of the provider implements `get_preference` as
follows:

* Prefer if any of the known requirements is "direct", e.g. points to an
explicit URL.
* If equal, prefer if any requirement is "pinned", i.e. contains
operator ``===`` or ``==``.
* Order user-specified requirements by the order they are specified.
* If equal, prefers "non-free" requirements, i.e. contains at least one
operator, such as ``>=`` or ``<``.
* If equal, order alphabetically for consistency (helps debuggability).
only consider them until there are no longer any resolution conflicts

Pip's current implementation of the provider implements `get_preference`
for known requirements with the following preferences in the following order:

* Any requirement that is "direct", e.g., points to an explicit URL.
* Any requirement that is "pinned", i.e., contains the operator ``===``
or ``==`` without a wildcard.
* Any requirement that imposes an upper version limit, i.e., contains the
operator ``<``, ``<=``, ``~=``, or ``==`` with a wildcard. Because
pip prioritizes the latest version, preferring explicit upper bounds
can rule out infeasible candidates sooner. This does not imply that
upper bounds are good practice; they can make dependency management
and resolution harder.
* Order user-specified requirements as they are specified, placing
other requirements afterward.
* Any "non-free" requirement, i.e., one that contains at least one
operator, such as ``>=`` or ``!=``.
* Alphabetical order for consistency (aids debuggability).
1 change: 1 addition & 0 deletions news/13273.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improved heuristics for determining the order of dependency resolution.
27 changes: 19 additions & 8 deletions src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,20 @@ def get_preference(

Currently pip considers the following in order:

* Prefer if any of the known requirements is "direct", e.g. points to an
explicit URL.
* If equal, prefer if any requirement is "pinned", i.e. contains
operator ``===`` or ``==``.
* Order user-specified requirements by the order they are specified.
* If equal, prefers "non-free" requirements, i.e. contains at least one
operator, such as ``>=`` or ``<``.
* If equal, order alphabetically for consistency (helps debuggability).
* Any requirement that is "direct", e.g., points to an explicit URL.
* Any requirement that is "pinned", i.e., contains the operator ``===``
or ``==`` without a wildcard.
* Any requirement that imposes an upper version limit, i.e., contains the
operator ``<``, ``<=``, ``~=``, or ``==`` with a wildcard. Because
pip prioritizes the latest version, preferring explicit upper bounds
can rule out infeasible candidates sooner. This does not imply that
upper bounds are good practice; they can make dependency management
and resolution harder.
* Order user-specified requirements as they are specified, placing
other requirements afterward.
* Any "non-free" requirement, i.e., one that contains at least one
operator, such as ``>=`` or ``!=``.
* Alphabetical order for consistency (aids debuggability).
"""
try:
next(iter(information[identifier]))
Expand All @@ -193,12 +199,17 @@ def get_preference(

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))
for op, ver in operators
)
unfree = bool(operators)
requested_order = self._user_requested.get(identifier, math.inf)

return (
not direct,
not pinned,
not upper_bounded,
requested_order,
not unfree,
identifier,
Expand Down
54 changes: 47 additions & 7 deletions tests/unit/resolution_resolvelib/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,47 +42,87 @@ def build_req_info(
{"pinned-package": [build_req_info("pinned-package==1.0")]},
[],
{},
(False, False, math.inf, False, "pinned-package"),
(False, 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, math.inf, False, "star-specified-package"),
(False, 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, math.inf, True, "backtrack-package"),
(False, 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, 1, True, "root-package"),
(False, True, True, 1, True, "root-package"),
),
# Unfree package (with specifier operator)
(
"unfree-package",
{"unfree-package": [build_req_info("unfree-package<1")]},
{"unfree-package": [build_req_info("unfree-package!=1")]},
[],
{},
(False, True, math.inf, False, "unfree-package"),
(False, True, True, math.inf, False, "unfree-package"),
),
# Free package (no operator)
(
"free-package",
{"free-package": [build_req_info("free-package")]},
[],
{},
(False, True, math.inf, True, "free-package"),
(False, True, True, math.inf, True, "free-package"),
),
# Upper bounded with <= operator
(
"upper-bound-lte-package",
{
"upper-bound-lte-package": [
build_req_info("upper-bound-lte-package<=2.0")
]
},
[],
{},
(False, 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"),
),
# Upper bounded with ~= operator
(
"upper-bound-compatible-package",
{
"upper-bound-compatible-package": [
build_req_info("upper-bound-compatible-package~=1.0")
]
},
[],
{},
(False, 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"),
),
],
)
Expand Down