1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

Rewrite get_topological_weights to allow cycles

We take the length for the longest path to any node from root, ignoring
any paths that contain any node twice (i.e. cycles).
This commit is contained in:
Pradyun Gedam 2020-04-24 19:58:52 +05:30
parent e6853336fe
commit d236004138
No known key found for this signature in database
GPG key ID: DA17C4B29CB32E4B

View file

@ -121,8 +121,10 @@ class Resolver(BaseResolver):
it. This helps ensure that the environment is kept consistent as they
get installed one-by-one.
The current implementation walks the resolved dependency graph, and
make sure every node has a greater "weight" than all its parents.
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.
"""
assert self._result is not None, "must call resolve() first"
@ -138,35 +140,48 @@ class Resolver(BaseResolver):
def get_topological_weights(graph):
# type: (Graph) -> Dict[str, int]
# type: (Graph) -> 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.
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
don't get stuck in a cycle.
When assigning weight, the longer path (i.e. larger length) is preferred.
"""
visited = set() # type: Set[str]
weights = {}
path = [] # type: List[Optional[str]]
weights = {} # type: Dict[Optional[str], int]
key_count = len(graph)
while len(weights) < key_count:
progressed = False
for key in graph:
if key in weights:
continue
parents = list(graph.iter_parents(key))
if not all(p in weights for p in parents):
continue
if parents:
weight = max(weights[p] for p in parents) + 1
else:
weight = 0
weights[key] = weight
progressed = True
def visit(node):
# type: (Optional[str]) -> None
if node in path:
# We hit a cycle, so we'll break it here.
return
# FIXME: This check will fail if there are unbreakable cycles.
# Implement something to forcifully break them up to continue.
if not progressed:
raise InstallationError(
"Could not determine installation order due to cicular "
"dependency."
)
# Time to visit the children!
path.append(node)
for child in graph.iter_children(node):
visit(child)
popped = path.pop()
assert popped == node, "Sanity check failed. Please file a bug report."
last_known_parent_count = weights.get(node, 0)
weights[node] = max(last_known_parent_count, len(path))
# `None` is guaranteed to be the root node by resolvelib.
visit(None)
# Sanity checks
assert weights[None] == 0
assert len(weights) == len(graph)
return weights