1+ import collections
2+ import math
13from typing import TYPE_CHECKING , Dict , Iterable , Iterator , Mapping , Sequence , Union
24
35from pip ._vendor .resolvelib .providers import AbstractProvider
@@ -60,6 +62,7 @@ def __init__(
6062 self ._ignore_dependencies = ignore_dependencies
6163 self ._upgrade_strategy = upgrade_strategy
6264 self ._user_requested = user_requested
65+ self ._known_depths = collections .defaultdict (lambda : math .inf )
6366
6467 def identify (self , requirement_or_candidate ):
6568 # type: (Union[Requirement, Candidate]) -> str
@@ -79,48 +82,43 @@ def get_preference(
7982
8083 Currently pip considers the followings in order:
8184
82- * Prefer if any of the known requirements points to an explicit URL.
83- * If equal, prefer if any requirements contain ``===`` and ``==``.
84- * If equal, prefer if requirements include version constraints, e.g.
85- ``>=`` and ``<``.
86- * If equal, prefer user-specified (non-transitive) requirements, and
87- order user-specified requirements by the order they are specified.
85+ * Prefer if any of the known requirements is "direct", e.g. points to an
86+ explicit URL.
87+ * If equal, prefer if any requirement is "pinned", i.e. contains
88+ operator ``===`` or ``==``.
89+ * If equal, calculate an approximate "depth" and resolve requirements
90+ closer to the user-specified requirements first.
91+ * Order user-specified requirements by the order they are specified.
92+ * If equal, prefers "non-free" requirements, i.e. contains at least one
93+ operator, such as ``>=`` or ``<``.
8894 * If equal, order alphabetically for consistency (helps debuggability).
8995 """
90-
91- def _get_restrictive_rating (requirements ):
92- # type: (Iterable[Requirement]) -> int
93- """Rate how restrictive a set of requirements are.
94-
95- ``Requirement.get_candidate_lookup()`` returns a 2-tuple for
96- lookup. The first element is ``Optional[Candidate]`` and the
97- second ``Optional[InstallRequirement]``.
98-
99- * If the requirement is an explicit one, the explicitly-required
100- candidate is returned as the first element.
101- * If the requirement is based on a PEP 508 specifier, the backing
102- ``InstallRequirement`` is returned as the second element.
103-
104- We use the first element to check whether there is an explicit
105- requirement, and the second for equality operator.
106- """
107- lookups = (r .get_candidate_lookup () for r in requirements )
108- cands , ireqs = zip (* lookups )
109- if any (cand is not None for cand in cands ):
110- return 0
111- spec_sets = (ireq .specifier for ireq in ireqs if ireq )
112- operators = [
113- specifier .operator for spec_set in spec_sets for specifier in spec_set
114- ]
115- if any (op in ("==" , "===" ) for op in operators ):
116- return 1
117- if operators :
118- return 2
119- # A "bare" requirement without any version requirements.
120- return 3
121-
122- rating = _get_restrictive_rating (r for r , _ in information [identifier ])
123- order = self ._user_requested .get (identifier , float ("inf" ))
96+ lookups = (r .get_candidate_lookup () for r , _ in information [identifier ])
97+ candidate , ireqs = zip (* lookups )
98+ operators = [
99+ specifier .operator
100+ for specifier_set in (ireq .specifier for ireq in ireqs if ireq )
101+ for specifier in specifier_set
102+ ]
103+
104+ direct = candidate is not None
105+ pinned = any (op [:2 ] == "==" for op in operators )
106+ unfree = bool (operators )
107+
108+ try :
109+ reqeusted_order = self ._user_requested [identifier ]
110+ except KeyError :
111+ reqeusted_order = math .inf
112+ parent_depths = (
113+ self ._known_depths [parent .name ] if parent is not None else 0.0
114+ for _ , parent in information [identifier ]
115+ )
116+ inferred_depth = min (d for d in parent_depths ) + 1.0
117+ self ._known_depths [identifier ] = inferred_depth
118+ else :
119+ inferred_depth = 1.0
120+
121+ reqeusted_order = self ._user_requested .get (identifier , math .inf )
124122
125123 # Requires-Python has only one candidate and the check is basically
126124 # free, so we always do it first to avoid needless work if it fails.
@@ -136,7 +134,16 @@ def _get_restrictive_rating(requirements):
136134 # while we work on "proper" branch pruning techniques.
137135 delay_this = identifier == "setuptools"
138136
139- return (not requires_python , delay_this , rating , order , identifier )
137+ return (
138+ not requires_python ,
139+ delay_this ,
140+ not direct ,
141+ not pinned ,
142+ inferred_depth ,
143+ reqeusted_order ,
144+ not unfree ,
145+ identifier ,
146+ )
140147
141148 def find_matches (
142149 self ,
0 commit comments