-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
gh-129847: Add graphlib.reverse(), graphlib.as_transitive() #130875
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1c47414
f395782
272fe8a
c0c1f06
4ccfe80
f10d12d
53a3a1b
0112e60
cd28a71
d732605
1889b93
d257a77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,8 @@ | ||||||||||
import graphlib | ||||||||||
import sys | ||||||||||
import os | ||||||||||
Comment on lines
+2
to
3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: sorting
Suggested change
|
||||||||||
import unittest | ||||||||||
from itertools import permutations | ||||||||||
|
||||||||||
from test.support.script_helper import assert_python_ok | ||||||||||
|
||||||||||
|
@@ -248,5 +250,133 @@ 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) | ||||||||||
|
||||||||||
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().""" | ||||||||||
Comment on lines
+292
to
+293
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Could we add a test for such a pathological graph? |
||||||||||
|
||||||||||
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_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_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]} | ||||||||||
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_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"]} | ||||||||||
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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Add :func:`graphlib.reverse` and :func:`graphlib.as_transitive` to work with | ||
directed acyclic graphs represented as dicts. | ||
lordmauve marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Patch by Daniel Pope. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be re-targeted to 3.15.