mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 19:34:08 +00:00 
			
		
		
		
	* Support TopologicalSorter type subscript * 📜🤖 Added by blurb_it. * Add TopologicalSorter to GenericAlias tests * Update Misc/NEWS.d/next/Library/2021-10-03-22-27-35.bpo-45359.LX_uxe.rst Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Batuhan Taskaya <isidentical@gmail.com>
		
			
				
	
	
		
			250 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			250 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from types import GenericAlias
 | 
						|
 | 
						|
__all__ = ["TopologicalSorter", "CycleError"]
 | 
						|
 | 
						|
_NODE_OUT = -1
 | 
						|
_NODE_DONE = -2
 | 
						|
 | 
						|
 | 
						|
class _NodeInfo:
 | 
						|
    __slots__ = "node", "npredecessors", "successors"
 | 
						|
 | 
						|
    def __init__(self, node):
 | 
						|
        # The node this class is augmenting.
 | 
						|
        self.node = node
 | 
						|
 | 
						|
        # Number of predecessors, generally >= 0. When this value falls to 0,
 | 
						|
        # and is returned by get_ready(), this is set to _NODE_OUT and when the
 | 
						|
        # node is marked done by a call to done(), set to _NODE_DONE.
 | 
						|
        self.npredecessors = 0
 | 
						|
 | 
						|
        # List of successor nodes. The list can contain duplicated elements as
 | 
						|
        # long as they're all reflected in the successor's npredecessors attribute.
 | 
						|
        self.successors = []
 | 
						|
 | 
						|
 | 
						|
