3 """Define :class:`Graph`, a directed, acyclic graph structure.
8 class CyclicGraphError (ValueError):
12 """A node/element in a graph.
14 Contains a list of the node's parents, and stores the node's
20 >>> a = Node(data='a')
21 >>> b = Node(parents=[a], data='b')
22 >>> c = Node(parents=[a], data='c')
23 >>> d = Node(parents=[b, c], data='d')
27 We can list all of a node's ancestors.
29 >>> print [node for node in d.ancestors()]
31 >>> print [node for node in d.ancestors(depth_first=True)]
34 Ancestors works with cycles.
37 >>> print [node for node in d.ancestors()]
40 We can find the cycle path.
45 def __init__(self, parents=[], data=None):
46 list.__init__(self, parents)
49 def __cmp__(self, other):
50 return -cmp(self.data, other.data)
51 def __eq__(self, other):
52 return self.__cmp__(other) == 0
53 def __ne__(self, other):
54 return self.__cmp__(other) != 0
55 def __lt__(self, other):
56 return self.__cmp__(other) < 0
57 def __gt__(self, other):
58 return self.__cmp__(other) > 0
59 def __le__(self, other):
60 return self.__cmp__(other) <= 0
61 def __ge__(self, other):
62 return self.__cmp__(other) >= 0
69 def ancestors(self, depth_first=False):
70 """Generate all ancestors.
72 Will only yield each ancestor once, even in the case of
73 diamond inheritance, etc.
75 Breadth first by default.
85 if depth_first == True:
86 for parent in reversed(node):
87 stack.insert(0, parent)
91 """Return the shortest list of parents connecting `self` to
97 paths = dict((id(n), [n]) for n in stack)
100 n_path = paths[id(n)]
102 if id(parent) in paths:
104 p_path = list(n_path)
105 p_path.append(parent)
106 if id(parent) == id(node):
109 paths[id(parent)] = p_path
111 class _GraphRowEntry (list):
112 """A node in the graph with outstanding children.
114 `columns` is a list of the indexes of outstanding columns which
115 will eventually attach to those children.
117 def __init__(self, node, columns=[]):
118 list.__init__(self, columns)
121 class GraphRow (list):
122 """A graph row generator.
124 Contains a list of :class:`_GraphRowEntry`\s (added with
125 :meth:`insert`), and generates the current line with
126 :meth:`__str__`. You should generate a graph with repeated
134 The string rendering can be cusomized by changing `g.chars`.
135 Control over the branch columns:
137 ================= ===========================================
138 `node: ...` the active (most recently inserted) node
139 `split/join: ...` branching/merging runs from the active node
140 `run: connected` connect a branch to its eventual node
141 `run: blank` place-holder for extinct runs
142 ================= ===========================================
144 Branch columns are seperated by separator columns:
146 ================= =======================================================
147 `sep: split/join` separate split/join branch columns from the active node
148 `sep: default` separate all remaining branch columns
149 ================= =======================================================
151 For the split/join branch columns, "born" and "died" are defined
152 from the point of view of `GraphRow`. Because trees are printed
153 tip-to-root (:meth:`Graph.topological_sort`), "born" runs are
154 determined by the active node's parents (which have not yet been
155 printed), and "dead" runs by its children (which have been
163 self._next_old_dead = []
165 'node: both tip and root': 'b',
168 'node: regular': '*',
169 'split/join: born and died left of active': '>',
170 'split/join: born and died right of active': '<',
171 'split/join: born left of active': '/',
172 'split/join: born right of active': '\\',
173 'split/join: died left of active': '\\',
174 'split/join: died right of active': '/',
176 'run: connected': '|',
177 'sep: split/join': '-',
180 def insert(self, node):
181 """Add a new node to the graph.
183 Nodes should be inserted in tip-to-root topological order
184 (i.e. node must be inserted before any of its parents).
187 self._last_width = self._width
188 self._old_dead = self._next_old_dead
191 if len(self) > 0 and len(self[-1].node) == 0:
192 # remove last entry's active column if it has no parents
193 self[-1].pop(0) # (it' already in _old_dead via _next_old_dead)
194 self._children = [(i,e) for i,e in enumerate(self) if node in e.node]
195 for i,e in self._children:
196 self._dead.append(e.pop(0)) # TODO: sharing branches vs. current 1 per child
197 exhausted = [] # _GraphRowEntry\s with no futher parents
204 remaining = max(len(node), 1)
205 slots = sorted(self._old_dead + self._dead)
206 self._born.extend(slots[:remaining])
207 remaining -= len(slots)
208 self._born.extend(range(self._last_width, self._last_width+remaining))
209 self._active = self._born[0]
210 self.append(_GraphRowEntry(node, columns=self._born))
211 self._width = max([e[-1] for e in self])+1
212 self._next_old_dead = [i for i in slots[len(self._born):] if i < self._width]
214 self._next_old_dead.insert(0, self._active)
217 """Render the current line of the graph as a string.
219 left_connects = [self._born[0]]
220 right_connects = [self._born[-1]]
221 if len(self._dead) > 0:
222 left_connects.append(self._dead[0])
223 right_connects.append(self._dead[-1])
224 left_connect = min(left_connects)
225 right_connect = max(right_connects)
227 for i in range(max(self._width, self._last_width)):
228 if i == self._active:
229 if len(self._node) == 0:
230 if len(self._children) == 0:
231 char = self.chars['node: both tip and root']
233 char = self.chars['node: root']
234 elif len(self._children) == 0:
235 char = self.chars['node: tip']
237 char = self.chars['node: regular']
238 elif i in self._born:
242 'split/join: born and died left of active']
245 'split/join: born and died right of active']
248 char = self.chars['split/join: born left of active']
250 char = self.chars['split/join: born right of active']
251 elif i in self._dead:
253 char = self.chars['split/join: died left of active']
255 char = self.chars['split/join: died right of active']
256 elif i in self._old_dead:
257 char = self.chars['run: blank']
259 char = self.chars['run: connected']
260 if i < left_connect or i >= right_connect:
261 sep = self.chars['sep: default']
263 sep = self.chars['sep: split/join']
264 if char == self.chars['run: blank']:
265 char = self.chars['sep: split/join']
266 string.extend([char, sep])
267 return ''.join(string)[:-1] # [-1] strips final sep
270 """A directed, acyclic graph structure.
272 Contains methods for sorting and printing graphs.
277 >>> class Nodes (object): pass
279 >>> for char in ['a','b','c','d','e','f','g','h','i']:
280 ... setattr(n, char, Node(data=char))
284 >>> n.e.extend([n.b, n.c, n.d])
288 >>> n.i.extend([n.f, n.g, n.h])
289 >>> g = Graph([n.a,n.b,n.c,n.d,n.e,n.f,n.g,n.h,n.i])
290 >>> g.topological_sort()
291 >>> print [node for node in g]
292 [i, h, g, f, e, d, c, b, a]
293 >>> print g.ascii_graph()
304 >>> for char in ['a','b','c','d','e','f','g','h']:
305 ... setattr(n, char, Node(data=char))
310 >>> n.f.extend([n.b, n.d])
311 >>> n.g.extend([n.e, n.f])
312 >>> n.h.extend([n.c, n.g])
313 >>> g = Graph([n.a,n.b,n.c,n.d,n.e,n.f,n.g,n.h])
314 >>> print g.ascii_graph()
324 >>> for char in ['a','b','c','d','e','f','g','h','i']:
325 ... setattr(n, char, Node(data=char))
326 >>> for char in ['a', 'b','c','d','e','f','g','h']:
327 ... nx = getattr(n, char)
329 >>> g = Graph([n.a,n.b,n.c,n.d,n.e,n.f,n.g,n.h,n.i])
330 >>> print g.ascii_graph()
341 >>> for char in ['a','b','c','d','e','f','g','h','i']:
342 ... setattr(n, char, Node(data=char))
343 >>> for char in ['b','c','d','e','f','g','h','i']:
344 ... nx = getattr(n, char)
346 >>> g = Graph([n.a,n.b,n.c,n.d,n.e,n.f,n.g,n.h,n.i])
347 >>> print g.ascii_graph()
358 >>> for char in ['a','b','c','d','e','f','g','h','i']:
359 ... setattr(n, char, Node(data=char))
361 >>> n.e.extend([n.a, n.c])
362 >>> n.f.extend([n.c, n.d, n.e])
363 >>> n.g.extend([n.b, n.e, n.f])
364 >>> n.h.extend([n.a, n.c, n.d, n.g])
365 >>> n.i.extend([n.a, n.b, n.c, n.g])
366 >>> g = Graph([n.a,n.b,n.c,n.d,n.e,n.f,n.g,n.h,n.i])
367 >>> print g.ascii_graph()
371 *-|-|-|-|-|-|-|-|-\-\ f
372 *-|-|-|-< | | | | | | e
373 | | | | | *-|-|-|-/ | d
374 r-/-|-|-|-|-/-|-|---/ c
378 Ok, enough pretty graphs ;). Here's an example of cycle
381 >>> for char in ['a','b','c','d']:
382 ... setattr(n, char, Node(data=char))
385 >>> n.d.extend([n.b, n.c])
387 >>> g = Graph([n.a,n.b,n.c,n.d])
388 >>> g.check_for_cycles()
389 Traceback (most recent call last):
391 CyclicGraphError: cycle detected:
397 def topological_sort(self):
398 """Algorithm from git's commit.c `sort_in_topological_order`_.
400 Ordering is tip-to-root.
402 In situations where topological sorting is ambiguous, the
403 nodes are sorted using the inverse (since we use tip-to-root
404 ordering) of their comparison functions (__cmp__, __lt__,
407 .. _sort_in_topological_order:
408 http://git.kernel.org/?p=git/git.git;a=blob;f=commit.c;h=731191e63bd39a89a8ea4ed0390c49d5605cdbed;hb=HEAD#l425
414 parent._outcount += 1
415 tips = sorted([node for node in self if node._outcount == 0])
421 parent._outcount -= 1
422 if parent._outcount == 0:
423 bisect.insort(tips, parent)
426 final_len = len(self)
427 if final_len != orig_len:
428 raise CyclicGraphError(
429 '%d of %d elements not reachable from tips'
430 % (orig_len - final_len, orig_len))
432 def check_for_cycles(self):
433 """Check for cyclic references.
436 if node in node.ancestors():
437 path = node.path(node)
438 raise CyclicGraphError(
439 'cycle detected:\n %s'
440 % '\n '.join([repr(node)]+[repr(node) for node in path]))
442 def graph_rows(self, graph_row=None):
443 """Generate a sequence of (`graph_row`, `node`) tuples.
445 Preforms :meth:`topological_sort` internally.
447 If `graph_row` is `None`, a new instance of :class:`GraphRow`
450 if graph_row == None:
451 graph_row = GraphRow()
452 self.topological_sort()
454 graph_row.insert(node)
455 yield (graph_row, node)
457 def ascii_graph(self, string_fn=str):
458 """Print an ascii graph on the left with `string_fn(node)` on
461 See the class docstring for example output.
464 for row,node in self.graph_rows():
465 graph.append('%s %s' % (str(row), string_fn(node)))
466 return '\n'.join(graph)