summaryrefslogtreecommitdiff
path: root/src/pip/_internal/resolution/resolvelib/resolver.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/pip/_internal/resolution/resolvelib/resolver.py')
-rw-r--r--src/pip/_internal/resolution/resolvelib/resolver.py100
1 files changed, 63 insertions, 37 deletions
diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py
index f89afaf43..32ef7899b 100644
--- a/src/pip/_internal/resolution/resolvelib/resolver.py
+++ b/src/pip/_internal/resolution/resolvelib/resolver.py
@@ -19,8 +19,6 @@ from pip._internal.resolution.resolvelib.reporter import (
PipDebuggingReporter,
PipReporter,
)
-from pip._internal.utils.deprecation import deprecated
-from pip._internal.utils.filetypes import is_archive_file
from .base import Candidate, Requirement
from .factory import Factory
@@ -49,6 +47,7 @@ class Resolver(BaseResolver):
ignore_requires_python: bool,
force_reinstall: bool,
upgrade_strategy: str,
+ suppress_build_failures: bool,
py_version_info: Optional[Tuple[int, ...]] = None,
):
super().__init__()
@@ -63,6 +62,7 @@ class Resolver(BaseResolver):
force_reinstall=force_reinstall,
ignore_installed=ignore_installed,
ignore_requires_python=ignore_requires_python,
+ suppress_build_failures=suppress_build_failures,
py_version_info=py_version_info,
)
self.ignore_dependencies = ignore_dependencies
@@ -136,25 +136,6 @@ class Resolver(BaseResolver):
)
continue
- looks_like_sdist = (
- is_archive_file(candidate.source_link.file_path)
- and candidate.source_link.ext != ".zip"
- )
- if looks_like_sdist:
- # is a local sdist -- show a deprecation warning!
- reason = (
- "Source distribution is being reinstalled despite an "
- "installed package having the same name and version as "
- "the installed package."
- )
- replacement = "use --force-reinstall"
- deprecated(
- reason=reason,
- replacement=replacement,
- gone_in="21.3",
- issue=8711,
- )
-
# is a local sdist or path -- reinstall
ireq.should_reinstall = True
else:
@@ -192,17 +173,19 @@ class Resolver(BaseResolver):
get installed one-by-one.
The current implementation creates a topological ordering of the
- dependency graph, while breaking any cycles in the graph at arbitrary
- points. We make no guarantees about where the cycle would be broken,
- other than they would be broken.
+ dependency graph, giving more weight to packages with less
+ or no dependencies, while breaking any cycles in the graph at
+ arbitrary points. We make no guarantees about where the cycle
+ would be broken, other than it *would* be broken.
"""
assert self._result is not None, "must call resolve() first"
+ if not req_set.requirements:
+ # Nothing is left to install, so we do not need an order.
+ return []
+
graph = self._result.graph
- weights = get_topological_weights(
- graph,
- expected_node_count=len(self._result.mapping) + 1,
- )
+ weights = get_topological_weights(graph, set(req_set.requirements.keys()))
sorted_items = sorted(
req_set.requirements.items(),
@@ -213,23 +196,32 @@ class Resolver(BaseResolver):
def get_topological_weights(
- graph: "DirectedGraph[Optional[str]]", expected_node_count: int
+ graph: "DirectedGraph[Optional[str]]", requirement_keys: Set[str]
) -> Dict[Optional[str], int]:
"""Assign weights to each node based on how "deep" they are.
This implementation may change at any point in the future without prior
notice.
- We take the length for the longest path to any node from root, ignoring any
- paths that contain a single node twice (i.e. cycles). This is done through
- a depth-first search through the graph, while keeping track of the path to
- the node.
+ We first simplify the dependency graph by pruning any leaves and giving them
+ the highest weight: a package without any dependencies should be installed
+ first. This is done again and again in the same way, giving ever less weight
+ to the newly found leaves. The loop stops when no leaves are left: all
+ remaining packages have at least one dependency left in the graph.
+
+ Then we continue with the remaining graph, by taking the length for the
+ longest path to any node from root, ignoring any paths that contain a single
+ node twice (i.e. cycles). This is done through a depth-first search through
+ the graph, while keeping track of the path to the node.
Cycles in the graph result would result in node being revisited while also
- being it's own path. In this case, take no action. This helps ensure we
+ being on its own path. In this case, take no action. This helps ensure we
don't get stuck in a cycle.
When assigning weight, the longer path (i.e. larger length) is preferred.
+
+ We are only interested in the weights of packages that are in the
+ requirement_keys.
"""
path: Set[Optional[str]] = set()
weights: Dict[Optional[str], int] = {}
@@ -245,15 +237,49 @@ def get_topological_weights(
visit(child)
path.remove(node)
+ if node not in requirement_keys:
+ return
+
last_known_parent_count = weights.get(node, 0)
weights[node] = max(last_known_parent_count, len(path))
+ # Simplify the graph, pruning leaves that have no dependencies.
+ # This is needed for large graphs (say over 200 packages) because the
+ # `visit` function is exponentially slower then, taking minutes.
+ # See https://github.com/pypa/pip/issues/10557
+ # We will loop until we explicitly break the loop.
+ while True:
+ leaves = set()
+ for key in graph:
+ if key is None:
+ continue
+ for _child in graph.iter_children(key):
+ # This means we have at least one child
+ break
+ else:
+ # No child.
+ leaves.add(key)
+ if not leaves:
+ # We are done simplifying.
+ break
+ # Calculate the weight for the leaves.
+ weight = len(graph) - 1
+ for leaf in leaves:
+ if leaf not in requirement_keys:
+ continue
+ weights[leaf] = weight
+ # Remove the leaves from the graph, making it simpler.
+ for leaf in leaves:
+ graph.remove(leaf)
+
+ # Visit the remaining graph.
# `None` is guaranteed to be the root node by resolvelib.
visit(None)
- # Sanity checks
- assert weights[None] == 0
- assert len(weights) == expected_node_count
+ # Sanity check: all requirement keys should be in the weights,
+ # and no other keys should be in the weights.
+ difference = set(weights.keys()).difference(requirement_keys)
+ assert not difference, difference
return weights