Adding new stuff
This commit is contained in:
parent
131691e143
commit
6c0a9f5b29
718 changed files with 0 additions and 0 deletions
282
vim-plugins/python-mode/pymode/libs/logilab/common/graph.py
Normal file
282
vim-plugins/python-mode/pymode/libs/logilab/common/graph.py
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
# 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
|
||||
import codecs
|
||||
import errno
|
||||
|
||||
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 sorted(additionnal_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, 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 = codecs.open(dot_sourcepath, 'w', encoding='utf8')
|
||||
pdot.write(self.source)
|
||||
pdot.close()
|
||||
if target != 'dot':
|
||||
if sys.platform == 'win32':
|
||||
use_shell = True
|
||||
else:
|
||||
use_shell = False
|
||||
try:
|
||||
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)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
e.strerror = 'File not found: {0}'.format(self.renderer)
|
||||
raise
|
||||
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
|
||||
|
||||
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, [], 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 not cycle 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()
|
||||
|
||||
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
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue