Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(229)

Side by Side Diff: third_party/logilab/common/graph.py

Issue 10447014: Add pylint to depot_tools. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Fix unittests. Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/logilab/common/fileutils.py ('k') | third_party/logilab/common/hg.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
3 #
4 # This file is part of logilab-common.
5 #
6 # logilab-common is free software: you can redistribute it and/or modify it unde r
7 # the terms of the GNU Lesser General Public License as published by the Free
8 # Software Foundation, either version 2.1 of the License, or (at your option) an y
9 # later version.
10 #
11 # logilab-common is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
14 # details.
15 #
16 # You should have received a copy of the GNU Lesser General Public License along
17 # with logilab-common. If not, see <http://www.gnu.org/licenses/>.
18 """Graph manipulation utilities.
19
20 (dot generation adapted from pypy/translator/tool/make_dot.py)
21 """
22
23 __docformat__ = "restructuredtext en"
24
25 __metaclass__ = type
26
27 import os.path as osp
28 import os
29 import sys
30 import tempfile
31 from logilab.common.compat import str_encode
32
33 def escape(value):
34 """Make <value> usable in a dot file."""
35 lines = [line.replace('"', '\\"') for line in value.split('\n')]
36 data = '\\l'.join(lines)
37 return '\\n' + data
38
39 def target_info_from_filename(filename):
40 """Transforms /some/path/foo.png into ('/some/path', 'foo.png', 'png')."""
41 basename = osp.basename(filename)
42 storedir = osp.dirname(osp.abspath(filename))
43 target = filename.split('.')[-1]
44 return storedir, basename, target
45
46
47 class DotBackend:
48 """Dot File backend."""
49 def __init__(self, graphname, rankdir=None, size=None, ratio=None,
50 charset='utf-8', renderer='dot', additionnal_param={}):
51 self.graphname = graphname
52 self.renderer = renderer
53 self.lines = []
54 self._source = None
55 self.emit("digraph %s {" % normalize_node_id(graphname))
56 if rankdir:
57 self.emit('rankdir=%s' % rankdir)
58 if ratio:
59 self.emit('ratio=%s' % ratio)
60 if size:
61 self.emit('size="%s"' % size)
62 if charset:
63 assert charset.lower() in ('utf-8', 'iso-8859-1', 'latin1'), \
64 'unsupported charset %s' % charset
65 self.emit('charset="%s"' % charset)
66 for param in additionnal_param.iteritems():
67 self.emit('='.join(param))
68
69 def get_source(self):
70 """returns self._source"""
71 if self._source is None:
72 self.emit("}\n")
73 self._source = '\n'.join(self.lines)
74 del self.lines
75 return self._source
76
77 source = property(get_source)
78
79 def generate(self, outputfile=None, dotfile=None, mapfile=None):
80 """Generates a graph file.
81
82 :param outputfile: filename and path [defaults to graphname.png]
83 :param dotfile: filename and path [defaults to graphname.dot]
84
85 :rtype: str
86 :return: a path to the generated file
87 """
88 import subprocess # introduced in py 2.4
89 name = self.graphname
90 if not dotfile:
91 # if 'outputfile' is a dot file use it as 'dotfile'
92 if outputfile and outputfile.endswith(".dot"):
93 dotfile = outputfile
94 else:
95 dotfile = '%s.dot' % name
96 if outputfile is not None:
97 storedir, basename, target = target_info_from_filename(outputfile)
98 if target != "dot":
99 pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
100 os.close(pdot)
101 else:
102 dot_sourcepath = osp.join(storedir, dotfile)
103 else:
104 target = 'png'
105 pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
106 ppng, outputfile = tempfile.mkstemp(".png", name)
107 os.close(pdot)
108 os.close(ppng)
109 pdot = open(dot_sourcepath, 'w')
110 pdot.write(str_encode(self.source, 'utf8'))
111 pdot.close()
112 if target != 'dot':
113 if sys.platform == 'win32':
114 use_shell = True
115 else:
116 use_shell = False
117 if mapfile:
118 subprocess.call([self.renderer, '-Tcmapx', '-o', mapfile, '-T', target, dot_sourcepath, '-o', outputfile],
119 shell=use_shell)
120 else:
121 subprocess.call([self.renderer, '-T', target,
122 dot_sourcepath, '-o', outputfile],
123 shell=use_shell)
124 os.unlink(dot_sourcepath)
125 return outputfile
126
127 def emit(self, line):
128 """Adds <line> to final output."""
129 self.lines.append(line)
130
131 def emit_edge(self, name1, name2, **props):
132 """emit an edge from <name1> to <name2>.
133 edge properties: see http://www.graphviz.org/doc/info/attrs.html
134 """
135 attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
136 n_from, n_to = normalize_node_id(name1), normalize_node_id(name2)
137 self.emit('%s -> %s [%s];' % (n_from, n_to, ", ".join(attrs)) )
138
139 def emit_node(self, name, **props):
140 """emit a node with given properties.
141 node properties: see http://www.graphviz.org/doc/info/attrs.html
142 """
143 attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
144 self.emit('%s [%s];' % (normalize_node_id(name), ", ".join(attrs)))
145
146 def normalize_node_id(nid):
147 """Returns a suitable DOT node id for `nid`."""
148 return '"%s"' % nid
149
150 class GraphGenerator:
151 def __init__(self, backend):
152 # the backend is responsible to output the graph in a particular format
153 self.backend = backend
154
155 # XXX doesn't like space in outpufile / mapfile
156 def generate(self, visitor, propshdlr, outputfile=None, mapfile=None):
157 # the visitor
158 # the property handler is used to get node and edge properties
159 # according to the graph and to the backend
160 self.propshdlr = propshdlr
161 for nodeid, node in visitor.nodes():
162 props = propshdlr.node_properties(node)
163 self.backend.emit_node(nodeid, **props)
164 for subjnode, objnode, edge in visitor.edges():
165 props = propshdlr.edge_properties(edge, subjnode, objnode)
166 self.backend.emit_edge(subjnode, objnode, **props)
167 return self.backend.generate(outputfile=outputfile, mapfile=mapfile)
168
169
170 class UnorderableGraph(Exception):
171 pass
172
173 def ordered_nodes(graph):
174 """takes a dependency graph dict as arguments and return an ordered tuple of
175 nodes starting with nodes without dependencies and up to the outermost node.
176
177 If there is some cycle in the graph, :exc:`UnorderableGraph` will be raised.
178
179 Also the given graph dict will be emptied.
180 """
181 # check graph consistency
182 cycles = get_cycles(graph)
183 if cycles:
184 cycles = '\n'.join([' -> '.join(cycle) for cycle in cycles])
185 raise UnorderableGraph('cycles in graph: %s' % cycles)
186 vertices = set(graph)
187 to_vertices = set()
188 for edges in graph.values():
189 to_vertices |= set(edges)
190 missing_vertices = to_vertices - vertices
191 if missing_vertices:
192 raise UnorderableGraph('missing vertices: %s' % ', '.join(missing_vertic es))
193 # order vertices
194 order = []
195 order_set = set()
196 old_len = None
197 while graph:
198 if old_len == len(graph):
199 raise UnorderableGraph('unknown problem with %s' % graph)
200 old_len = len(graph)
201 deps_ok = []
202 for node, node_deps in graph.items():
203 for dep in node_deps:
204 if dep not in order_set:
205 break
206 else:
207 deps_ok.append(node)
208 order.append(deps_ok)
209 order_set |= set(deps_ok)
210 for node in deps_ok:
211 del graph[node]
212 result = []
213 for grp in reversed(order):
214 result.extend(sorted(grp))
215 return tuple(result)
216
217
218 def get_cycles(graph_dict, vertices=None):
219 '''given a dictionary representing an ordered graph (i.e. key are vertices
220 and values is a list of destination vertices representing edges), return a
221 list of detected cycles
222 '''
223 if not graph_dict:
224 return ()
225 result = []
226 if vertices is None:
227 vertices = graph_dict.keys()
228 for vertice in vertices:
229 _get_cycles(graph_dict, vertice, [], result)
230 return result
231
232 def _get_cycles(graph_dict, vertice=None, path=None, result=None):
233 """recursive function doing the real work for get_cycles"""
234 if vertice in path:
235 cycle = [vertice]
236 for node in path[::-1]:
237 if node == vertice:
238 break
239 cycle.insert(0, node)
240 # make a canonical representation
241 start_from = min(cycle)
242 index = cycle.index(start_from)
243 cycle = cycle[index:] + cycle[0:index]
244 # append it to result if not already in
245 if not cycle in result:
246 result.append(cycle)
247 return
248 path.append(vertice)
249 try:
250 for node in graph_dict[vertice]:
251 _get_cycles(graph_dict, node, path, result)
252 except KeyError:
253 pass
254 path.pop()
255
256 def has_path(graph_dict, fromnode, tonode, path=None):
257 """generic function taking a simple graph definition as a dictionary, with
258 node has key associated to a list of nodes directly reachable from it.
259
260 Return None if no path exists to go from `fromnode` to `tonode`, else the
261 first path found (as a list including the destination node at last)
262 """
263 if path is None:
264 path = []
265 elif fromnode in path:
266 return None
267 path.append(fromnode)
268 for destnode in graph_dict[fromnode]:
269 if destnode == tonode or has_path(graph_dict, destnode, tonode, path):
270 return path[1:] + [tonode]
271 path.pop()
272 return None
273
OLDNEW
« no previous file with comments | « third_party/logilab/common/fileutils.py ('k') | third_party/logilab/common/hg.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698