From 1c474143a54720887554531b2258b9ae35dd66b6 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Tue, 4 Mar 2025 10:45:01 +0000 Subject: [PATCH 01/12] Add graphlib.reverse(), graphlib.as_transitive() --- Doc/library/graphlib.rst | 67 +++++++++++++++- Doc/whatsnew/3.14.rst | 9 +++ Lib/graphlib.py | 77 +++++++++++++++++- Lib/test/test_graphlib.py | 78 +++++++++++++++++++ ...-03-05-10-43-31.gh-issue-129847.dug2ca.rst | 2 + 5 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst diff --git a/Doc/library/graphlib.rst b/Doc/library/graphlib.rst index a0b16576fad219..40c51c088af94c 100644 --- a/Doc/library/graphlib.rst +++ b/Doc/library/graphlib.rst @@ -14,6 +14,8 @@ -------------- +Topological Ordering +-------------------- .. class:: TopologicalSorter(graph=None) @@ -194,15 +196,74 @@ .. versionadded:: 3.9 +Graph Functions +--------------- + +Some functions are provided to work with directed acyclic graph structures using +the same representation accepted by :meth:`TopologicalSorter.__init__`: a +mapping of nodes to iterables of predecessors. + +Nodes can be any :term:`hashable` object. Duplicate edges are ignored. + +For example, it can be useful to construct a TopologicalSorter that processes +a graph in reverse order. This can be done by passing the :func:`reverse` of a +graph to the constructor of :class:`TopologicalSorter`:: + + ts = TopologicalSorter(reverse(graph)) + + +.. function:: reverse(graph) + + Return a new graph with the edges reversed. + + The *graph* argument must be a dictionary representing a directed graph + where the keys are nodes and the values are iterables of predecessors. Keys + and predecessors must be hashable. + + Return a dict mapping nodes to sets of successors. The returned graph will + include a key for all nodes in the input graph, possibly with an empty set + of successors:: + + >>> reverse({"a": ["b", "c"], "d": [], "c": ["e"]}) + {'b': {'a'}, 'c': {'a'}, 'a': set(), 'd': set(), 'e': {'c'}} + + .. versionadded:: next + + +.. function:: as_transitive(graph) + + Compute the transitive closure of a dependency graph. + + The *graph* argument must be a dictionary representing a directed graph + where the keys are nodes and the values are iterables of predecessors. Keys + and predecessors must be hashable. + + If A is a direct predecessor of B, and B is a direct predecessor of C, + then A is a transitive predecessor of C. The returned dict maps each key in + the input graph to a sets of all such transitive predecessors for that key. + + If the input graph contains cycles, raise CycleError. + + Nodes that do not appear as keys in the input graph, but appear as + predecessors of other nodes, will not be included as keys in the returned + transitive graph:: + + >>> as_transitive({"a": ["b"], "b": ["c"]}) + {'a': {'b', 'c'}, 'b': {'c'}} + + .. versionadded:: next + + Exceptions ---------- The :mod:`graphlib` module defines the following exception classes: .. exception:: CycleError - Subclass of :exc:`ValueError` raised by :meth:`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. + Subclass of :exc:`ValueError` raised by :meth:`TopologicalSorter.prepare` and + :func:`as_transitive` when a cycle is detected in the 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 :attr:`~BaseException.args` attribute of the exception instance and consists in a list of nodes, such that each node is, diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 6898b50b2c932a..bd16c0d4401df6 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -584,6 +584,15 @@ getopt * Add support for returning intermixed options and non-option arguments in order. (Contributed by Serhiy Storchaka in :gh:`126390`.) + +graphlib +-------- + +* Add :func:`graphlib.reverse` and :func:`graphlib.as_transitive` to work with + directed acyclic graphs represented as dicts. + (Contributed by Daniel Pope in :gh:`129847`.) + + http ---- diff --git a/Lib/graphlib.py b/Lib/graphlib.py index 82f33fb5cf312c..1cecba1d30f1c1 100644 --- a/Lib/graphlib.py +++ b/Lib/graphlib.py @@ -1,6 +1,6 @@ from types import GenericAlias -__all__ = ["TopologicalSorter", "CycleError"] +__all__ = ["TopologicalSorter", "CycleError", "reverse", "as_transitive"] _NODE_OUT = -1 _NODE_DONE = -2 @@ -248,3 +248,78 @@ def static_order(self): self.done(*node_group) __class_getitem__ = classmethod(GenericAlias) + + +def reverse(graph): + """Reverse the direction of the edges in a directed graph. + + Given a mapping from nodes to collections of their dependencies, + construct a dict mapping each node to the set of nodes that depend on it. + + Nodes that have no dependents appear as keys in the result, with an empty + set as value. + + For example: + + >>> reverse({"a": ["b", "c"], "d": []}) + {'b': {'a'}, 'c': {'a'}, 'a': set(), 'd': set()} + + """ + result = {} + for node, deps in graph.items(): + for dep in deps: + if dep not in result: + result[dep] = {node} + else: + result[dep].add(node) + if node not in result: + result[node] = set() + return result + + +def as_transitive(graph): + """Compute the transitive closure of a dependency graph. + + If the input graph contains cycles, raise CycleError. + + The returned dict will contain the same keys as the input graph, but the + values will be sets of transitive predecessors of the key, rather than + direct predecessors. + + Examples: + >>> as_transitive({"a": ["b"], "b": ["c"]}) + {'a': {'b', 'c'}, 'b': {'c'}} + """ + unprocessed = dict(graph) + transitive_graph = {node: set() for node in graph} + + while unprocessed: + node, deps = unprocessed.popitem() + + stack = [iter(deps)] + path = [node] # Ordering for cycle detection + seen = {node} # Fast test for cycle detection + + while stack: + try: + child = next(stack[-1]) + except StopIteration: + stack.pop() + seen.remove(path.pop()) + continue + + if child in seen: + cycle = [child, *reversed(path[path.index(child):])] + raise CycleError("nodes are in a cycle", cycle) + + if (deps := unprocessed.pop(child, None)) is not None: + if deps: + stack.append(iter(deps)) + path.append(child) + seen.add(child) + continue + + transitive_graph[node].add(child) + transitive_graph[node].update(transitive_graph.get(child, ())) + + return transitive_graph diff --git a/Lib/test/test_graphlib.py b/Lib/test/test_graphlib.py index 5f38af4024c5b0..6376346f9782c3 100644 --- a/Lib/test/test_graphlib.py +++ b/Lib/test/test_graphlib.py @@ -248,5 +248,83 @@ def check_order_with_hash_seed(seed): self.assertNotEqual(run2, "") self.assertEqual(run1, run2) + +class TestReverse(unittest.TestCase): + """Tests for graphlib.reverse().""" + + def test_reverse_empty(self): + """An empty graph has an empty reverse.""" + self.assertEqual(graphlib.reverse({}), {}) + + def test_reverse_simple(self): + """We can reverse a simple graph.""" + graph = {"a": ["b", "c"]} + expected = {"b": {"a"}, "c": {"a"}, "a": set()} + self.assertEqual(graphlib.reverse(graph), expected) + + def test_reverse_with_empty_dependencies(self): + """Nodes with no predecessors are included in the output.""" + graph = {"a": []} + expected = {"a": set()} + self.assertEqual(graphlib.reverse(graph), expected) + + def test_reverse_with_int_keys(self): + """Nodes may be any hashable type, such as int.""" + graph = {1: {2, 3}, 2: {3}} + expected = {2: {1}, 3: {1, 2}, 1: set()} + self.assertEqual(graphlib.reverse(graph), expected) + + +class TestAsTransitive(unittest.TestCase): + """Tests for graphlib.as_transitive().""" + + def test_as_transitive_empty(self): + """An empty graph has an empty transitive closure.""" + self.assertEqual(graphlib.as_transitive({}), {}) + + def test_as_transitive_no_dependencies(self): + """Nodes with no predecessors are included in the output.""" + graph = {"a": [], "b": []} + expected = {"a": set(), "b": set()} + self.assertEqual(graphlib.as_transitive(graph), expected) + + def test_as_transitive_simple(self): + """We can compute the transitive closure of a simple graph. + + Given the input does not include "d" as a key, the output omits it. + """ + graph = {"a": ["b"], "b": ["c", "e"], "c": ["d"]} + expected = {"a": {"b", "c", "d", "e"}, "b": {"c", "d", "e"}, "c": {"d"}} + self.assertEqual(graphlib.as_transitive(graph), expected) + + def test_as_transitive_disjoint(self): + """We compute the transitive closure of disjoint subgraphs.""" + graph = {"a": "b", "b": "c", 1: [2], 2: [3]} + expected = {"a": {"b", "c"}, "b": {"c"}, 1: {2, 3}, 2: {3}} + self.assertEqual(graphlib.as_transitive(graph), expected) + + def test_as_transitive_with_tuple_keys(self): + """Nodes may be any hashable type, such as tuple.""" + graph = {('a',): [('b',)], ('b',): [('c',)]} + expected = {('a',): {('b',), ('c',)}, ('b',): {('c',)}} + self.assertEqual(graphlib.as_transitive(graph), expected) + + def test_as_transitive_with_int_keys_and_list_values(self): + """Nodes may be any hashable type, such as int.""" + graph = {1: [2], 2: [3], 3: []} + expected = {1: {2, 3}, 2: {3}, 3: set()} + self.assertEqual(graphlib.as_transitive(graph), expected) + + def test_as_transitive_cyclic(self): + """Raise CycleError if a cycle is detected.""" + graph = {"a": ["b"], "b": ["c"], "c": ["a"]} + with self.assertRaises(graphlib.CycleError) as cm: + graphlib.as_transitive(graph) + self.assertEqual( + cm.exception.args, + ("nodes are in a cycle", ["c", "b", "a", "c"]), + ) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst new file mode 100644 index 00000000000000..29b62cc422a5f6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst @@ -0,0 +1,2 @@ +Add :func:`graphlib.reverse` and :func:`graphlib.as_transitive` to work with +directed acyclic graphs represented as dicts. From f395782c2329c3759bb0fdea273edceaf32897c4 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Thu, 6 Mar 2025 11:02:53 +0000 Subject: [PATCH 02/12] Collapse redundant nested if --- Lib/graphlib.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Lib/graphlib.py b/Lib/graphlib.py index 1cecba1d30f1c1..d635e972c9730c 100644 --- a/Lib/graphlib.py +++ b/Lib/graphlib.py @@ -286,7 +286,7 @@ def as_transitive(graph): values will be sets of transitive predecessors of the key, rather than direct predecessors. - Examples: + For example: >>> as_transitive({"a": ["b"], "b": ["c"]}) {'a': {'b', 'c'}, 'b': {'c'}} """ @@ -296,6 +296,8 @@ def as_transitive(graph): while unprocessed: node, deps = unprocessed.popitem() + # We use a recursive algorithm but don't use Python's stack in + # order to avoid hitting the recursion limit. stack = [iter(deps)] path = [node] # Ordering for cycle detection seen = {node} # Fast test for cycle detection @@ -312,12 +314,12 @@ def as_transitive(graph): cycle = [child, *reversed(path[path.index(child):])] raise CycleError("nodes are in a cycle", cycle) - if (deps := unprocessed.pop(child, None)) is not None: - if deps: - stack.append(iter(deps)) - path.append(child) - seen.add(child) - continue + # "Recurse" into the child's deps if it has any + if deps := unprocessed.pop(child, None): + stack.append(iter(deps)) + path.append(child) + seen.add(child) + continue transitive_graph[node].add(child) transitive_graph[node].update(transitive_graph.get(child, ())) From 272fe8a7a51f2296e2d1ce7d430e34f9bfb71e18 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Thu, 6 Mar 2025 11:03:43 +0000 Subject: [PATCH 03/12] Fix reST link to TopologicalSorter constructor --- Doc/library/graphlib.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/graphlib.rst b/Doc/library/graphlib.rst index 40c51c088af94c..abbc9cb940424a 100644 --- a/Doc/library/graphlib.rst +++ b/Doc/library/graphlib.rst @@ -200,8 +200,8 @@ Graph Functions --------------- Some functions are provided to work with directed acyclic graph structures using -the same representation accepted by :meth:`TopologicalSorter.__init__`: a -mapping of nodes to iterables of predecessors. +the same representation accepted by :class:`.TopologicalSorter`: a mapping of +nodes to iterables of predecessors. Nodes can be any :term:`hashable` object. Duplicate edges are ignored. From c0c1f0629e66ad7ef9ede41d1d80b880a9bd5112 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Thu, 6 Mar 2025 11:15:29 +0000 Subject: [PATCH 04/12] Add attribution to blurb Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- .../2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst index 29b62cc422a5f6..d7f63e44b986b0 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-03-05-10-43-31.gh-issue-129847.dug2ca.rst @@ -1,2 +1,3 @@ Add :func:`graphlib.reverse` and :func:`graphlib.as_transitive` to work with directed acyclic graphs represented as dicts. +Patch by Daniel Pope. From 4ccfe801cbb34b3a3b3a2b8cd8bf6f0acbb13de1 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Thu, 6 Mar 2025 11:20:51 +0000 Subject: [PATCH 05/12] Fix recursion logic --- Lib/graphlib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/graphlib.py b/Lib/graphlib.py index d635e972c9730c..85d10ffcef4e69 100644 --- a/Lib/graphlib.py +++ b/Lib/graphlib.py @@ -306,6 +306,7 @@ def as_transitive(graph): try: child = next(stack[-1]) except StopIteration: + # "Return" stack.pop() seen.remove(path.pop()) continue @@ -321,6 +322,7 @@ def as_transitive(graph): seen.add(child) continue + node = path[-1] transitive_graph[node].add(child) transitive_graph[node].update(transitive_graph.get(child, ())) From f10d12d46fefceccff844375aabf6016a5c137b4 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Thu, 6 Mar 2025 11:38:49 +0000 Subject: [PATCH 06/12] Add tests for as_transitive() in all orders --- Lib/test/test_graphlib.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Lib/test/test_graphlib.py b/Lib/test/test_graphlib.py index 6376346f9782c3..46b3757e84eb4a 100644 --- a/Lib/test/test_graphlib.py +++ b/Lib/test/test_graphlib.py @@ -1,6 +1,7 @@ import graphlib import os import unittest +from itertools import permutations from test.support.script_helper import assert_python_ok @@ -297,6 +298,31 @@ def test_as_transitive_simple(self): expected = {"a": {"b", "c", "d", "e"}, "b": {"c", "d", "e"}, "c": {"d"}} self.assertEqual(graphlib.as_transitive(graph), expected) + def test_as_transitive_disordered_chain(self): + """We can compute the transitive closure of a graph in any order.""" + graph = {"a": "b", "b": "c", "c": "d", "d": "e"}.items() + expected = { + "a": {"b", "c", "d", "e"}, + "b": {"c", "d", "e"}, + "c": {"d", "e"}, + "d": {"e"} + } + for perm in permutations(graph): + with self.subTest(perm): + self.assertEqual(graphlib.as_transitive(dict(perm)), expected) + + def test_as_transitive_disordered_wide(self): + """We can compute the transitive closure of a graph in any order.""" + graph = {"a": "bc", "c": "de", "e": "f"}.items() + expected = { + "a": {"b", "c", "d", "e", "f"}, + "c": {"d", "e", "f"}, + "e": {"f"} + } + for perm in permutations(graph): + with self.subTest(perm): + self.assertEqual(graphlib.as_transitive(dict(perm)), expected) + def test_as_transitive_disjoint(self): """We compute the transitive closure of disjoint subgraphs.""" graph = {"a": "b", "b": "c", 1: [2], 2: [3]} From 53a3a1b954286fe7aff7638d03a62740962605aa Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Thu, 6 Mar 2025 21:02:55 +0000 Subject: [PATCH 07/12] Fix failing tests --- Lib/graphlib.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Lib/graphlib.py b/Lib/graphlib.py index 85d10ffcef4e69..b5f123384e7aa0 100644 --- a/Lib/graphlib.py +++ b/Lib/graphlib.py @@ -308,8 +308,14 @@ def as_transitive(graph): except StopIteration: # "Return" stack.pop() - seen.remove(path.pop()) - continue + if not stack: + break + child = path.pop() + seen.remove(child) + node = path[-1] + transitive_graph[node].update(transitive_graph.get(child, ())) + + transitive_graph[node].add(child) if child in seen: cycle = [child, *reversed(path[path.index(child):])] @@ -317,13 +323,11 @@ def as_transitive(graph): # "Recurse" into the child's deps if it has any if deps := unprocessed.pop(child, None): + node = child stack.append(iter(deps)) path.append(child) seen.add(child) continue - - node = path[-1] - transitive_graph[node].add(child) transitive_graph[node].update(transitive_graph.get(child, ())) return transitive_graph From 0112e603b68a61e4ae087f4c17693c0c6896c09b Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Fri, 7 Mar 2025 08:31:57 +0000 Subject: [PATCH 08/12] Fix style issues in docs/docstrings --- Doc/library/graphlib.rst | 6 ++++-- Lib/graphlib.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/library/graphlib.rst b/Doc/library/graphlib.rst index abbc9cb940424a..92f10718d1c44d 100644 --- a/Doc/library/graphlib.rst +++ b/Doc/library/graphlib.rst @@ -207,7 +207,9 @@ Nodes can be any :term:`hashable` object. Duplicate edges are ignored. For example, it can be useful to construct a TopologicalSorter that processes a graph in reverse order. This can be done by passing the :func:`reverse` of a -graph to the constructor of :class:`TopologicalSorter`:: +graph to the constructor of :class:`TopologicalSorter`: + +.. code-block:: python ts = TopologicalSorter(reverse(graph)) @@ -242,7 +244,7 @@ graph to the constructor of :class:`TopologicalSorter`:: then A is a transitive predecessor of C. The returned dict maps each key in the input graph to a sets of all such transitive predecessors for that key. - If the input graph contains cycles, raise CycleError. + If the input graph contains cycles, raise a CycleError. Nodes that do not appear as keys in the input graph, but appear as predecessors of other nodes, will not be included as keys in the returned diff --git a/Lib/graphlib.py b/Lib/graphlib.py index b5f123384e7aa0..1740bc51198791 100644 --- a/Lib/graphlib.py +++ b/Lib/graphlib.py @@ -280,7 +280,7 @@ def reverse(graph): def as_transitive(graph): """Compute the transitive closure of a dependency graph. - If the input graph contains cycles, raise CycleError. + If the input graph contains cycles, raise a CycleError. The returned dict will contain the same keys as the input graph, but the values will be sets of transitive predecessors of the key, rather than From cd28a71a1a6e931ea3b959154681793574f55d4b Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Fri, 7 Mar 2025 08:32:35 +0000 Subject: [PATCH 09/12] Add a test for graphs bigger than the recursion limit --- Lib/test/test_graphlib.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_graphlib.py b/Lib/test/test_graphlib.py index 46b3757e84eb4a..05edfe632d6186 100644 --- a/Lib/test/test_graphlib.py +++ b/Lib/test/test_graphlib.py @@ -1,4 +1,5 @@ import graphlib +import sys import os import unittest from itertools import permutations @@ -323,6 +324,13 @@ def test_as_transitive_disordered_wide(self): with self.subTest(perm): self.assertEqual(graphlib.as_transitive(dict(perm)), expected) + def test_large_diameter_graph(self): + """We can compute the transitive closure of a large graph.""" + size = sys.getrecursionlimit() + graph = {k: [k + 1] for k in range(size)} + expected = {k: set(range(k + 1, size + 1)) for k in range(size)} + self.assertEqual(graphlib.as_transitive(graph), expected) + def test_as_transitive_disjoint(self): """We compute the transitive closure of disjoint subgraphs.""" graph = {"a": "b", "b": "c", 1: [2], 2: [3]} From d73260563eb1896eb27950abad17bdbb89b13962 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Fri, 7 Mar 2025 08:40:35 +0000 Subject: [PATCH 10/12] Cross-reference to CycleError --- Doc/library/graphlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/graphlib.rst b/Doc/library/graphlib.rst index 92f10718d1c44d..1a0e07a1d27490 100644 --- a/Doc/library/graphlib.rst +++ b/Doc/library/graphlib.rst @@ -244,7 +244,7 @@ graph to the constructor of :class:`TopologicalSorter`: then A is a transitive predecessor of C. The returned dict maps each key in the input graph to a sets of all such transitive predecessors for that key. - If the input graph contains cycles, raise a CycleError. + If the input graph contains cycles, raise a :exc:`CycleError`. Nodes that do not appear as keys in the input graph, but appear as predecessors of other nodes, will not be included as keys in the returned From 1889b935181df46c0077e305d2507a667a713d77 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Fri, 7 Mar 2025 09:29:26 +0000 Subject: [PATCH 11/12] Add test for iterator values --- Lib/test/test_graphlib.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lib/test/test_graphlib.py b/Lib/test/test_graphlib.py index 05edfe632d6186..88bac904d37019 100644 --- a/Lib/test/test_graphlib.py +++ b/Lib/test/test_graphlib.py @@ -276,6 +276,18 @@ def test_reverse_with_int_keys(self): expected = {2: {1}, 3: {1, 2}, 1: set()} self.assertEqual(graphlib.reverse(graph), expected) + def test_reverse_with_iterators(self): + """Values may be any iterable including iterators.""" + graph = {"a": iter("cd"), "b": iter("ace"), "e": iter("f")} + expected = { + "a": {"b"}, + "b": set(), + "c": {"a", "b"}, + "d": {"a"}, + "e": {"b"}, + "f": {"e"}, + } + self.assertEqual(graphlib.reverse(graph), expected) class TestAsTransitive(unittest.TestCase): """Tests for graphlib.as_transitive().""" @@ -349,6 +361,12 @@ def test_as_transitive_with_int_keys_and_list_values(self): expected = {1: {2, 3}, 2: {3}, 3: set()} self.assertEqual(graphlib.as_transitive(graph), expected) + def test_as_transitive_with_iterators(self): + """Values may be any iterable including iterators.""" + graph = {"a": iter("cd"), "b": iter("ace"), "e": iter("f")} + expected = {"a": {"c", "d"}, "b": {"c", "d", "e", "a", "f"}, "e": {"f"}} + self.assertEqual(graphlib.as_transitive(graph), expected) + def test_as_transitive_cyclic(self): """Raise CycleError if a cycle is detected.""" graph = {"a": ["b"], "b": ["c"], "c": ["a"]} From d257a77018cf99c2accf1064d3d65da373a0dc39 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Sun, 16 Mar 2025 08:29:56 +0000 Subject: [PATCH 12/12] Convert :: to explicit code-block directive --- Doc/library/graphlib.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Doc/library/graphlib.rst b/Doc/library/graphlib.rst index 1a0e07a1d27490..2dc2e22a9cacc7 100644 --- a/Doc/library/graphlib.rst +++ b/Doc/library/graphlib.rst @@ -224,7 +224,9 @@ graph to the constructor of :class:`TopologicalSorter`: Return a dict mapping nodes to sets of successors. The returned graph will include a key for all nodes in the input graph, possibly with an empty set - of successors:: + of successors: + + .. code-block:: pycon >>> reverse({"a": ["b", "c"], "d": [], "c": ["e"]}) {'b': {'a'}, 'c': {'a'}, 'a': set(), 'd': set(), 'e': {'c'}} @@ -248,7 +250,9 @@ graph to the constructor of :class:`TopologicalSorter`: Nodes that do not appear as keys in the input graph, but appear as predecessors of other nodes, will not be included as keys in the returned - transitive graph:: + transitive graph: + + .. code-block:: pycon >>> as_transitive({"a": ["b"], "b": ["c"]}) {'a': {'b', 'c'}, 'b': {'c'}}