2017-03-18 21:10:40 +01:00
|
|
|
from __future__ import absolute_import
|
|
|
|
from __future__ import print_function
|
|
|
|
|
2017-03-17 21:46:15 +01:00
|
|
|
from collections import defaultdict
|
|
|
|
|
2017-03-20 13:25:45 +01:00
|
|
|
from typing import Callable, DefaultDict, Iterator, List, Set, Tuple
|
|
|
|
|
|
|
|
Edge = Tuple[str, str]
|
|
|
|
EdgeSet = Set[Edge]
|
2017-03-17 21:46:15 +01:00
|
|
|
|
|
|
|
class Graph(object):
|
2017-03-20 13:25:45 +01:00
|
|
|
def __init__(self, tuples):
|
|
|
|
# type: (EdgeSet) -> None
|
2017-05-07 16:53:52 +02:00
|
|
|
self.children = defaultdict(list) # type: DefaultDict[str, List[str]]
|
|
|
|
self.parents = defaultdict(list) # type: DefaultDict[str, List[str]]
|
|
|
|
self.nodes = set() # type: Set[str]
|
2017-03-17 21:46:15 +01:00
|
|
|
|
|
|
|
for parent, child in tuples:
|
|
|
|
self.parents[child].append(parent)
|
|
|
|
self.children[parent].append(child)
|
|
|
|
self.nodes.add(parent)
|
|
|
|
self.nodes.add(child)
|
|
|
|
|
2017-03-20 13:25:45 +01:00
|
|
|
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
|
|
|
|
|
2017-03-17 21:46:15 +01:00
|
|
|
def remove_exterior_nodes(self):
|
|
|
|
# type: () -> None
|
|
|
|
still_work_to_do = True
|
|
|
|
while still_work_to_do:
|
2017-05-07 16:53:52 +02:00
|
|
|
still_work_to_do = False # for now
|
2017-03-17 21:46:15 +01:00
|
|
|
for node in self.nodes:
|
2017-03-18 04:24:07 +01:00
|
|
|
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]
|
2017-03-17 21:46:15 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2017-03-18 21:10:40 +01:00
|
|
|
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)
|
|
|
|
|
2017-03-20 13:25:45 +01:00
|
|
|
def best_edge_to_remove(orig_graph, is_exempt):
|
|
|
|
# type: (Graph, Callable[[Edge], bool]) -> 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
|
|
|
|
|
2017-03-17 21:46:15 +01:00
|
|
|
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
|
2017-03-20 13:25:45 +01:00
|
|
|
graph = Graph(set([
|
2017-03-17 21:46:15 +01:00
|
|
|
('x', 'a'),
|
|
|
|
('a', 'b'),
|
|
|
|
('b', 'c'),
|
|
|
|
('c', 'a'),
|
|
|
|
('c', 'd'),
|
|
|
|
('d', 'e'),
|
|
|
|
('e', 'f'),
|
|
|
|
('e', 'g'),
|
2017-03-20 13:25:45 +01:00
|
|
|
]))
|
2017-03-17 21:46:15 +01:00
|
|
|
graph.remove_exterior_nodes()
|
|
|
|
|
|
|
|
s = make_dot_file(graph)
|
|
|
|
open('zulip-deps.dot', 'w').write(s)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
test()
|