| Index: third_party/logilab/common/graph.py | 
| diff --git a/third_party/logilab/common/graph.py b/third_party/logilab/common/graph.py | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..75a2ee7a7ade6306464e9768220fde729eaefb79 | 
| --- /dev/null | 
| +++ b/third_party/logilab/common/graph.py | 
| @@ -0,0 +1,273 @@ | 
| +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. | 
| +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr | 
| +# | 
| +# This file is part of logilab-common. | 
| +# | 
| +# logilab-common is free software: you can redistribute it and/or modify it under | 
| +# the terms of the GNU Lesser General Public License as published by the Free | 
| +# Software Foundation, either version 2.1 of the License, or (at your option) any | 
| +# later version. | 
| +# | 
| +# logilab-common is distributed in the hope that it will be useful, but WITHOUT | 
| +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | 
| +# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more | 
| +# details. | 
| +# | 
| +# You should have received a copy of the GNU Lesser General Public License along | 
| +# with logilab-common.  If not, see <http://www.gnu.org/licenses/>. | 
| +"""Graph manipulation utilities. | 
| + | 
| +(dot generation adapted from pypy/translator/tool/make_dot.py) | 
| +""" | 
| + | 
| +__docformat__ = "restructuredtext en" | 
| + | 
| +__metaclass__ = type | 
| + | 
| +import os.path as osp | 
| +import os | 
| +import sys | 
| +import tempfile | 
| +from logilab.common.compat import str_encode | 
| + | 
| +def escape(value): | 
| +    """Make <value> usable in a dot file.""" | 
| +    lines = [line.replace('"', '\\"') for line in value.split('\n')] | 
| +    data = '\\l'.join(lines) | 
| +    return '\\n' + data | 
| + | 
| +def target_info_from_filename(filename): | 
| +    """Transforms /some/path/foo.png into ('/some/path', 'foo.png', 'png').""" | 
| +    basename = osp.basename(filename) | 
| +    storedir = osp.dirname(osp.abspath(filename)) | 
| +    target = filename.split('.')[-1] | 
| +    return storedir, basename, target | 
| + | 
| + | 
| +class DotBackend: | 
| +    """Dot File backend.""" | 
| +    def __init__(self, graphname, rankdir=None, size=None, ratio=None, | 
| +            charset='utf-8', renderer='dot', additionnal_param={}): | 
| +        self.graphname = graphname | 
| +        self.renderer = renderer | 
| +        self.lines = [] | 
| +        self._source = None | 
| +        self.emit("digraph %s {" % normalize_node_id(graphname)) | 
| +        if rankdir: | 
| +            self.emit('rankdir=%s' % rankdir) | 
| +        if ratio: | 
| +            self.emit('ratio=%s' % ratio) | 
| +        if size: | 
| +            self.emit('size="%s"' % size) | 
| +        if charset: | 
| +            assert charset.lower() in ('utf-8', 'iso-8859-1', 'latin1'), \ | 
| +                   'unsupported charset %s' % charset | 
| +            self.emit('charset="%s"' % charset) | 
| +        for param in additionnal_param.iteritems(): | 
| +            self.emit('='.join(param)) | 
| + | 
| +    def get_source(self): | 
| +        """returns self._source""" | 
| +        if self._source is None: | 
| +            self.emit("}\n") | 
| +            self._source = '\n'.join(self.lines) | 
| +            del self.lines | 
| +        return self._source | 
| + | 
| +    source = property(get_source) | 
| + | 
| +    def generate(self, outputfile=None, dotfile=None, mapfile=None): | 
| +        """Generates a graph file. | 
| + | 
| +        :param outputfile: filename and path [defaults to graphname.png] | 
| +        :param dotfile: filename and path [defaults to graphname.dot] | 
| + | 
| +        :rtype: str | 
| +        :return: a path to the generated file | 
| +        """ | 
| +        import subprocess # introduced in py 2.4 | 
| +        name = self.graphname | 
| +        if not dotfile: | 
| +            # if 'outputfile' is a dot file use it as 'dotfile' | 
| +            if outputfile and outputfile.endswith(".dot"): | 
| +                dotfile = outputfile | 
| +            else: | 
| +                dotfile = '%s.dot' % name | 
| +        if outputfile is not None: | 
| +            storedir, basename, target = target_info_from_filename(outputfile) | 
| +            if target != "dot": | 
| +                pdot, dot_sourcepath = tempfile.mkstemp(".dot", name) | 
| +                os.close(pdot) | 
| +            else: | 
| +                dot_sourcepath = osp.join(storedir, dotfile) | 
| +        else: | 
| +            target = 'png' | 
| +            pdot, dot_sourcepath = tempfile.mkstemp(".dot", name) | 
| +            ppng, outputfile = tempfile.mkstemp(".png", name) | 
| +            os.close(pdot) | 
| +            os.close(ppng) | 
| +        pdot = open(dot_sourcepath, 'w') | 
| +        pdot.write(str_encode(self.source, 'utf8')) | 
| +        pdot.close() | 
| +        if target != 'dot': | 
| +            if sys.platform == 'win32': | 
| +                use_shell = True | 
| +            else: | 
| +                use_shell = False | 
| +            if mapfile: | 
| +                subprocess.call([self.renderer,  '-Tcmapx', '-o', mapfile, '-T', target, dot_sourcepath, '-o', outputfile], | 
| +                                shell=use_shell) | 
| +            else: | 
| +                subprocess.call([self.renderer, '-T',  target, | 
| +                                 dot_sourcepath, '-o',  outputfile], | 
| +                                shell=use_shell) | 
| +            os.unlink(dot_sourcepath) | 
| +        return outputfile | 
| + | 
| +    def emit(self, line): | 
| +        """Adds <line> to final output.""" | 
| +        self.lines.append(line) | 
| + | 
| +    def emit_edge(self, name1, name2, **props): | 
| +        """emit an edge from <name1> to <name2>. | 
| +        edge properties: see http://www.graphviz.org/doc/info/attrs.html | 
| +        """ | 
| +        attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()] | 
| +        n_from, n_to = normalize_node_id(name1), normalize_node_id(name2) | 
| +        self.emit('%s -> %s [%s];' % (n_from, n_to, ", ".join(attrs)) ) | 
| + | 
| +    def emit_node(self, name, **props): | 
| +        """emit a node with given properties. | 
| +        node properties: see http://www.graphviz.org/doc/info/attrs.html | 
| +        """ | 
| +        attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()] | 
| +        self.emit('%s [%s];' % (normalize_node_id(name), ", ".join(attrs))) | 
| + | 
| +def normalize_node_id(nid): | 
| +    """Returns a suitable DOT node id for `nid`.""" | 
| +    return '"%s"' % nid | 
| + | 
| +class GraphGenerator: | 
| +    def __init__(self, backend): | 
| +        # the backend is responsible to output the graph in a particular format | 
| +        self.backend = backend | 
| + | 
| +    # XXX doesn't like space in outpufile / mapfile | 
| +    def generate(self, visitor, propshdlr, outputfile=None, mapfile=None): | 
| +        # the visitor | 
| +        # the property handler is used to get node and edge properties | 
| +        # according to the graph and to the backend | 
| +        self.propshdlr = propshdlr | 
| +        for nodeid, node in visitor.nodes(): | 
| +            props = propshdlr.node_properties(node) | 
| +            self.backend.emit_node(nodeid, **props) | 
| +        for subjnode, objnode, edge in visitor.edges(): | 
| +            props = propshdlr.edge_properties(edge, subjnode, objnode) | 
| +            self.backend.emit_edge(subjnode, objnode, **props) | 
| +        return self.backend.generate(outputfile=outputfile, mapfile=mapfile) | 
| + | 
| + | 
| +class UnorderableGraph(Exception): | 
| +    pass | 
| + | 
| +def ordered_nodes(graph): | 
| +    """takes a dependency graph dict as arguments and return an ordered tuple of | 
| +    nodes starting with nodes without dependencies and up to the outermost node. | 
| + | 
| +    If there is some cycle in the graph, :exc:`UnorderableGraph` will be raised. | 
| + | 
| +    Also the given graph dict will be emptied. | 
| +    """ | 
| +    # check graph consistency | 
| +    cycles = get_cycles(graph) | 
| +    if cycles: | 
| +        cycles = '\n'.join([' -> '.join(cycle) for cycle in cycles]) | 
| +        raise UnorderableGraph('cycles in graph: %s' % cycles) | 
| +    vertices = set(graph) | 
| +    to_vertices = set() | 
| +    for edges in graph.values(): | 
| +        to_vertices |= set(edges) | 
| +    missing_vertices = to_vertices - vertices | 
| +    if missing_vertices: | 
| +        raise UnorderableGraph('missing vertices: %s' % ', '.join(missing_vertices)) | 
| +    # order vertices | 
| +    order = [] | 
| +    order_set = set() | 
| +    old_len = None | 
| +    while graph: | 
| +        if old_len == len(graph): | 
| +            raise UnorderableGraph('unknown problem with %s' % graph) | 
| +        old_len = len(graph) | 
| +        deps_ok = [] | 
| +        for node, node_deps in graph.items(): | 
| +            for dep in node_deps: | 
| +                if dep not in order_set: | 
| +                    break | 
| +            else: | 
| +                deps_ok.append(node) | 
| +        order.append(deps_ok) | 
| +        order_set |= set(deps_ok) | 
| +        for node in deps_ok: | 
| +            del graph[node] | 
| +    result = [] | 
| +    for grp in reversed(order): | 
| +        result.extend(sorted(grp)) | 
| +    return tuple(result) | 
| + | 
| + | 
| +def get_cycles(graph_dict, vertices=None): | 
| +    '''given a dictionary representing an ordered graph (i.e. key are vertices | 
| +    and values is a list of destination vertices representing edges), return a | 
| +    list of detected cycles | 
| +    ''' | 
| +    if not graph_dict: | 
| +        return () | 
| +    result = [] | 
| +    if vertices is None: | 
| +        vertices = graph_dict.keys() | 
| +    for vertice in vertices: | 
| +        _get_cycles(graph_dict, vertice, [], result) | 
| +    return result | 
| + | 
| +def _get_cycles(graph_dict, vertice=None, path=None, result=None): | 
| +    """recursive function doing the real work for get_cycles""" | 
| +    if vertice in path: | 
| +        cycle = [vertice] | 
| +        for node in path[::-1]: | 
| +            if node == vertice: | 
| +                break | 
| +            cycle.insert(0, node) | 
| +        # make a canonical representation | 
| +        start_from = min(cycle) | 
| +        index = cycle.index(start_from) | 
| +        cycle = cycle[index:] + cycle[0:index] | 
| +        # append it to result if not already in | 
| +        if not cycle in result: | 
| +            result.append(cycle) | 
| +        return | 
| +    path.append(vertice) | 
| +    try: | 
| +        for node in graph_dict[vertice]: | 
| +            _get_cycles(graph_dict, node, path, result) | 
| +    except KeyError: | 
| +        pass | 
| +    path.pop() | 
| + | 
| +def has_path(graph_dict, fromnode, tonode, path=None): | 
| +    """generic function taking a simple graph definition as a dictionary, with | 
| +    node has key associated to a list of nodes directly reachable from it. | 
| + | 
| +    Return None if no path exists to go from `fromnode` to `tonode`, else the | 
| +    first path found (as a list including the destination node at last) | 
| +    """ | 
| +    if path is None: | 
| +        path = [] | 
| +    elif fromnode in path: | 
| +        return None | 
| +    path.append(fromnode) | 
| +    for destnode in graph_dict[fromnode]: | 
| +        if destnode == tonode or has_path(graph_dict, destnode, tonode, path): | 
| +            return path[1:] + [tonode] | 
| +    path.pop() | 
| +    return None | 
| + | 
|  |