1
2 r"""
3 :Copyright:
4
5 Copyright 2013 - 2015
6 Andr\xe9 Malo or his licensors, as applicable
7
8 :License:
9
10 Licensed under the Apache License, Version 2.0 (the "License");
11 you may not use this file except in compliance with the License.
12 You may obtain a copy of the License at
13
14 http://www.apache.org/licenses/LICENSE-2.0
15
16 Unless required by applicable law or agreed to in writing, software
17 distributed under the License is distributed on an "AS IS" BASIS,
18 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 See the License for the specific language governing permissions and
20 limitations under the License.
21
22 ===========================
23 Dependency Graph Resolver
24 ===========================
25
26 Dependency Graph Resolver.
27 """
28 if __doc__:
29
30 __doc__ = __doc__.encode('ascii').decode('unicode_escape')
31 __author__ = r"Andr\xe9 Malo".encode('ascii').decode('unicode_escape')
32 __docformat__ = "restructuredtext en"
33
34 import collections as _collections
35 import operator as _op
36
37 from ._exceptions import DependencyCycle
38
39
41 """
42 Dependency Graph Container
43
44 This is a simple directed acyclic graph. The graph starts empty, and new
45 nodes (and edges) are added using the `add` method. If the newly added
46 create a cycle, an exception is thrown.
47
48 Finally, the graph is resolved using the `resolve` method. The method will
49 return topologically ordered nodes and destroy the graph. The topological
50 order is *stable*, meaning, the same graph will always produce the same
51 output.
52
53 :IVariables:
54 `_outgoing` : ``defaultdict``
55 Mapping of outgoing nodes (node -> set(outgoing neighbours))
56
57 `_incoming` : ``defaultdict``
58 Mapping of incoming nodes (node -> set(incoming neighbours))
59 """
60 __slots__ = ('_outgoing', '_incoming')
61
63 """ Initialization """
64 self._outgoing = _collections.defaultdict(set)
65 self._incoming = _collections.defaultdict(set)
66
67 - def add(self, start, end):
68 """
69 Add a new nodes with edge to the graph
70
71 The edge is directed from `start` to `end`.
72
73 :Parameters:
74 `start` : ``str``
75 Node
76
77 `end` : ``str``
78 Node
79 """
80 self._outgoing[start].add(end)
81 self._incoming[end].add(start)
82 self._check_cycle(end)
83
85 """
86 Resolve graph and return nodes in topological order
87
88 The graph is defined by outgoing and incoming dicts (mapping nodes to
89 their outgoing or incoming neighbours). The graph is destroyed in the
90 process.
91
92 :Return: Sorted node list. The output is stable, because nodes on
93 the same level are sorted alphabetically. Furthermore all
94 leaf nodes are put at the end.
95 :Rtype: ``list``
96 """
97 result, outgoing, incoming = [], self._outgoing, self._incoming
98 roots = list(set(outgoing.iterkeys()) - set(incoming.iterkeys()))
99 leaves = set(incoming.iterkeys()) - set(outgoing.iterkeys())
100
101 roots.sort()
102 roots = _collections.deque(roots)
103 roots_push, roots_pop = roots.appendleft, roots.pop
104 result_push, opop, ipop = result.append, outgoing.pop, incoming.pop
105 while roots:
106 node = roots_pop()
107 if node not in leaves:
108 result_push(node)
109 children = list(opop(node, ()))
110 children.sort()
111 for child in children:
112 parents = incoming[child]
113 parents.remove(node)
114 if not parents:
115 roots_push(child)
116 ipop(child)
117
118 if outgoing or incoming:
119 raise AssertionError("Graph not resolved (this is a bug).")
120
121 leaves = list(leaves)
122 leaves.sort()
123 return result + leaves
124
126 """
127 Find a cycle containing `node`
128
129 This assumes, that there's no other possible cycle in the graph. This
130 assumption is valid, because the graph is checked whenever a new
131 edge is added.
132
133 :Parameters:
134 `node` : ``str``
135 Node which may be part of a cycle.
136
137 :Exceptions:
138 - `DependencyCycle` : Raised, if there is, indeed, a cycle in the
139 graph. The cycling nodes are passed as a list to the exception.
140 """
141
142
143
144 outgoing = self._outgoing
145 if node in outgoing:
146 iter_ = iter
147 stack = [(node, iter_(outgoing[node]).next)]
148 exhausted, push, pop = StopIteration, stack.append, stack.pop
149
150 while stack:
151 try:
152 child = stack[-1][1]()
153 except exhausted:
154 pop()
155 else:
156 if child == node:
157 raise DependencyCycle(map(_op.itemgetter(0), stack))
158 elif child in outgoing:
159 push((child, iter_(outgoing[child]).next))
160