blob: 0faca2186b268cc5ac66e144952574b5f6ef01ee [file] [log] [blame]
Pablo Galindo2f172d82020-06-01 00:41:14 +01001:mod:`graphlib` --- Functionality to operate with graph-like structures
2=========================================================================
3
4.. module:: graphlib
5 :synopsis: Functionality to operate with graph-like structures
6
7
8**Source code:** :source:`Lib/graphlib.py`
9
10.. testsetup:: default
11
12 import graphlib
13 from graphlib import *
14
15--------------
16
17
18.. class:: TopologicalSorter(graph=None)
19
20 Provides functionality to topologically sort a graph of hashable nodes.
21
22 A topological order is a linear ordering of the vertices in a graph such that
23 for every directed edge u -> v from vertex u to vertex v, vertex u comes
24 before vertex v in the ordering. For instance, the vertices of the graph may
25 represent tasks to be performed, and the edges may represent constraints that
26 one task must be performed before another; in this example, a topological
27 ordering is just a valid sequence for the tasks. A complete topological
28 ordering is possible if and only if the graph has no directed cycles, that
29 is, if it is a directed acyclic graph.
30
31 If the optional *graph* argument is provided it must be a dictionary
32 representing a directed acyclic graph where the keys are nodes and the values
33 are iterables of all predecessors of that node in the graph (the nodes that
34 have edges that point to the value in the key). Additional nodes can be added
35 to the graph using the :meth:`~TopologicalSorter.add` method.
36
37 In the general case, the steps required to perform the sorting of a given
38 graph are as follows:
39
40 * Create an instance of the :class:`TopologicalSorter` with an optional
41 initial graph.
42 * Add additional nodes to the graph.
43 * Call :meth:`~TopologicalSorter.prepare` on the graph.
44 * While :meth:`~TopologicalSorter.is_active` is ``True``, iterate over
45 the nodes returned by :meth:`~TopologicalSorter.get_ready` and
46 process them. Call :meth:`~TopologicalSorter.done` on each node as it
47 finishes processing.
48
49 In case just an immediate sorting of the nodes in the graph is required and
50 no parallelism is involved, the convenience method
51 :meth:`TopologicalSorter.static_order` can be used directly:
52
53 .. doctest::
54
55 >>> graph = {"D": {"B", "C"}, "C": {"A"}, "B": {"A"}}
56 >>> ts = TopologicalSorter(graph)
57 >>> tuple(ts.static_order())
58 ('A', 'C', 'B', 'D')
59
60 The class is designed to easily support parallel processing of the nodes as
61 they become ready. For instance::
62
63 topological_sorter = TopologicalSorter()
64
65 # Add nodes to 'topological_sorter'...
66
67 topological_sorter.prepare()
68 while topological_sorter.is_active():
69 for node in topological_sorter.get_ready():
70 # Worker threads or processes take nodes to work on off the
71 # 'task_queue' queue.
72 task_queue.put(node)
73
74 # When the work for a node is done, workers put the node in
75 # 'finalized_tasks_queue' so we can get more nodes to work on.
76 # The definition of 'is_active()' guarantees that, at this point, at
77 # least one node has been placed on 'task_queue' that hasn't yet
78 # been passed to 'done()', so this blocking 'get()' must (eventually)
79 # succeed. After calling 'done()', we loop back to call 'get_ready()'
80 # again, so put newly freed nodes on 'task_queue' as soon as
81 # logically possible.
82 node = finalized_tasks_queue.get()
83 topological_sorter.done(node)
84
85 .. method:: add(node, *predecessors)
86
87 Add a new node and its predecessors to the graph. Both the *node* and all
88 elements in *predecessors* must be hashable.
89
90 If called multiple times with the same node argument, the set of
91 dependencies will be the union of all dependencies passed in.
92
93 It is possible to add a node with no dependencies (*predecessors* is not
94 provided) or to provide a dependency twice. If a node that has not been
95 provided before is included among *predecessors* it will be automatically
96 added to the graph with no predecessors of its own.
97
98 Raises :exc:`ValueError` if called after :meth:`~TopologicalSorter.prepare`.
99
100 .. method:: prepare()
101
102 Mark the graph as finished and check for cycles in the graph. If any cycle
103 is detected, :exc:`CycleError` will be raised, but
104 :meth:`~TopologicalSorter.get_ready` can still be used to obtain as many
105 nodes as possible until cycles block more progress. After a call to this
106 function, the graph cannot be modified, and therefore no more nodes can be
107 added using :meth:`~TopologicalSorter.add`.
108
109 .. method:: is_active()
110
111 Returns ``True`` if more progress can be made and ``False`` otherwise.
112 Progress can be made if cycles do not block the resolution and either
113 there are still nodes ready that haven't yet been returned by
114 :meth:`TopologicalSorter.get_ready` or the number of nodes marked
115 :meth:`TopologicalSorter.done` is less than the number that have been
116 returned by :meth:`TopologicalSorter.get_ready`.
117
118 The :meth:`~TopologicalSorter.__bool__` method of this class defers to
119 this function, so instead of::
120
121 if ts.is_active():
122 ...
123
Mario Šaško85160602020-09-03 12:00:10 +0200124 it is possible to simply do::
Pablo Galindo2f172d82020-06-01 00:41:14 +0100125
126 if ts:
127 ...
128
129 Raises :exc:`ValueError` if called without calling
130 :meth:`~TopologicalSorter.prepare` previously.
131
132 .. method:: done(*nodes)
133
134 Marks a set of nodes returned by :meth:`TopologicalSorter.get_ready` as
135 processed, unblocking any successor of each node in *nodes* for being
136 returned in the future by a call to :meth:`TopologicalSorter.get_ready`.
137
138 Raises :exc:`ValueError` if any node in *nodes* has already been marked as
139 processed by a previous call to this method or if a node was not added to
140 the graph by using :meth:`TopologicalSorter.add`, if called without
141 calling :meth:`~TopologicalSorter.prepare` or if node has not yet been
142 returned by :meth:`~TopologicalSorter.get_ready`.
143
144 .. method:: get_ready()
145
146 Returns a ``tuple`` with all the nodes that are ready. Initially it
147 returns all nodes with no predecessors, and once those are marked as
148 processed by calling :meth:`TopologicalSorter.done`, further calls will
149 return all new nodes that have all their predecessors already processed.
150 Once no more progress can be made, empty tuples are returned.
151
152 Raises :exc:`ValueError` if called without calling
153 :meth:`~TopologicalSorter.prepare` previously.
154
155 .. method:: static_order()
156
157 Returns an iterable of nodes in a topological order. Using this method
158 does not require to call :meth:`TopologicalSorter.prepare` or
159 :meth:`TopologicalSorter.done`. This method is equivalent to::
160
161 def static_order(self):
162 self.prepare()
163 while self.is_active():
164 node_group = self.get_ready()
165 yield from node_group
166 self.done(*node_group)
167
168 The particular order that is returned may depend on the specific order in
169 which the items were inserted in the graph. For example:
170
171 .. doctest::
172
173 >>> ts = TopologicalSorter()
174 >>> ts.add(3, 2, 1)
175 >>> ts.add(1, 0)
176 >>> print([*ts.static_order()])
177 [2, 0, 1, 3]
178
179 >>> ts2 = TopologicalSorter()
180 >>> ts2.add(1, 0)
181 >>> ts2.add(3, 2, 1)
182 >>> print([*ts2.static_order()])
183 [0, 2, 1, 3]
184
185 This is due to the fact that "0" and "2" are in the same level in the
186 graph (they would have been returned in the same call to
187 :meth:`~TopologicalSorter.get_ready`) and the order between them is
188 determined by the order of insertion.
189
190
191 If any cycle is detected, :exc:`CycleError` will be raised.
192
193 .. versionadded:: 3.9
194
195
196Exceptions
197----------
198The :mod:`graphlib` module defines the following exception classes:
199
200.. exception:: CycleError
201
202 Subclass of :exc:`ValueError` raised by :meth:`TopologicalSorter.prepare` if cycles exist
203 in the working graph. If multiple cycles exist, only one undefined choice among them will
204 be reported and included in the exception.
205
206 The detected cycle can be accessed via the second element in the :attr:`~CycleError.args`
207 attribute of the exception instance and consists in a list of nodes, such that each node is,
208 in the graph, an immediate predecessor of the next node in the list. In the reported list,
209 the first and the last node will be the same, to make it clear that it is cyclic.