Synopsis - Synopsis/Formatters/Dot.py

File: Synopsis/Formatters/Dot.py

      1#
      2# Copyright (C) 2000 Stefan Seefeld
      3# Copyright (C) 2000 Stephen Davies
      4# All rights reserved.
      5# Licensed to the public under the terms of the GNU LGPL (>= 2),
      6# see the file COPYING for details.
      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      # Quote to remove characters that dot can't handle
     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      #self.write(", fontSize = 10, height = 0.2, width = 0.4")
     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                  # Base is longer than parent name, or found a difference
    156                  base[j:] = []
    157                  break
    158         except: pass # typedefs etc may cause errors here.. ignore
    159      if not node.parents:
    160         base = self.scope
    161      return str(self.scope.prune(node.name))
    162
    163   #################### Type Visitor ##########################################
    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   #################### ASG Visitor ###########################################
    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      # NB: old version of dot needed the label surrounded in {}'s (?)
    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         #FIXME: we shouldn't only be looking for variables of the exact type,
    256         #       but also derived types such as pointers, references, STL containers, etc.
    257         #
    258         # find attributes of type 'Class' so we can link to it
    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 = {} # classes already visited, to prevent recursion
    296
    297   #################### Type Visitor ##########################################
    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      # to restore the ref/label...
    305      InheritanceGenerator.visit_declared_type_id(self, type)
    306
    307   #################### ASG Visitor ###########################################
    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      # Prevent recursion
    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      # if this is the main class and if there is a type dictionary,
    343      # look for classes that are derived from this class
    344
    345      # if this is the main class
    346      if self.__current == 1 and self.__types:
    347         # fool the visit_declared_type_id method to stop walking upwards
    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      # we only need the toc if format=='html'
    488      if format == 'html':
    489         # beware: HTML.Fragments.ClassHierarchyGraph sets self.toc !!
    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)