From b3882c5ec1cf1b8fc5b1ae3284c8e1674f6ab8a5 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Sat, 8 Mar 2025 15:31:32 -0500 Subject: [PATCH 1/6] Prefer requirements with upper bounds --- .../html/topics/more-dependency-resolution.md | 7 ++- .../resolution/resolvelib/provider.py | 12 ++++- .../resolution_resolvelib/test_provider.py | 46 ++++++++++++++++--- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/docs/html/topics/more-dependency-resolution.md b/docs/html/topics/more-dependency-resolution.md index c0afd3a4e28..96036bedf61 100644 --- a/docs/html/topics/more-dependency-resolution.md +++ b/docs/html/topics/more-dependency-resolution.md @@ -168,8 +168,11 @@ 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 ``==``. + operator ``===`` or ``==`` without a wildcard. +* Prefer requirements that are "upper-bounded" using operators that do + not allow all future versions, i.e. ``<``, ``<=``, ``~=``, and ``==`` + with a wildcard. * 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 ``<``. + operator, such as ``>=`` or ``!=``. * If equal, order alphabetically for consistency (helps debuggability). diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index afdffe8191e..45edadf1aae 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -164,10 +164,13 @@ def get_preference( * 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 ``==``. + operator ``===`` or ``==`` without a wildcard. + * Prefer requirements that are "upper-bounded" using operators that do + not allow all future versions, i.e. ``<``, ``<=``, ``~=``, and ``==`` + with a wildcard. * 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 ``<``. + operator, such as ``>=`` or ``!=``. * If equal, order alphabetically for consistency (helps debuggability). """ try: @@ -193,12 +196,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, diff --git a/tests/unit/resolution_resolvelib/test_provider.py b/tests/unit/resolution_resolvelib/test_provider.py index 690217e85ce..dcca9fd639f 100644 --- a/tests/unit/resolution_resolvelib/test_provider.py +++ b/tests/unit/resolution_resolvelib/test_provider.py @@ -42,7 +42,7 @@ 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 "*" ( @@ -50,7 +50,7 @@ def build_req_info( {"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 ( @@ -58,7 +58,7 @@ def build_req_info( {"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 ( @@ -66,15 +66,15 @@ def build_req_info( {"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) ( @@ -82,7 +82,39 @@ def build_req_info( {"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-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"), ), ], ) From de28f96a011b6a6b7f245ffcbef30d2ca7996637 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Sat, 8 Mar 2025 15:31:38 -0500 Subject: [PATCH 2/6] NEWS ENTRY --- news/13273.feature.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/13273.feature.rst diff --git a/news/13273.feature.rst b/news/13273.feature.rst new file mode 100644 index 00000000000..c4fcb576a76 --- /dev/null +++ b/news/13273.feature.rst @@ -0,0 +1,3 @@ +When resolving dependencies, prefer requirements that are "upper-bounded" using +operators that do not allow all future versions, i.e. ``<``, ``<=``, ``~=``, +and ``==`` with a wildcard. From e7f52d07e4517e23ce884db3811a59be244f96c9 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Sun, 9 Mar 2025 12:40:30 -0400 Subject: [PATCH 3/6] Clarify `get_preference` docs --- .../html/topics/more-dependency-resolution.md | 33 ++++++++++--------- .../resolution/resolvelib/provider.py | 23 +++++++------ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/docs/html/topics/more-dependency-resolution.md b/docs/html/topics/more-dependency-resolution.md index 96036bedf61..132cfef8043 100644 --- a/docs/html/topics/more-dependency-resolution.md +++ b/docs/html/topics/more-dependency-resolution.md @@ -160,19 +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 ``==`` without a wildcard. -* Prefer requirements that are "upper-bounded" using operators that do - not allow all future versions, i.e. ``<``, ``<=``, ``~=``, and ``==`` - with a wildcard. -* Order user-specified requirements by the order they are specified. -* If equal, prefers "non-free" requirements, i.e. contains at least one + 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 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 ``!=``. -* If equal, order alphabetically for consistency (helps debuggability). +* Alphabetical order for consistency (aids debuggability). diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 45edadf1aae..617a993cb03 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -161,17 +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 ``==`` without a wildcard. - * Prefer requirements that are "upper-bounded" using operators that do - not allow all future versions, i.e. ``<``, ``<=``, ``~=``, and ``==`` - with a wildcard. - * Order user-specified requirements by the order they are specified. - * If equal, prefers "non-free" requirements, i.e. contains at least one + * 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 ``!=``. - * If equal, order alphabetically for consistency (helps debuggability). + * Alphabetical order for consistency (aids debuggability). """ try: next(iter(information[identifier])) From cb50601c240d9974fb940b1fcc4b40e293c1537c Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Sun, 9 Mar 2025 12:40:40 -0400 Subject: [PATCH 4/6] Update NEWS ENTRY --- news/13273.feature.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/news/13273.feature.rst b/news/13273.feature.rst index c4fcb576a76..ad3177ec3dd 100644 --- a/news/13273.feature.rst +++ b/news/13273.feature.rst @@ -1,3 +1 @@ -When resolving dependencies, prefer requirements that are "upper-bounded" using -operators that do not allow all future versions, i.e. ``<``, ``<=``, ``~=``, -and ``==`` with a wildcard. +Improved heuristics for determining the order of dependency resolution. From d7536d4a73b13ef1f351d5118be85fc7b90f1cb5 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Sun, 9 Mar 2025 16:53:43 -0400 Subject: [PATCH 5/6] Fix grammar --- docs/html/topics/more-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/topics/more-dependency-resolution.md b/docs/html/topics/more-dependency-resolution.md index 132cfef8043..c048acd8528 100644 --- a/docs/html/topics/more-dependency-resolution.md +++ b/docs/html/topics/more-dependency-resolution.md @@ -163,7 +163,7 @@ Pip's current implementation of the provider implements 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 following order: +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 ``===`` From 1e68d9cb3bd3b57578b0ff0b08f72832ee2dc635 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Sun, 9 Mar 2025 16:57:00 -0400 Subject: [PATCH 6/6] Add missing test --- tests/unit/resolution_resolvelib/test_provider.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit/resolution_resolvelib/test_provider.py b/tests/unit/resolution_resolvelib/test_provider.py index dcca9fd639f..8db40ddc298 100644 --- a/tests/unit/resolution_resolvelib/test_provider.py +++ b/tests/unit/resolution_resolvelib/test_provider.py @@ -96,6 +96,14 @@ def build_req_info( {}, (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",