class CycleError(ValueError):
 | 
						|
    """Subclass of ValueError raised by TopologicalSorter.prepare if cycles
 | 
						|
    exist in the working graph.
 | 
						|
 | 
						|
    If multiple cycles exist, only one undefined choice among them will be reported
 | 
						|
    and included in the exception. The detected cycle can be accessed via the second
 | 
						|
    element in the *args* attribute of the exception instance and consists in a list
 | 
						|
    of nodes, such that each node is, in the graph, an immediate predecessor of the
 | 
						|
    next node in the list. In the reported list, the first and the last node will be
 | 
						|
    the same, to make it clear that it is cyclic.
 | 
						|
    """
 | 
						|
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class TopologicalSorter:
 | 
						|
    """Provides functionality to topologically sort a graph of hashable nodes"""
 | 
						|
 | 
						|
    def __init__(self, graph=None):
 | 
						|
        self._node2info = {}
 | 
						|
        self._ready_nodes = None
 | 
						|
        self._npassedout = 0
 | 
						|
        self._nfinished = 0
 | 
						|
 | 
						|
        if graph is not None:
 | 
						|
            for node, predecessors in graph.items():
 | 
						|
                self.add(node, *predecessors)
 | 
						|
 | 
						|
    def _get_nodeinfo(self, node):
 | 
						|
        if (result := self._node2info.get(node)) is None:
 | 
						|
            self._node2info[node] = result = _NodeInfo(node)
 | 
						|
        return result
 | 
						|
 | 
						|
    def add(self, node, *predecessors):
 | 
						|
        """Add a new node and its predecessors to the graph.
 | 
						|
 | 
						|
        Both the *node* and all elements in *predecessors* must be hashable.
 | 
						|
 | 
						|
        If called multiple times with the same node argument, the set of dependencies
 | 
						|
        will be the union of all dependencies passed in.
 | 
						|
 | 
						|
        It is possible to add a node with no dependencies (*predecessors* is not provided)
 | 
						|
        as well as provide a dependency twice. If a node that has not been provided before
 | 
						|
        is included among *predecessors* it will be automatically added to the graph with
 | 
						|
        no predecessors of its own.
 | 
						|
 | 
						|
        Raises ValueError if called after "prepare".
 | 
						|
        """
 | 
						|
        if self._ready_nodes is not None:
 | 
						|
            raise ValueError("Nodes cannot be added after a call to prepare()")
 | 
						|
 | 
						|
        # Create the node -> predecessor edges
 | 
						|
        nodeinfo = self._get_nodeinfo(node)
 | 
						|
        nodeinfo.npredecessors += len(predecessors)
 | 
						|
 | 
						|
        # Create the predecessor -> node edges
 | 
						|
        for pred in predecessors:
 | 
						|
            pred_info = self._get_nodeinfo(pred)
 | 
						|
            pred_info.successors.append(node)
 | 
						|
 | 
						|
    def prepare(self):
 | 
						|
        """Mark the graph as finished and check for cycles in the graph.
 | 
						|
 | 
						|
        If any cycle is detected, "CycleError" will be raised, but "get_ready" can
 | 
						|
        still be used to obtain as many nodes as possible until cycles block more
 | 
						|
        progress. After a call to this function, the graph cannot be modified and
 | 
						|
        therefore no more nodes can be added using "add".
 | 
						|
        """
 | 
						|
        if self._ready_nodes is not None:
 | 
						|
            raise ValueError("cannot prepare() more than once")
 | 
						|
 | 
						|
        self._ready_nodes = [
 | 
						|
            i.node for i in self._node2info.values() if i.npredecessors == 0
 | 
						|
        ]
 | 
						|
        # ready_nodes is set before we look for cycles on purpose:
 | 
						|
        # if the user wants to catch the CycleError, that's fine,
 | 
						|
        # they can continue using the instance to grab as many
 | 
						|
        # nodes as possible before cycles block more progress
 | 
						|
        cycle = self._find_cycle()
 | 
						|
        if cycle:
 | 
						|
            raise CycleError(f"nodes are in a cycle", cycle)
 | 
						|
 | 
						|
    def get_ready(self):
 | 
						|
        """Return a tuple of all the nodes that are ready.
 | 
						|
 | 
						|
        Initially it returns all nodes with no predecessors; once those are marked
 | 
						|
        as processed by calling "done", further calls will return all new nodes that
 | 
						|
        have all their predecessors already processed. Once no more progress can be made,
 | 
						|
        empty tuples are returned.
 | 
						|
 | 
						|
        Raises ValueError if called without calling "prepare" previously.
 | 
						|
        """
 | 
						|
        if self._ready_nodes is None:
 | 
						|
            raise ValueError("prepare() must be called first")
 | 
						|
 | 
						|
        # Get the nodes that are ready and mark them
 | 
						|
        result = tuple(self._ready_nodes)
 | 
						|
        n2i = self._node2info
 | 
						|
        for node in result:
 | 
						|
            n2i[node].npredecessors = _NODE_OUT
 | 
						|
 | 
						|
        # Clean the list of nodes that are ready and update
 | 
						|
        # the counter of nodes that we have returned.
 | 
						|
        self._ready_nodes.clear()
 | 
						|
        self._npassedout += len(result)
 | 
						|
 | 
						|
        return result
 | 
						|
 | 
						|
    def is_active(self):
 | 
						|
        """Return ``True`` if more progress can be made and ``False`` otherwise.
 | 
						|
 | 
						|
        Progress can be made if cycles do not block the resolution and either there
 | 
						|
        are still nodes ready that haven't yet been returned by "get_ready" or the
 | 
						|
        number of nodes marked "done" is less than the number that have been returned
 | 
						|
        by "get_ready".
 | 
						|
 | 
						|
        Raises ValueError if called without calling "prepare" previously.
 | 
						|
        """
 | 
						|
        if self._ready_nodes is None:
 | 
						|
            raise ValueError("prepare() must be called first")
 | 
						|
        return self._nfinished < self._npassedout or bool(self._ready_nodes)
 | 
						|
 | 
						|
    def __bool__(self):
 | 
						|
        return self.is_active()
 | 
						|
 | 
						|
    def done(self, *nodes):
 | 
						|
        """Marks a set of nodes returned by "get_ready" as processed.
 | 
						|
 | 
						|
        This method unblocks any successor of each node in *nodes* for being returned
 | 
						|
        in the future by a call to "get_ready".
 | 
						|
 | 
						|
        Raises :exec:`ValueError` if any node in *nodes* has already been marked as
 | 
						|
        processed by a previous call to this method, if a node was not added to the
 | 
						|
        graph by using "add" or if called without calling "prepare" previously or if
 | 
						|
        node has not yet been returned by "get_ready".
 | 
						|
        """
 | 
						|
 | 
						|
        if self._ready_nodes is None:
 | 
						|
            raise ValueError("prepare() must be called first")
 | 
						|
 | 
						|
        n2i = self._node2info
 | 
						|
 | 
						|
        for node in nodes:
 | 
						|
 | 
						|
            # Check if we know about this node (it was added previously using add()
 | 
						|
            if (nodeinfo := n2i.get(node)) is None:
 | 
						|
                raise ValueError(f"node {node!r} was not added using add()")
 | 
						|
 | 
						|
            # If the node has not being returned (marked as ready) previously, inform the user.
 | 
						|
            stat = nodeinfo.npredecessors
 | 
						|
            if stat != _NODE_OUT:
 | 
						|
                if stat >= 0:
 | 
						|
                    raise ValueError(
 | 
						|
                        f"node {node!r} was not passed out (still not ready)"
 | 
						|
                    )
 | 
						|
                elif stat == _NODE_DONE:
 | 
						|
                    raise ValueError(f"node {node!r} was already marked done")
 | 
						|
                else:
 | 
						|
                    assert False, f"node {node!r}: unknown status {stat}"
 | 
						|
 | 
						|
            # Mark the node as processed
 | 
						|
            nodeinfo.npredecessors = _NODE_DONE
 | 
						|
 | 
						|
            # Go to all the successors and reduce the number of predecessors, collecting all the ones
 | 
						|
            # that are ready to be returned in the next get_ready() call.
 | 
						|
            for successor in nodeinfo.successors:
 | 
						|
                successor_info = n2i[successor]
 | 
						|
                successor_info.npredecessors -= 1
 | 
						|
                if successor_info.npredecessors == 0:
 | 
						|
                    self._ready_nodes.append(successor)
 | 
						|
            self._nfinished += 1
 | 
						|
 | 
						|
    def _find_cycle(self):
 | 
						|
        n2i = self._node2info
 | 
						|
        stack = []
 | 
						|
        itstack = []
 | 
						|
        seen = set()
 | 
						|
        node2stacki = {}
 | 
						|
 | 
						|
        for node in n2i:
 | 
						|
            if node in seen:
 | 
						|
                continue
 | 
						|
 | 
						|
            while True:
 | 
						|
                if node in seen:
 | 
						|
                    # If we have seen already the node and is in the
 | 
						|
                    # current stack we have found a cycle.
 | 
						|
                    if node in node2stacki:
 | 
						|
                        return stack[node2stacki[node] :] + [node]
 | 
						|
                    # else go on to get next successor
 | 
						|
                else:
 | 
						|
                    seen.add(node)
 | 
						|
                    itstack.append(iter(n2i[node].successors).__next__)
 | 
						|
                    node2stacki[node] = len(stack)
 | 
						|
                    stack.append(node)
 | 
						|
 | 
						|
                # Backtrack to the topmost stack entry with
 | 
						|
                # at least another successor.
 | 
						|
                while stack:
 | 
						|
                    try:
 | 
						|
                        node = itstack[-1]()
 | 
						|
                        break
 | 
						|
                    except StopIteration:
 | 
						|
                        del node2stacki[stack.pop()]
 | 
						|
                        itstack.pop()
 | 
						|
                else:
 | 
						|
                    break
 | 
						|
        return None
 | 
						|
 | 
						|
    def static_order(self):
 | 
						|
        """Returns an iterable of nodes in a topological order.
 | 
						|
 | 
						|
        The particular order that is returned may depend on the specific
 | 
						|
        order in which the items were inserted in the graph.
 | 
						|
 | 
						|
        Using this method does not require to call "prepare" or "done". If any
 | 
						|
        cycle is detected, :exc:`CycleError` will be raised.
 | 
						|
        """
 | 
						|
        self.prepare()
 | 
						|
        while self.is_active():
 | 
						|
            node_group = self.get_ready()
 | 
						|
            yield from node_group
 | 
						|
            self.done(*node_group)
 | 
						|
 | 
						|
    __class_getitem__ = classmethod(GenericAlias)
 |