diff --git a/docs/html/topics/more-dependency-resolution.md b/docs/html/topics/more-dependency-resolution.md index c0afd3a4e28..c048acd8528 100644 --- a/docs/html/topics/more-dependency-resolution.md +++ b/docs/html/topics/more-dependency-resolution.md @@ -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). diff --git a/news/13273.feature.rst b/news/13273.feature.rst new file mode 100644 index 00000000000..ad3177ec3dd --- /dev/null +++ b/news/13273.feature.rst @@ -0,0 +1 @@ +Improved heuristics for determining the order of dependency resolution. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index afdffe8191e..617a993cb03 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -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])) @@ -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, diff --git a/tests/unit/resolution_resolvelib/test_provider.py b/tests/unit/resolution_resolvelib/test_provider.py index 690217e85ce..8db40ddc298 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,47 @@ 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-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"), ), ], )