From eb548fc9a3180aebeff088099f5ce42381f97bf2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 1 Jun 2021 03:19:50 +0800 Subject: [PATCH] Rework resolution ordering to consider "depth" --- news/9455.feature.rst | 2 + .../resolution/resolvelib/provider.py | 70 ++++++++----------- 2 files changed, 32 insertions(+), 40 deletions(-) create mode 100644 news/9455.feature.rst diff --git a/news/9455.feature.rst b/news/9455.feature.rst new file mode 100644 index 00000000000..4008a65c89a --- /dev/null +++ b/news/9455.feature.rst @@ -0,0 +1,2 @@ +New resolver: The order of dependencies resolution has been tweaked to traverse +the dependency graph in a more depth-first approach. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 0be58fd3ba8..c5c65a5320c 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -59,6 +59,7 @@ def __init__( self._ignore_dependencies = ignore_dependencies self._upgrade_strategy = upgrade_strategy self._user_requested = user_requested + self._known_depths = {key: 1.0 for key in user_requested} def identify(self, requirement_or_candidate): # type: (Union[Requirement, Candidate]) -> str @@ -78,47 +79,36 @@ def get_preference( Currently pip considers the followings in order: - * Prefer if any of the known requirements points to an explicit URL. - * If equal, prefer if any requirements contain ``===`` and ``==``. - * If equal, prefer if requirements include version constraints, e.g. - ``>=`` and ``<``. - * If equal, prefer user-specified (non-transitive) requirements, and - order user-specified requirements by the order they are specified. + * 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 ``==``. + * If equal, calculate an approximate "depth" and resolve requirements + closer to the user-specified requirements first. + * 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). """ + lookups = (r.get_candidate_lookup() for r, _ in information[identifier]) + candidate, ireqs = zip(*lookups) + operators = [ + specifier.operator + for specifier_set in (ireq.specifier for ireq in ireqs if ireq) + for specifier in specifier_set + ] + + direct = candidate is not None + pinned = any(op[:2] == "==" for op in operators) + free = bool(operators) + + parent_depths = ( + self._known_depths.get(parent.name, float("inf")) if parent else 0.0 + for _, parent in information[identifier] + ) + inferred_depth = min(d for d in parent_depths if d is not None) + 1.0 + depth = self._known_depths[identifier] = inferred_depth - def _get_restrictive_rating(requirements): - # type: (Iterable[Requirement]) -> int - """Rate how restrictive a set of requirements are. - - ``Requirement.get_candidate_lookup()`` returns a 2-tuple for - lookup. The first element is ``Optional[Candidate]`` and the - second ``Optional[InstallRequirement]``. - - * If the requirement is an explicit one, the explicitly-required - candidate is returned as the first element. - * If the requirement is based on a PEP 508 specifier, the backing - ``InstallRequirement`` is returned as the second element. - - We use the first element to check whether there is an explicit - requirement, and the second for equality operator. - """ - lookups = (r.get_candidate_lookup() for r in requirements) - cands, ireqs = zip(*lookups) - if any(cand is not None for cand in cands): - return 0 - spec_sets = (ireq.specifier for ireq in ireqs if ireq) - operators = [ - specifier.operator for spec_set in spec_sets for specifier in spec_set - ] - if any(op in ("==", "===") for op in operators): - return 1 - if operators: - return 2 - # A "bare" requirement without any version requirements. - return 3 - - rating = _get_restrictive_rating(r for r, _ in information[identifier]) order = self._user_requested.get(identifier, float("inf")) # HACK: Setuptools have a very long and solid backward compatibility @@ -129,9 +119,9 @@ def _get_restrictive_rating(requirements): # delaying Setuptools helps reduce branches the resolver has to check. # This serves as a temporary fix for issues like "apache-airlfow[all]" # while we work on "proper" branch pruning techniques. - delay_this = identifier == "setuptools" + delayed = identifier == "setuptools" - return (delay_this, rating, order, identifier) + return (delayed, not direct, not pinned, depth, order, free, identifier) def find_matches( self,