#!/usr/bin/env python """ $ brew install graphviz $ brew graph | dot -Tsvg -ohomebrew.svg $ open homebrew.svg """ from __future__ import with_statement from contextlib import contextmanager import re from subprocess import Popen, PIPE import sys def run(command, print_command=False): "Run a command, returning the exit code and output." if print_command: print command p = Popen(command, stdout=PIPE) output, errput = p.communicate() return p.returncode, output def _quote_id(id): return '"' + id.replace('"', '\"') + '"' def format_attribs(attrib): if len(attrib) == 0: return '' values = ['%s="%s"' % (k, attrib[k]) for k in attrib] return '[' + ','.join(values) + ']' class Output(object): def __init__(self, fd=sys.stdout, tabstyle=" "): self.fd = fd self.tabstyle = tabstyle self.tablevel = 0 def close(self): self.fd = None def out(self, s): self.tabout() self.fd.write(s) def outln(self, s=None): if s is not None: self.tabout() self.fd.write(s) self.fd.write('\n') @contextmanager def indented(self): self.indent() yield self self.dedent() def indent(self): self.tablevel += 1 def dedent(self): if self.tablevel == 0: raise Exception('No existing indent level.') self.tablevel -= 1 def tabout(self): if self.tablevel: self.fd.write(self.tabstyle * self.tablevel) class NodeContainer(object): def __init__(self): self.nodes = list() self.node_defaults = dict() # Stack of node attribs self._node_styles = list() def _node_style(self): if (len(self._node_styles) > 0): return self._node_styles[-1] else: return dict() def _push_node_style(self, attrib): self._node_styles.append(attrib) def _pop_node_style(self): return self._node_styles.pop() @contextmanager def node_styles(self, attrib): self._push_node_style(attrib) yield self._pop_node_style() def node(self, nodeid, label, attrib=None): _attrib = dict(self._node_style()) if attrib is not None: _attrib.update(attrib) n = Node(nodeid, label, _attrib) self.nodes.append(n) return n def nodes_to_dot(self, out): if len(self.node_defaults) > 0: out.outln("node " + format_attribs(self.node_defaults) + ";") if len(self.nodes) == 0: return id_width = max([len(_quote_id(n.id)) for n in self.nodes]) for node in self.nodes: node.to_dot(out, id_width) class Node(object): def __init__(self, nodeid, label, attrib=None): self.id = nodeid self.label = label self.attrib = attrib if attrib is not None else dict() def as_dot(self, id_width=1): _attribs = dict(self.attrib) _attribs['label'] = self.label return '%-*s %s' % (id_width, _quote_id(self.id), format_attribs(_attribs)) def to_dot(self, out, id_width=1): out.outln(self.as_dot(id_width)) class ClusterContainer(object): def __init__(self): self.clusters = list() def cluster(self, clusterid, label, attrib=None): c = Cluster(clusterid, label, self, attrib) self.clusters.append(c) return c class Cluster(NodeContainer, ClusterContainer): def __init__(self, clusterid, label, parentcluster=None, attrib=None): NodeContainer.__init__(self) ClusterContainer.__init__(self) self.id = clusterid self.label = label self.attrib = attrib if attrib is not None else dict() self.parentcluster = parentcluster def cluster_id(self): return _quote_id("cluster_" + self.id) def to_dot(self, out): out.outln("subgraph %s {" % self.cluster_id()) with out.indented(): out.outln('label = "%s"' % self.label) for k in self.attrib: out.outln('%s = "%s"' % (k, self.attrib[k])) for cluster in self.clusters: cluster.to_dot(out) self.nodes_to_dot(out) out.outln("}") class Edge(object): def __init__(self, source, target, attrib=None): if attrib is None: attrib = dict() self.source = source self.target = target self.attrib = attrib def to_dot(self, out): out.outln(self.as_dot()) def as_dot(self): return " ".join((_quote_id(self.source), "->", _quote_id(self.target), format_attribs(self.attrib))) class EdgeContainer(object): def __init__(self): self.edges = list() self.edge_defaults = dict() # Stack of edge attribs self._edge_styles = list() def _edge_style(self): if (len(self._edge_styles) > 0): return self._edge_styles[-1] else: return dict() def _push_edge_style(self, attrib): self._edge_styles.append(attrib) def _pop_edge_style(self): return self._edge_styles.pop() @contextmanager def edge_styles(self, attrib): self._push_edge_style(attrib) yield self._pop_edge_style() def link(self, source, target, attrib=None): _attrib = dict(self._edge_style()) if attrib is not None: _attrib.update(attrib) e = Edge(source, target, _attrib) self.edges.append(e) return e def edges_to_dot(self, out): if len(self.edge_defaults) > 0: out.outln("edge " + format_attribs(self.edge_defaults) + ";") if len(self.edges) == 0: return for edge in self.edges: edge.to_dot(out) class Graph(NodeContainer, EdgeContainer, ClusterContainer): """ Contains the nodes, edges, and subgraph definitions for a graph to be turned into a Graphviz DOT file. """ def __init__(self, label=None, attrib=None): NodeContainer.__init__(self) EdgeContainer.__init__(self) ClusterContainer.__init__(self) self.label = label if label is not None else "Default Label" self.attrib = attrib if attrib is not None else dict() def dot(self, fd=sys.stdout): try: self.o = Output(fd) self._dot() finally: self.o.close() def _dot(self): self.o.outln("digraph G {") with self.o.indented(): self.o.outln('label = "%s"' % self.label) for k in self.attrib: self.o.outln('%s = "%s"' % (k, self.attrib[k])) self.nodes_to_dot(self.o) for cluster in self.clusters: self.o.outln() cluster.to_dot(self.o) self.o.outln() self.edges_to_dot(self.o) self.o.outln("}") def main(): code, output = run(["brew", "deps", "--all"]) output = output.strip() depgraph = list() for f in output.split("\n"): stuff = f.split(":",2) name = stuff[0] deps = stuff[1].strip() if not deps: deps = list() else: deps = deps.split(" ") depgraph.append((name, deps)) hb = Graph("Homebrew Dependencies", attrib={'labelloc':'b', 'rankdir':'LR', 'ranksep':'5'}) used = set() for f in depgraph: for d in f[1]: used.add(f[0]) used.add(d) for f in depgraph: if f[0] not in used: continue n = hb.node(f[0], f[0]) for d in f[1]: hb.link(d, f[0]) hb.dot() if __name__ == "__main__": main()