""" Soft implementation of a Direct Acyclic Graph (DAG) Nota Bene: It is NOT fully checked if valid (i.e. some cycle can be introduced)! """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import copy from DIRAC import gLogger __RCSID__ = "$Id $" class DAG(object): """a Direct Acyclic Graph (DAG) Represented as a dictionary whose keys are nodes, and values are sets holiding their dependencies """ def __init__(self): """Defines graph variable holding the dag representation""" self.graph = {} def addNode(self, node): """add a node to graph Args: node (object): Any type of object - if not hashable, it will be converted to a frozenset """ node = checkNode(node) if node not in self.graph: self.graph[node] = set() def addEdge(self, fromNode, toNode): """add an edge (checks if both nodes exist) Args: fromNode (object) toNode (object) """ fromNode = checkNode(fromNode) toNode = checkNode(toNode) if fromNode not in self.graph: gLogger.error("Missing node from where the edge start") return if toNode not in self.graph: gLogger.error("Missing node to where the edge lands") return for node, toNodes in self.graph.items(): # This is clearly not enough to assure that it's really acyclic... if toNode == node and fromNode in toNodes: gLogger.error("Can't insert this edge") return self.graph[fromNode].add(toNode) def getIndexNodes(self): """Return a list of index nodes""" notIndexNodes = set() for depNodes in self.graph.values(): [notIndexNodes.add(depNode) for depNode in depNodes] indexNodes = list(set(self.graph.keys()) - notIndexNodes) return [unHashNode(inu) for inu in indexNodes] def getList(self): """Returns a list out of the DAG, if possible""" cDAG = copy.deepcopy(self) l = [] while True: try: indexNodes = cDAG.getIndexNodes() if len(indexNodes) != 1: gLogger.warn("The DAG is not sequential") break ind = checkNode(indexNodes[0]) del cDAG.graph[ind] l.append(unHashNode(ind)) except KeyError: break except IndexError: break return l def checkNode(node): """Returns a hashable version of node""" try: node.__hash__() return node except TypeError: # nodeName is not hashable, so it can't be a key in the graph (which is a dictionary) return makeFrozenSet(node) def unHashNode(node): """Returns a dict or list, if node is frozenset""" if isinstance(node, frozenset): try: return dict(node) # Is it a dictionary? except TypeError: return list(node) except ValueError: return list(node) else: return node def makeFrozenSet(ob): """ Makes a hash from a dictionary, list, tuple or set to any level, that contains only other hashable types (including any lists, tuples, sets, and dictionaries). """ if isinstance(ob, (set, tuple, list)): return frozenset([makeFrozenSet(e) for e in ob]) elif not isinstance(ob, dict): return ob new_ob = copy.deepcopy(ob) for obk, obv in new_ob.items(): new_ob[obk] = makeFrozenSet(obv) return frozenset(sorted(new_ob.items()))