Skip to content

Commit 99e6c26

Browse files
pablogsaltim-one
andauthored
bpo-17005: Add a class to perform topological sorting to the standard library (GH-11583)
Co-Authored-By: Tim Peters <[email protected]>
1 parent 79f89e6 commit 99e6c26

File tree

6 files changed

+738
-3
lines changed

6 files changed

+738
-3
lines changed

Doc/library/functools.rst

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@
88
.. moduleauthor:: Raymond Hettinger <[email protected]>
99
.. moduleauthor:: Nick Coghlan <[email protected]>
1010
.. moduleauthor:: Łukasz Langa <[email protected]>
11+
.. moduleauthor:: Pablo Galindo <[email protected]>
1112
.. sectionauthor:: Peter Harris <[email protected]>
1213

1314
**Source code:** :source:`Lib/functools.py`
1415

16+
.. testsetup:: default
17+
18+
import functools
19+
from functools import *
20+
1521
--------------
1622

1723
The :mod:`functools` module is for higher-order functions: functions that act on
@@ -512,6 +518,192 @@ The :mod:`functools` module defines the following functions:
512518
.. versionadded:: 3.8
513519

514520

521+
.. class:: TopologicalSorter(graph=None)
522+
523+
Provides functionality to topologically sort a graph of hashable nodes.
524+
525+
A topological order is a linear ordering of the vertices in a graph such that for
526+
every directed edge u -> v from vertex u to vertex v, vertex u comes before vertex
527+
v in the ordering. For instance, the vertices of the graph may represent tasks to
528+
be performed, and the edges may represent constraints that one task must be
529+
performed before another; in this example, a topological ordering is just a valid
530+
sequence for the tasks. A complete topological ordering is possible if and only if
531+
the graph has no directed cycles, that is, if it is a directed acyclic graph.
532+
533+
If the optional *graph* argument is provided it must be a dictionary representing
534+
a directed acyclic graph where the keys are nodes and the values are iterables of
535+
all predecessors of that node in the graph (the nodes that have edges that point
536+
to the value in the key). Additional nodes can be added to the graph using the
537+
:meth:`~TopologicalSorter.add` method.
538+
539+
In the general case, the steps required to perform the sorting of a given graph
540+
are as follows:
541+
542+
* Create an instance of the :class:`TopologicalSorter` with an optional initial graph.
543+
* Add additional nodes to the graph.
544+
* Call :meth:`~TopologicalSorter.prepare` on the graph.
545+
* While :meth:`~TopologicalSorter.is_active` is ``True``, iterate over the
546+
nodes returned by :meth:`~TopologicalSorter.get_ready` and process them.
547+
Call :meth:`~TopologicalSorter.done` on each node as it finishes processing.
548+
549+
In case just an immediate sorting of the nodes in the graph is required and
550+
no parallelism is involved, the convenience method :meth:`TopologicalSorter.static_order`
551+
can be used directly. For example, this method can be used to implement a simple
552+
version of the C3 linearization algorithm used by Python to calculate the Method
553+
Resolution Order (MRO) of a derived class:
554+
555+
.. doctest::
556+
557+
>>> class A: pass
558+
>>> class B(A): pass
559+
>>> class C(A): pass
560+
>>> class D(B, C): pass
561+
562+
>>> D.__mro__
563+
(<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
564+
565+
>>> graph = {D: {B, C}, C: {A}, B: {A}, A:{object}}
566+
>>> ts = TopologicalSorter(graph)
567+
>>> topological_order = tuple(ts.static_order())
568+
>>> tuple(reversed(topological_order))
569+
(<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
570+
571+
The class is designed to easily support parallel processing of the nodes as they
572+
become ready. For instance::
573+
574+
topological_sorter = TopologicalSorter()
575+
576+
# Add nodes to 'topological_sorter'...
577+
578+
topological_sorter.prepare()
579+
while topological_sorter.is_active():
580+
for node in topological_sorter.get_ready():
581+
# Worker threads or processes take nodes to work on off the
582+
# 'task_queue' queue.
583+
task_queue.put(node)
584+
585+
# When the work for a node is done, workers put the node in
586+
# 'finalized_tasks_queue' so we can get more nodes to work on.
587+
# The definition of 'is_active()' guarantees that, at this point, at
588+
# least one node has been placed on 'task_queue' that hasn't yet
589+
# been passed to 'done()', so this blocking 'get()' must (eventually)
590+
# succeed. After calling 'done()', we loop back to call 'get_ready()'
591+
# again, so put newly freed nodes on 'task_queue' as soon as
592+
# logically possible.
593+
node = finalized_tasks_queue.get()
594+
topological_sorter.done(node)
595+
596+
.. method:: add(node, *predecessors)
597+
598+
Add a new node and its predecessors to the graph. Both the *node* and
599+
all elements in *predecessors* must be hashable.
600+
601+
If called multiple times with the same node argument, the set of dependencies
602+
will be the union of all dependencies passed in.
603+
604+
It is possible to add a node with no dependencies (*predecessors* is not
605+
provided) or to provide a dependency twice. If a node that has not been
606+
provided before is included among *predecessors* it will be automatically added
607+
to the graph with no predecessors of its own.
608+
609+
Raises :exc:`ValueError` if called after :meth:`~TopologicalSorter.prepare`.
610+
611+
.. method:: prepare()
612+
613+
Mark the graph as finished and check for cycles in the graph. If any cycle is
614+
detected, :exc:`CycleError` will be raised, but
615+
:meth:`~TopologicalSorter.get_ready` can still be used to obtain as many nodes
616+
as possible until cycles block more progress. After a call to this function,
617+
the graph cannot be modified, and therefore no more nodes can be added using
618+
:meth:`~TopologicalSorter.add`.
619+
620+
.. method:: is_active()
621+
622+
Returns ``True`` if more progress can be made and ``False`` otherwise. Progress
623+
can be made if cycles do not block the resolution and either there are still
624+
nodes ready that haven't yet been returned by
625+
:meth:`TopologicalSorter.get_ready` or the number of nodes marked
626+
:meth:`TopologicalSorter.done` is less than the number that have been returned
627+
by :meth:`TopologicalSorter.get_ready`.
628+
629+
The :meth:`~TopologicalSorter.__bool__` method of this class defers to this
630+
function, so instead of::
631+
632+
if ts.is_active():
633+
...
634+
635+
if possible to simply do::
636+
637+
if ts:
638+
...
639+
640+
Raises :exc:`ValueError` if called without calling :meth:`~TopologicalSorter.prepare`
641+
previously.
642+
643+
.. method:: done(*nodes)
644+
645+
Marks a set of nodes returned by :meth:`TopologicalSorter.get_ready` as
646+
processed, unblocking any successor of each node in *nodes* for being returned
647+
in the future by a call to :meth:`TopologicalSorter.get_ready`.
648+
649+
Raises :exc:`ValueError` if any node in *nodes* has already been marked as
650+
processed by a previous call to this method or if a node was not added to the
651+
graph by using :meth:`TopologicalSorter.add`, if called without calling
652+
:meth:`~TopologicalSorter.prepare` or if node has not yet been returned by
653+
:meth:`~TopologicalSorter.get_ready`.
654+
655+
.. method:: get_ready()
656+
657+
Returns a ``tuple`` with all the nodes that are ready. Initially it returns all
658+
nodes with no predecessors, and once those are marked as processed by calling
659+
:meth:`TopologicalSorter.done`, further calls will return all new nodes that
660+
have all their predecessors already processed. Once no more progress can be
661+
made, empty tuples are returned.
662+
made.
663+
664+
Raises :exc:`ValueError` if called without calling
665+
:meth:`~TopologicalSorter.prepare` previously.
666+
667+
.. method:: static_order()
668+
669+
Returns an iterable of nodes in a topological order. Using this method
670+
does not require to call :meth:`TopologicalSorter.prepare` or
671+
:meth:`TopologicalSorter.done`. This method is equivalent to::
672+
673+
def static_order(self):
674+
self.prepare()
675+
while self.is_active():
676+
node_group = self.get_ready()
677+
yield from node_group
678+
self.done(*node_group)
679+
680+
The particular order that is returned may depend on the specific order in
681+
which the items were inserted in the graph. For example:
682+
683+
.. doctest::
684+
685+
>>> ts = TopologicalSorter()
686+
>>> ts.add(3, 2, 1)
687+
>>> ts.add(1, 0)
688+
>>> print([*ts.static_order()])
689+
[2, 0, 1, 3]
690+
691+
>>> ts2 = TopologicalSorter()
692+
>>> ts2.add(1, 0)
693+
>>> ts2.add(3, 2, 1)
694+
>>> print([*ts2.static_order()])
695+
[0, 2, 1, 3]
696+
697+
This is due to the fact that "0" and "2" are in the same level in the graph (they
698+
would have been returned in the same call to :meth:`~TopologicalSorter.get_ready`)
699+
and the order between them is determined by the order of insertion.
700+
701+
702+
If any cycle is detected, :exc:`CycleError` will be raised.
703+
704+
.. versionadded:: 3.9
705+
706+
515707
.. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
516708

517709
Update a *wrapper* function to look like the *wrapped* function. The optional
@@ -621,3 +813,19 @@ differences. For instance, the :attr:`~definition.__name__` and :attr:`__doc__`
621813
are not created automatically. Also, :class:`partial` objects defined in
622814
classes behave like static methods and do not transform into bound methods
623815
during instance attribute look-up.
816+
817+
818+
Exceptions
819+
----------
820+
The :mod:`functools` module defines the following exception classes:
821+
822+
.. exception:: CycleError
823+
824+
Subclass of :exc:`ValueError` raised by :meth:`TopologicalSorter.prepare` if cycles exist
825+
in the working graph. If multiple cycles exist, only one undefined choice among them will
826+
be reported and included in the exception.
827+
828+
The detected cycle can be accessed via the second element in the :attr:`~CycleError.args`
829+
attribute of the exception instance and consists in a list of nodes, such that each node is,
830+
in the graph, an immediate predecessor of the next node in the list. In the reported list,
831+
the first and the last node will be the same, to make it clear that it is cyclic.

Doc/myfile.bz2

331 Bytes
Binary file not shown.

Doc/whatsnew/3.9.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ ftplib
166166
if the given timeout for their constructor is zero to prevent the creation of
167167
a non-blocking socket. (Contributed by Dong-hee Na in :issue:`39259`.)
168168

169+
functools
170+
---------
171+
172+
Add the :class:`functools.TopologicalSorter` class to offer functionality to perform
173+
topological sorting of graphs. (Contributed by Pablo Galindo, Tim Peters and Larry
174+
Hastings in :issue:`17005`.)
175+
169176
gc
170177
--
171178

0 commit comments

Comments
 (0)