# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import itertools from heat.common import exception class CircularDependencyException(exception.OpenstackException): message = _("Circular Dependency Found: %(cycle)s") class Dependencies(object): '''Helper class for calculating a dependency graph''' class Node(object): def __init__(self, requires=None, required_by=None): ''' Initialise the node, optionally with a set of keys this node requires and/or a set of keys that this node is required by. ''' self.require = requires and requires.copy() or set() self.satisfy = required_by and required_by.copy() or set() def copy(self): '''Make a copy of the node''' return Dependencies.Node(self.require, self.satisfy) def reverse_copy(self): '''Make a copy of the node with the edge directions reversed''' return Dependencies.Node(self.satisfy, self.require) def required_by(self, source=None): ''' List the keys that require this node, and optionally add a new one ''' if source is not None: self.satisfy.add(source) return iter(self.satisfy) def requires(self, target): '''Add a key that this node requires''' self.require.add(target) def __isub__(self, target): '''Remove a key that this node requires''' self.require.remove(target) return self def __nonzero__(self): '''Test if this node is a leaf (requires nothing)''' return bool(self.require) def stem(self): '''Test if this node is a stem (required by nothing)''' return not bool(self.satisfy) def disjoint(self): '''Test if this node is both a lead and a stem''' return self and self.stem() def __len__(self): '''Count the number of keys required by this node''' return len(self.require) def __iter__(self): '''Iterate over the keys required by this node''' return iter(self.require) def __str__(self): '''Return a human-readable string representation of the node''' return '{%s}' % ', '.join(str(n) for n in self) def __repr__(self): '''Return a string representation of the node''' return repr(self.require) def __init__(self, edges=[]): ''' Initialise, optionally with a list of edges, in the form of (requirer, required) tuples. ''' self.deps = collections.defaultdict(self.Node) for e in edges: self += e def __iadd__(self, edge): '''Add another edge, in the form of a (requirer, required) tuple''' requirer, required = edge if required is None: # Just ensure the node is created by accessing the defaultdict self.deps[requirer] else: self.deps[required].required_by(requirer) self.deps[requirer].requires(required) return self def __getitem__(self, last): ''' Return a partial dependency graph consisting of the specified node and all those that require it only. ''' if last not in self.deps: raise KeyError def get_edges(key): def requirer_edges(rqr): # Concatenate the dependency on the current node with the # recursive generated list return itertools.chain([(rqr, key)], get_edges(rqr)) # Get the edge list for each node that requires the current node edge_lists = itertools.imap(requirer_edges, self.deps[key].required_by()) # Combine the lists into one long list return itertools.chain.from_iterable(edge_lists) if self.deps[last].stem(): # Nothing requires this, so just add the node itself edges = [(last, None)] else: edges = get_edges(last) return Dependencies(edges) @staticmethod def _deps_to_str(deps): '''Convert the given dependency graph to a human-readable string''' pairs = ('%s: %s' % (str(k), str(v)) for k, v in deps.items()) return '{%s}' % ', '.join(pairs) def __str__(self): ''' Return a human-readable string representation of the dependency graph ''' return self._deps_to_str(self.deps) def _edges(self): '''Return an iterator over all of the edges in the graph''' def outgoing_edges(rqr, node): if node.disjoint(): yield (rqr, None) else: for rqd in node: yield (rqr, rqd) return (outgoing_edges(*item) for item in self.deps.iteritems()) def __repr__(self): '''Return a string representation of the object''' return 'Dependencies([%s])' % ', '.join(repr(e) for e in edges) def _toposort(self, deps): '''Generate a topological sort of a dependency graph''' def next_leaf(): for leaf, node in deps.items(): if not node: return leaf, node # There are nodes remaining, but no more leaves: a cycle cycle = self._deps_to_str(deps) raise CircularDependencyException(cycle=cycle) for iteration in xrange(len(deps)): leaf, node = next_leaf() yield leaf # Remove the node and all edges connected to it before continuing # to look for more leaves for src in node.required_by(): deps[src] -= leaf del deps[leaf] def _mapgraph(self, func): '''Map the supplied function onto every node in the graph.''' return dict((k, func(n)) for k, n in self.deps.items()) def __iter__(self): '''Return a topologically sorted iterator''' deps = self._mapgraph(lambda n: n.copy()) return self._toposort(deps) def __reversed__(self): '''Return a reverse topologically sorted iterator''' rev_deps = self._mapgraph(lambda n: n.reverse_copy()) return self._toposort(rev_deps)