| 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
|
| +
|
|
|