File: Synopsis/Formatters/Dot.py 1
2
3
4
5
6
7
8
9"""
10Uses 'dot' from graphviz to generate various graphs.
11"""
12
13from Synopsis.Processor import *
14from Synopsis.QualifiedName import *
15from Synopsis import ASG
16from Synopsis.Formatters import TOC
17from Synopsis.Formatters import quote_name, open_file
18import sys, os
19
20verbose = False
21debug = False
22
23class SystemError:
24 """Error thrown by the system() function. Attributes are 'retval', encoded
25 as per os.wait(): low-byte is killing signal number, high-byte is return
26 value of command."""
27
28 def __init__(self, retval, command):
29
30 self.retval = retval
31 self.command = command
32
33 def __repr__(self):
34
35 return 'SystemError: %(retval)x"%(command)s" failed.'%self.__dict__
36
37def system(command):
38 """Run the command. If the command fails, an exception SystemError is
39 thrown."""
40
41 ret = os.system(command)
42 if (ret>>8) != 0:
43 raise SystemError(ret, command)
44
45
46def normalize(color):
47 """Generate a color triplet from a color string."""
48
49 if type(color) is str and color[0] == '#':
50 return (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16))
51 elif type(color) is tuple:
52 return (color[0] * 255, color[1] * 255, color[2] * 255)
53
54
55def light(color):
56
57 import colorsys
58 hsv = colorsys.rgb_to_hsv(*color)
59 return colorsys.hsv_to_rgb(hsv[0], hsv[1], hsv[2]/2)
60
61
62class DotFileGenerator:
63 """A class that encapsulates the dot file generation"""
64 def __init__(self, os, direction, bgcolor):
65
66 self.__os = os
67 self.direction = direction
68 self.bgcolor = bgcolor and '"#%X%X%X"'%bgcolor
69 self.light_color = bgcolor and '"#%X%X%X"'%light(bgcolor) or 'gray75'
70 self.nodes = {}
71
72 def write(self, text): self.__os.write(text)
73
74 def write_node(self, ref, name, label, **attr):
75 """helper method to generate output for a given node"""
76
77 if self.nodes.has_key(name): return
78 self.nodes[name] = len(self.nodes)
79 number = self.nodes[name]
80
81
82 for p in [('<', '\<'), ('>', '\>'), ('{','\{'), ('}','\}')]:
83 label = label.replace(*p)
84
85 if self.bgcolor:
86 attr['fillcolor'] = self.bgcolor
87 attr['style'] = 'filled'
88
89 self.write("Node" + str(number) + " [shape=\"record\", label=\"{" + label + "}\"")
90
91 self.write(''.join([', %s=%s'%item for item in attr.items()]))
92 if ref: self.write(', URL="' + ref + '"')
93 self.write('];\n')
94
95 def write_edge(self, parent, child, **attr):
96
97 self.write("Node" + str(self.nodes[parent]) + " -> ")
98 self.write("Node" + str(self.nodes[child]))
99 self.write('[ color="black", fontsize=10, dir=back' + ''.join([', %s="%s"'%item for item in attr.items()]) + '];\n')
100
101class InheritanceGenerator(DotFileGenerator, ASG.Visitor):
102 """A Formatter that generates an inheritance graph. If the 'toc' argument is not None,
103 it is used to generate URLs. If no reference could be found in the toc, the node will
104 be grayed out."""
105 def __init__(self, os, direction, operations, attributes, aggregation,
106 toc, prefix, no_descend, bgcolor):
107
108 DotFileGenerator.__init__(self, os, direction, bgcolor)
109 if operations: self.__operations = []
110 else: self.__operations = None
111 if attributes: self.__attributes = []
112 else: self.__attributes = None
113 self.aggregation = aggregation
114 self.toc = toc
115 self.scope = QualifiedName()
116 if prefix:
117 if prefix.contains('::'):
118 self.scope = QualifiedCxxName(prefix.split('::'))
119 elif prefix.contains('.'):
120 self.scope = QualifiedPythonName(prefix.split('.'))
121 else:
122 self.scope = QualifiedName((prefix,))
123 self.__type_ref = None
124 self.__type_label = ''
125 self.__no_descend = no_descend
126 self.nodes = {}
127
128 def type_ref(self): return self.__type_ref
129 def type_label(self): return self.__type_label
130 def parameter(self): return self.__parameter
131
132 def format_type(self, typeObj):
133 "Returns a reference string for the given type object"
134
135 if typeObj is None: return "(unknown)"
136 typeObj.accept(self)
137 return self.type_label()
138
139 def clear_type(self):
140
141 self.__type_ref = None
142 self.__type_label = ''
143
144 def get_class_name(self, node):
145 """Returns the name of the given class node, relative to all its
146 parents. This makes the graph simpler by making the names shorter"""
147
148 base = node.name
149 for i in node.parents:
150 try:
151 parent = i.parent
152 pname = parent.name
153 for j in range(len(base)):
154 if j > len(pname) or pname[j] != base[j]:
155
156 base[j:] = []
157 break
158 except: pass
159 if not node.parents:
160 base = self.scope
161 return str(self.scope.prune(node.name))
162
163
164
165 def visit_modifier_type(self, type):
166
167 self.format_type(type.alias)
168 self.__type_label = ''.join(type.premod) + self.__type_label
169 self.__type_label = self.__type_label + ''.join(type.postmod)
170
171 def visit_unknown_type(self, type):
172
173 self.__type_ref = self.toc and self.toc[type.link] or None
174 self.__type_label = str(self.scope.prune(type.name))
175
176 def visit_builtin_type_id(self, type):
177
178 self.__type_ref = None
179 self.__type_label = type.name[-1]
180
181 def visit_dependent_type_id(self, type):
182
183 self.__type_ref = None
184 self.__type_label = type.name[-1]
185
186 def visit_declared_type_id(self, type):
187
188 self.__type_ref = self.toc and self.toc[type.declaration.name] or None
189 if isinstance(type.declaration, ASG.Class):
190 self.__type_label = self.get_class_name(type.declaration)
191 else:
192 self.__type_label = str(self.scope.prune(type.declaration.name))
193
194 def visit_parametrized_type_id(self, type):
195
196 if type.template:
197 type_ref = self.toc and self.toc[type.template.name] or None
198 type_label = str(self.scope.prune(type.template.name))
199 else:
200 type_ref = None
201 type_label = "(unknown)"
202 parameters_label = []
203 for p in type.parameters:
204 parameters_label.append(self.format_type(p))
205 self.__type_ref = type_ref
206 self.__type_label = type_label + "<" + ','.join(parameters_label) + ">"
207
208 def visit_template_id(self, type):
209 self.__type_ref = None
210 def clip(x, max=20):
211 if len(x) > max: return '...'
212 return x
213 self.__type_label = "template<%s>"%(clip(','.join([clip(self.format_type(p)) for p in type.parameters]), 40))
214
215
216
217 def visit_inheritance(self, node):
218
219 self.format_type(node.parent)
220 if self.type_ref():
221 self.write_node(self.type_ref().link, self.type_label(), self.type_label())
222 elif self.toc:
223 self.write_node('', self.type_label(), self.type_label(), color=self.light_color, fontcolor=self.light_color)
224 else:
225 self.write_node('', self.type_label(), self.type_label())
226
227 def visit_class(self, node):
228
229 if self.__operations is not None: self.__operations.append([])
230 if self.__attributes is not None: self.__attributes.append([])
231 name = self.get_class_name(node)
232 ref = self.toc and self.toc[node.name] or None
233 for d in node.declarations: d.accept(self)
234
235 label = name
236 if type(node) is ASG.ClassTemplate and node.template:
237 if self.direction == 'vertical':
238 label = self.format_type(node.template) + '\\n' + label
239 else:
240 label = self.format_type(node.template) + ' ' + label
241 if self.__operations or self.__attributes:
242 label = label + '\\n'
243 if self.__operations:
244 label += '|' + ''.join([x[-1] + '()\\l' for x in self.__operations[-1]])
245 if self.__attributes:
246 label += '|' + ''.join([x[-1] + '\\l' for x in self.__attributes[-1]])
247 if ref:
248 self.write_node(ref.link, name, label)
249 elif self.toc:
250 self.write_node('', name, label, color=self.light_color, fontcolor=self.light_color)
251 else:
252 self.write_node('', name, label)
253
254 if self.aggregation:
255
256
257
258
259 for a in filter(lambda a:isinstance(a, ASG.Variable), node.declarations):
260 if isinstance(a.vtype, ASG.DeclaredTypeId):
261 d = a.vtype.declaration
262 if isinstance(d, ASG.Class) and self.nodes.has_key(self.get_class_name(d)):
263 self.write_edge(self.get_class_name(node), self.get_class_name(d),
264 arrowtail='ediamond')
265
266 for p in node.parents:
267 p.accept(self)
268 self.write_edge(self.type_label(), name, arrowtail='empty')
269 if self.__no_descend: return
270 if self.__operations: self.__operations.pop()
271 if self.__attributes: self.__attributes.pop()
272
273 def visit_operation(self, operation):
274
275 if self.__operations:
276 self.__operations[-1].append(operation.real_name)
277
278 def visit_variable(self, variable):
279
280 if self.__attributes:
281 self.__attributes[-1].append(variable.name)
282
283class SingleInheritanceGenerator(InheritanceGenerator):
284 """A Formatter that generates an inheritance graph for a specific class.
285 This Visitor visits the ASG upwards, i.e. following the inheritance links, instead of
286 the declarations contained in a given scope."""
287
288 def __init__(self, os, direction, operations, attributes, levels, types,
289 toc, prefix, no_descend, bgcolor):
290 InheritanceGenerator.__init__(self, os, direction, operations, attributes, False,
291 toc, prefix, no_descend, bgcolor)
292 self.__levels = levels
293 self.__types = types
294 self.__current = 1
295 self.__visited_classes = {}
296
297
298
299 def visit_declared_type_id(self, type):
300 if self.__current < self.__levels or self.__levels == -1:
301 self.__current = self.__current + 1
302 type.declaration.accept(self)
303 self.__current = self.__current - 1
304
305 InheritanceGenerator.visit_declared_type_id(self, type)
306
307
308
309 def visit_inheritance(self, node):
310
311 node.parent.accept(self)
312 if self.type_label():
313 if self.type_ref():
314 self.write_node(self.type_ref().link, self.type_label(), self.type_label())
315 elif self.toc:
316 self.write_node('', self.type_label(), self.type_label(), color=self.light_color, fontcolor=self.light_color)
317 else:
318 self.write_node('', self.type_label(), self.type_label())
319
320 def visit_class(self, node):
321
322
323 if self.__visited_classes.has_key(id(node)): return
324 self.__visited_classes[id(node)] = None
325
326 name = self.get_class_name(node)
327 if self.__current == 1:
328 self.write_node('', name, name, style='filled', color=self.light_color, fontcolor=self.light_color)
329 else:
330 ref = self.toc and self.toc[node.name] or None
331 if ref:
332 self.write_node(ref.link, name, name)
333 elif self.toc:
334 self.write_node('', name, name, color=self.light_color, fontcolor=self.light_color)
335 else:
336 self.write_node('', name, name)
337
338 for p in node.parents:
339 p.accept(self)
340 if self.nodes.has_key(self.type_label()):
341 self.write_edge(self.type_label(), name, arrowtail='empty')
342
343
344
345
346 if self.__current == 1 and self.__types:
347
348 self.__levels = 0
349 for t in self.__types.values():
350 if isinstance(t, ASG.DeclaredTypeId):
351 child = t.declaration
352 if isinstance(child, ASG.Class):
353 for i in child.parents:
354 type = i.parent
355 type.accept(self)
356 if self.type_ref():
357 if self.type_ref().name == node.name:
358 child_label = self.get_class_name(child)
359 ref = self.toc and self.toc[child.name] or None
360 if ref:
361 self.write_node(ref.link, child_label, child_label)
362 elif self.toc:
363 self.write_node('', child_label, child_label, color=self.light_color, fontcolor=self.light_color)
364 else:
365 self.write_node('', child_label, child_label)
366
367 self.write_edge(name, child_label, arrowtail='empty')
368
369class FileDependencyGenerator(DotFileGenerator, ASG.Visitor):
370 """A Formatter that generates a file dependency graph"""
371
372 def visit_file(self, file):
373 if file.annotations['primary']:
374 self.write_node('', file.name, file.name)
375 for i in file.includes:
376 target = i.target
377 if target.annotations['primary']:
378 self.write_node('', target.name, target.name)
379 name = i.name
380 name = name.replace('"', '\\"')
381 self.write_edge(target.name, file.name, label=name, style='dashed')
382
383def _rel(frm, to):
384 "Find link to to relative to frm"
385
386 frm = frm.split('/'); to = to.split('/')
387 for l in range((len(frm)<len(to)) and len(frm)-1 or len(to)-1):
388 if to[0] == frm[0]: del to[0]; del frm[0]
389 else: break
390 if frm: to = ['..'] * (len(frm) - 1) + to
391 return '/'.join(to)
392
393def _convert_map(input, output, base_url):
394 """convert map generated from Dot to a html region map.
395 input and output are (open) streams"""
396
397 line = input.readline()
398 while line:
399 line = line[:-1]
400 if line[0:4] == "rect":
401 url, x1y1, x2y2 = line[4:].split()
402 x1, y1 = x1y1.split(',')
403 x2, y2 = x2y2.split(',')
404 output.write('<area alt="'+url+'" href="' + _rel(base_url, url) + '" shape="rect" coords="')
405 output.write(str(x1) + ", " + str(y1) + ", " + str(x2) + ", " + str(y2) + '" />\n')
406 line = input.readline()
407
408def _format(input, output, format):
409
410 command = 'dot -T%s -o "%s" "%s"'%(format, output, input)
411 if verbose: print "Dot Formatter: running command '" + command + "'"
412 try:
413 system(command)
414 except SystemError, e:
415 if debug:
416 print 'failed to execute "%s"'%command
417 raise InvalidCommand, "could not execute 'dot'"
418
419def _format_png(input, output): _format(input, output, "png")
420
421def _format_html(input, output, base_url):
422 """generate (active) image for html.
423 input and output are file names. If output ends
424 in '.html', its stem is used with an '.png' suffix for the
425 actual image."""
426
427 if output[-5:] == ".html": output = output[:-5]
428 _format_png(input, output + ".png")
429 _format(input, output + ".map", "imap")
430 prefix, name = os.path.split(output)
431 reference = name + ".png"
432 html = open_file(output + ".html")
433 html.write('<img alt="'+name+'" src="' + reference + '" hspace="8" vspace="8" border="0" usemap="#')
434 html.write(name + "_map\" />\n")
435 html.write("<map name=\"" + name + "_map\">")
436 dotmap = open(output + ".map", "r+")
437 _convert_map(dotmap, html, base_url)
438 dotmap.close()
439 os.remove(output + ".map")
440 html.write("</map>\n")
441
442class Formatter(Processor):
443 """The Formatter class acts merely as a frontend to
444 the various InheritanceGenerators"""
445
446 title = Parameter('Inheritance Graph', 'the title of the graph')
447 type = Parameter('class', 'type of graph (one of "file", "class", "single"')
448 hide_operations = Parameter(True, 'hide operations')
449 hide_attributes = Parameter(True, 'hide attributes')
450 show_aggregation = Parameter(False, 'show aggregation')
451 bgcolor = Parameter(None, 'background color for nodes')
452 format = Parameter('ps', 'Generate output in format "dot", "ps", "png", "svg", "gif", "map", "html"')
453 layout = Parameter('vertical', 'Direction of graph')
454 prefix = Parameter(None, 'Prefix to strip from all class names')
455 toc_in = Parameter([], 'list of table of content files to use for symbol lookup')
456 base_url = Parameter(None, 'base url to use for generated links')
457
458 def process(self, ir, **kwds):
459 global verbose, debug
460
461 self.set_parameters(kwds)
462 if self.bgcolor:
463 bgcolor = normalize(self.bgcolor)
464 if not bgcolor:
465 raise InvalidArgument('bgcolor=%s'%repr(self.bgcolor))
466 else:
467 self.bgcolor = bgcolor
468
469 self.ir = self.merge_input(ir)
470 verbose = self.verbose
471 debug = self.debug
472
473 formats = {'dot' : 'dot',
474 'ps' : 'ps',
475 'png' : 'png',
476 'gif' : 'gif',
477 'svg' : 'svg',
478 'map' : 'imap',
479 'html' : 'html'}
480
481 if formats.has_key(self.format): format = formats[self.format]
482 else:
483 print "Error: Unknown format. Available formats are:",
484 print ', '.join(formats.keys())
485 return self.ir
486
487
488 if format == 'html':
489
490 toc = getattr(self, 'toc', TOC.TOC(TOC.Linker()))
491 for t in self.toc_in: toc.load(t)
492 else:
493 toc = None
494
495 head, tail = os.path.split(self.output)
496 tmpfile = os.path.join(head, quote_name(tail)) + ".dot"
497 if self.verbose: print "Dot Formatter: Writing dot file..."
498 dotfile = open_file(tmpfile)
499 dotfile.write("digraph \"%s\" {\n"%(self.title))
500 if self.layout == 'horizontal':
501 dotfile.write('rankdir="LR";\n')
502 dotfile.write('ranksep="1.0";\n')
503 dotfile.write("node[shape=record, fontsize=10, height=0.2, width=0.4, color=black]\n")
504 if self.type == 'single':
505 generator = SingleInheritanceGenerator(dotfile, self.layout,
506 not self.hide_operations,
507 not self.hide_attributes,
508 -1, self.ir.asg.types,
509 toc, self.prefix, False,
510 self.bgcolor)
511 elif self.type == 'class':
512 generator = InheritanceGenerator(dotfile, self.layout,
513 not self.hide_operations,
514 not self.hide_attributes,
515 self.show_aggregation,
516 toc, self.prefix, False,
517 self.bgcolor)
518 elif self.type == 'file':
519 generator = FileDependencyGenerator(dotfile, self.layout, self.bgcolor)
520 else:
521 sys.stderr.write("Dot: unknown type\n");
522
523
524 if self.type == 'file':
525 for f in self.ir.files.values():
526 generator.visit_file(f)
527 else:
528 for d in self.ir.asg.declarations:
529 d.accept(generator)
530 dotfile.write("}\n")
531 dotfile.close()
532 if format == "dot":
533 os.rename(tmpfile, self.output)
534 elif format == "png":
535 _format_png(tmpfile, self.output)
536 os.remove(tmpfile)
537 elif format == "html":
538 _format_html(tmpfile, self.output, self.base_url)
539 os.remove(tmpfile)
540 else:
541 _format(tmpfile, self.output, format)
542 os.remove(tmpfile)
543
544 return self.ir
545
546
Generated on Thu Apr 16 16:27:14 2009 by
synopsis (version devel)