forked from zulip/zulip
-
Notifications
You must be signed in to change notification settings - Fork 0
/
graph.py
138 lines (117 loc) · 3.89 KB
/
graph.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from collections import defaultdict
from typing import Callable, DefaultDict, Iterator, List, Optional, Set, Tuple
Edge = Tuple[str, str]
EdgeSet = Set[Edge]
class Graph:
def __init__(self, tuples):
# type: (EdgeSet) -> None
self.children = defaultdict(list) # type: DefaultDict[str, List[str]]
self.parents = defaultdict(list) # type: DefaultDict[str, List[str]]
self.nodes = set() # type: Set[str]
for parent, child in tuples:
self.parents[child].append(parent)
self.children[parent].append(child)
self.nodes.add(parent)
self.nodes.add(child)
def copy(self):
# type: () -> 'Graph'
return Graph(self.edges())
def num_edges(self):
# type: () -> int
return len(self.edges())
def minus_edge(self, edge):
# type: (Edge) -> 'Graph'
edges = self.edges().copy()
edges.remove(edge)
return Graph(edges)
def edges(self):
# type: () -> EdgeSet
s = set()
for parent in self.nodes:
for child in self.children[parent]:
s.add((parent, child))
return s
def remove_exterior_nodes(self):
# type: () -> None
still_work_to_do = True
while still_work_to_do:
still_work_to_do = False # for now
for node in self.nodes:
if self.is_exterior_node(node):
self.remove(node)
still_work_to_do = True
break
def is_exterior_node(self, node):
# type: (str) -> bool
parents = self.parents[node]
children = self.children[node]
if not parents:
return True
if not children:
return True
if len(parents) > 1 or len(children) > 1:
return False
# If our only parent and child are the same node, then we could
# effectively be collapsed into the parent, so don't add clutter.
return parents[0] == children[0]
def remove(self, node):
# type: (str) -> None
for parent in self.parents[node]:
self.children[parent].remove(node)
for child in self.children[node]:
self.parents[child].remove(node)
self.nodes.remove(node)
def report(self):
# type: () -> None
print('parents/children/module')
tups = sorted([
(len(self.parents[node]), len(self.children[node]), node)
for node in self.nodes])
for tup in tups:
print(tup)
def best_edge_to_remove(orig_graph, is_exempt):
# type: (Graph, Callable[[Edge], bool]) -> Optional[Edge]
# expects an already reduced graph as input
orig_edges = orig_graph.edges()
def get_choices():
# type: () -> Iterator[Tuple[int, Edge]]
for edge in orig_edges:
if is_exempt(edge):
continue
graph = orig_graph.minus_edge(edge)
graph.remove_exterior_nodes()
size = graph.num_edges()
yield (size, edge)
choices = list(get_choices())
if not choices:
return None
min_size, best_edge = min(choices)
if min_size >= orig_graph.num_edges():
raise Exception('no edges work here')
return best_edge
def make_dot_file(graph):
# type: (Graph) -> str
buffer = 'digraph G {\n'
for node in graph.nodes:
buffer += node + ';\n'
for child in graph.children[node]:
buffer += '{} -> {};\n'.format(node, child)
buffer += '}'
return buffer
def test():
# type: () -> None
graph = Graph(set([
('x', 'a'),
('a', 'b'),
('b', 'c'),
('c', 'a'),
('c', 'd'),
('d', 'e'),
('e', 'f'),
('e', 'g'),
]))
graph.remove_exterior_nodes()
s = make_dot_file(graph)
open('zulip-deps.dot', 'w').write(s)
if __name__ == '__main__':
test()