| # Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). |
| # This program is free software; you can redistribute it and/or modify it under |
| # the terms of the GNU General Public License as published by the Free Software |
| # Foundation; either version 2 of the License, or (at your option) any later |
| # version. |
| # |
| # This program 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 General Public License for more details |
| # |
| # You should have received a copy of the GNU General Public License along with |
| # this program; if not, write to the Free Software Foundation, Inc., |
| # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| |
| # This file was copied from logilab-common with the same license: |
| # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
| """Graph manipulation utilities. |
| |
| (dot generation adapted from pypy/translator/tool/make_dot.py) |
| """ |
| |
| import os.path as osp |
| import os |
| import sys |
| import tempfile |
| import codecs |
| |
| 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(object): |
| """Dot File backend.""" |
| def __init__(self, graphname, rankdir=None, size=None, ratio=None, |
| charset='utf-8', renderer='dot', additional_param=None): |
| if additional_param is None: |
| additional_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 additional_param.items(): |
| 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, _, 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 = codecs.open(dot_sourcepath, 'w', encoding='utf8') |
| pdot.write(self.source) |
| pdot.close() |
| if target != 'dot': |
| use_shell = sys.platform == 'win32' |
| 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(sorted(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(sorted(attrs)))) |
| |
| def normalize_node_id(nid): |
| """Returns a suitable DOT node id for `nid`.""" |
| return '"%s"' % nid |
| |
| 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, [], set(), result, vertice) |
| return result |
| |
| def _get_cycles(graph_dict, path, visited, result, vertice): |
| """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 cycle not in result: |
| result.append(cycle) |
| return |
| path.append(vertice) |
| try: |
| for node in graph_dict[vertice]: |
| # don't check already visited nodes again |
| if node not in visited: |
| _get_cycles(graph_dict, path, visited, result, node) |
| visited.add(node) |
| except KeyError: |
| pass |
| path.pop() |