epydoc.docwriter.dotgraph

Package epydoc :: Package docwriter :: Module dotgraph |
[hide private]
[frames] | no frames]

# Source Code for Module epydoc.docwriter.dotgraph

       1  # epydoc -- Graph generation
       2  #
       3  # Copyright (C) 2005 Edward Loper
       4  # Author: Edward Loper <edloper@loper.org>
       5  # URL: <http://epydoc.sf.net>
       6  #
       7  # $Id: dotgraph.py 1663 2007-11-07 15:29:47Z dvarrazzo $
       8
       9  """
      10  Render Graphviz directed graphs as images.  Below are some examples.
      11
      12  .. importgraph::
      13
      14  .. classtree:: epydoc.apidoc.APIDoc
      15
      16  .. packagetree:: epydoc
      17
      18  :see: `The Graphviz Homepage
      19         <http://www.research.att.com/sw/tools/graphviz/>`__
      20  """
      21  __docformat__ = 'restructuredtext'
      22
      23  import re
      24  import sys
      25  from epydoc import log
      26  from epydoc.apidoc import *
      27  from epydoc.util import *
      28  from epydoc.compat import * # Backwards compatibility
      29
      30  # colors for graphs of APIDocs
      31  MODULE_BG = '#d8e8ff'
      32  CLASS_BG = '#d8ffe8'
      33  SELECTED_BG = '#ffd0d0'
      34  BASECLASS_BG = '#e0b0a0'
      35  SUBCLASS_BG = '#e0b0a0'
      36  ROUTINE_BG = '#e8d0b0' # maybe?
      37  INH_LINK_COLOR = '#800000'
      38
      39  ######################################################################
      40  #{ Dot Graphs
      41  ######################################################################
      42
      43  DOT_COMMAND = 'dot'
      44  """The command that should be used to spawn dot"""
      45
      46 -class DotGraph:
      47      """
      48      A ``dot`` directed graph.  The contents of the graph are
      49      constructed from the following instance variables:
      50
      51        - `nodes`: A list of `DotGraphNode`\\s, encoding the nodes
      52          that are present in the graph.  Each node is characterized
      53          a set of attributes, including an optional label.
      54        - `edges`: A list of `DotGraphEdge`\\s, encoding the edges
      55          that are present in the graph.  Each edge is characterized
      56          by a set of attributes, including an optional label.
      57        - `node_defaults`: Default attributes for nodes.
      58        - `edge_defaults`: Default attributes for edges.
      59        - `body`: A string that is appended as-is in the body of
      60          the graph.  This can be used to build more complex dot
      61          graphs.
      62
      63      The `link()` method can be used to resolve crossreference links
      64      within the graph.  In particular, if the 'href' attribute of any
      65      node or edge is assigned a value of the form ``<name>``, then it
      66      will be replaced by the URL of the object with that name.  This
      67      applies to the `body` as well as the `nodes` and `edges`.
      68
      69      To render the graph, use the methods `write()` and `render()`.
      70      Usually, you should call `link()` before you render the graph.
      71      """
      72      _uids = set()
      73      """A set of all uids that that have been generated, used to ensure
      74      that each new graph has a unique uid."""
      75
      76      DEFAULT_NODE_DEFAULTS={'fontsize':10, 'fontname': 'Helvetica'}
      77      DEFAULT_EDGE_DEFAULTS={'fontsize':10, 'fontname': 'Helvetica'}
      78
      79 -    def __init__(self, title, body='', node_defaults=None,
      80                   edge_defaults=None, caption=None):
      81          """
      82          Create a new `DotGraph`.
      83          """
      84          self.title = title
      85          """The title of the graph."""
      86
      87          self.caption = caption
      88          """A caption for the graph."""
      89
      90          self.nodes = []
      91          """A list of the nodes that are present in the graph.
      92
      93          :type: ``list`` of `DotGraphNode`"""
      94
      95          self.edges = []
      96          """A list of the edges that are present in the graph.
      97
      98          :type: ``list`` of `DotGraphEdge`"""
      99
     100          self.body = body
     101          """A string that should be included as-is in the body of the
     102          graph.
     103
     104          :type: ``str``"""
     105
     106          self.node_defaults = node_defaults or self.DEFAULT_NODE_DEFAULTS
     107          """Default attribute values for nodes."""
     108
     109          self.edge_defaults = edge_defaults or self.DEFAULT_EDGE_DEFAULTS
     110          """Default attribute values for edges."""
     111
     112          self.uid = re.sub(r'\W', '_', title).lower()
     113          """A unique identifier for this graph.  This can be used as a
     114          filename when rendering the graph.  No two `DotGraph`\s will
     115          have the same uid."""
     116
     117          # Encode the title, if necessary.
     118          if isinstance(self.title, unicode):
     119              self.title = self.title.encode('ascii', 'xmlcharrefreplace')
     120
     121          # Make sure the UID isn't too long.
     122          self.uid = self.uid[:30]
     123
     124          # Make sure the UID is unique
     125          if self.uid in self._uids:
     126              n = 2
     127              while ('%s_%s' % (self.uid, n)) in self._uids: n += 1
     128              self.uid = '%s_%s' % (self.uid, n)
     129          self._uids.add(self.uid)
     130
     131 -    def to_html(self, image_file, image_url, center=True):
     132          """
     133          Return the HTML code that should be uesd to display this graph
     134          (including a client-side image map).
     135
     136          :param image_url: The URL of the image file for this graph;
     137              this should be generated separately with the `write()` method.
     138          """
     139          # If dotversion >1.8.10, then we can generate the image and
     140          # the cmapx with a single call to dot.  Otherwise, we need to
     141          # run dot twice.
     142          if get_dot_version() > [1,8,10]:
     143              cmapx = self._run_dot('-Tgif', '-o%s' % image_file, '-Tcmapx')
     144              if cmapx is None: return '' # failed to render
     145          else:
     146              if not self.write(image_file):
     147                  return '' # failed to render
     148              cmapx = self.render('cmapx') or ''
     149
     150          # Decode the cmapx (dot uses utf-8)
     151          try:
     152              cmapx = cmapx.decode('utf-8')
     153          except UnicodeDecodeError:
     154              log.debug('%s: unable to decode cmapx from dot; graph will '
     155                        'not have clickable regions' % image_file)
     156              cmapx = ''
     157
     158          title = plaintext_to_html(self.title or '')
     159          caption = plaintext_to_html(self.caption or '')
     160          if title or caption:
     161              css_class = 'graph-with-title'
     162          else:
     163              css_class = 'graph-without-title'
     164          if len(title)+len(caption) > 80:
     165              title_align = 'left'
     166              table_width = ' width="600"'
     167          else:
     168              title_align = 'center'
     169              table_width = ''
     170
     171          if center: s = '<center>'
     172          if title or caption:
     173              s += ('<table border="0" cellpadding="0" cellspacing="0" '
     174                    'class="graph"%s>\n  <tr><td align="center">\n' %
     175                    table_width)
     176          s += ('  %s\n  <img src="%s" alt=%r usemap="#%s" '
     177                'ismap="ismap" class="%s" />\n' %
     178                (cmapx.strip(), image_url, title, self.uid, css_class))
     179          if title or caption:
     180              s += '  </td></tr>\n  <tr><td align=%r>\n' % title_align
     181              if title:
     182                  s += '<span class="graph-title">%s</span>' % title
     183              if title and caption:
     184                  s += ' -- '
     185              if caption:
     186                  s += '<span class="graph-caption">%s</span>' % caption
     187              s += '\n  </td></tr>\n</table><br />'
     188          if center: s += '</center>'
     189          return s
     190
     191 -    def link(self, docstring_linker):
     192          """
     193          Replace any href attributes whose value is ``<name>`` with
     194          the url of the object whose name is ``<name>``.
     195          """
     196          # Link xrefs in nodes
     197          self._link_href(self.node_defaults, docstring_linker)
     198          for node in self.nodes:
     199              self._link_href(node.attribs, docstring_linker)
     200
     201          # Link xrefs in edges
     202          self._link_href(self.edge_defaults, docstring_linker)
     203          for edge in self.nodes:
     204              self._link_href(edge.attribs, docstring_linker)
     205
     206          # Link xrefs in body
     207          def subfunc(m):
     208              url = docstring_linker.url_for(m.group(1))
     209              if url: return 'href="%s"%s' % (url, m.group(2))
     210              else: return ''
     211          self.body = re.sub("href\s*=\s*['\"]?<([\w\.]+)>['\"]?\s*(,?)",
     212                             subfunc, self.body)
     213
     214 -    def _link_href(self, attribs, docstring_linker):
     215          """Helper for `link()`"""
     216          if 'href' in attribs:
     217              m = re.match(r'^<([\w\.]+)>$', attribs['href'])
     218              if m:
     219                  url = docstring_linker.url_for(m.group(1))
     220                  if url: attribs['href'] = url
     221                  else: del attribs['href']
     222
     223 -    def write(self, filename, language='gif'):
     224          """
     225          Render the graph using the output format `language`, and write
     226          the result to `filename`.
     227
     228          :return: True if rendering was successful.
     229          """
     230          result = self._run_dot('-T%s' % language,
     231                                 '-o%s' % filename)
     232          # Decode into unicode, if necessary.
     233          if language == 'cmapx' and result is not None:
     234              result = result.decode('utf-8')
     235          return (result is not None)
     236
     237 -    def render(self, language='gif'):
     238          """
     239          Use the ``dot`` command to render this graph, using the output
     240          format `language`.  Return the result as a string, or ``None``
     241          if the rendering failed.
     242          """
     243          return self._run_dot('-T%s' % language)
     244
     245 -    def _run_dot(self, *options):
     246          try:
     247              result, err = run_subprocess((DOT_COMMAND,)+options,
     248                                           self.to_dotfile())
     249              if err: log.warning("Graphviz dot warning(s):\n%s" % err)
     250          except OSError, e:
     251              log.warning("Unable to render Graphviz dot graph:\n%s" % e)
     252              #log.debug(self.to_dotfile())
     253              return None
     254
     255          return result
     256
     257 -    def to_dotfile(self):
     258          """
     259          Return the string contents of the dot file that should be used
     260          to render this graph.
     261          """
     262          lines = ['digraph %s {' % self.uid,
     263                   'node [%s]' % ','.join(['%s="%s"' % (k,v) for (k,v)
     264                                           in self.node_defaults.items()]),
     265                   'edge [%s]' % ','.join(['%s="%s"' % (k,v) for (k,v)
     266                                           in self.edge_defaults.items()])]
     267          if self.body:
     268              lines.append(self.body)
     269          lines.append('/* Nodes */')
     270          for node in self.nodes:
     271              lines.append(node.to_dotfile())
     272          lines.append('/* Edges */')
     273          for edge in self.edges:
     274              lines.append(edge.to_dotfile())
     275          lines.append('}')
     276
     277          # Default dot input encoding is UTF-8
     278          return u'\n'.join(lines).encode('utf-8')
     279
     280 -class DotGraphNode:
     281      _next_id = 0
     282 -    def __init__(self, label=None, html_label=None, **attribs):
     283          if label is not None and html_label is not None:
     284              raise ValueError('Use label or html_label, not both.')
     285          if label is not None: attribs['label'] = label
     286          self._html_label = html_label
     287          self._attribs = attribs
     288          self.id = self.__class__._next_id
     289          self.__class__._next_id += 1
     290          self.port = None
     291
     292 -    def __getitem__(self, attr):
     293          return self._attribs[attr]
     294
     295 -    def __setitem__(self, attr, val):
     296          if attr == 'html_label':
     297              self._attribs.pop('label')
     298              self._html_label = val
     299          else:
     300              if attr == 'label': self._html_label = None
     301              self._attribs[attr] = val
     302
     303 -    def to_dotfile(self):
     304          """
     305          Return the dot commands that should be used to render this node.
     306          """
     307          attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()
     308                     if v is not None]
     309          if self._html_label:
     310              attribs.insert(0, 'label=<%s>' % (self._html_label,))
     311          if attribs: attribs = ' [%s]' % (','.join(attribs))
     312          return 'node%d%s' % (self.id, attribs)
     313
     314 -class DotGraphEdge:
     315 -    def __init__(self, start, end, label=None, **attribs):
     316          """
     317          :type start: `DotGraphNode`
     318          :type end: `DotGraphNode`
     319          """
     320          assert isinstance(start, DotGraphNode)
     321          assert isinstance(end, DotGraphNode)
     322          if label is not None: attribs['label'] = label
     323          self.start = start       #: :type: `DotGraphNode`
     324          self.end = end           #: :type: `DotGraphNode`
     325          self._attribs = attribs
     326
     327 -    def __getitem__(self, attr):
     328          return self._attribs[attr]
     329
     330 -    def __setitem__(self, attr, val):
     331          self._attribs[attr] = val
     332
     333 -    def to_dotfile(self):
     334          """
     335          Return the dot commands that should be used to render this edge.
     336          """
     337          # Set head & tail ports, if the nodes have preferred ports.
     338          attribs = self._attribs.copy()
     339          if (self.start.port is not None and 'headport' not in attribs):
     340              attribs['headport'] = self.start.port
     341          if (self.end.port is not None and 'tailport' not in attribs):
     342              attribs['tailport'] = self.end.port
     343          # Convert attribs to a string
     344          attribs = ','.join(['%s="%s"' % (k,v) for (k,v) in attribs.items()
     345                              if v is not None])
     346          if attribs: attribs = ' [%s]' % attribs
     347          # Return the dotfile edge.
     348          return 'node%d -> node%d%s' % (self.start.id, self.end.id, attribs)
     349
     350  ######################################################################
     351  #{ Specialized Nodes for UML Graphs
     352  ######################################################################
     353
     354 -class DotGraphUmlClassNode(DotGraphNode):
     355      """
     356      A specialized dot graph node used to display `ClassDoc`\s using
     357      UML notation.  The node is rendered as a table with three cells:
     358      the top cell contains the class name; the middle cell contains a
     359      list of attributes; and the bottom cell contains a list of
     360      operations::
     361
     362           +-------------+
     363           |  ClassName  |
     364           +-------------+
     365           | x: int      |
     366           |     ...     |
     367           +-------------+
     368           | f(self, x)  |
     369           |     ...     |
     370           +-------------+
     371
     372      `DotGraphUmlClassNode`\s may be *collapsed*, in which case they are
     373      drawn as a simple box containing the class name::
     374
     375           +-------------+
     376           |  ClassName  |
     377           +-------------+
     378
     379      Attributes with types corresponding to documented classes can
     380      optionally be converted into edges, using `link_attributes()`.
     381
     382      :todo: Add more options?
     383        - show/hide operation signature
     384        - show/hide operation signature types
     385        - show/hide operation signature return type
     386        - show/hide attribute types
     387        - use qualifiers
     388      """
     389
     390 -    def __init__(self, class_doc, linker, context, collapsed=False,
     391                   bgcolor=CLASS_BG, **options):
     392          """
     393          Create a new `DotGraphUmlClassNode` based on the class
     394          `class_doc`.
     395
     396          :Parameters:
     397              `linker` : `markup.DocstringLinker`
     398                  Used to look up URLs for classes.
     399              `context` : `APIDoc`
     400                  The context in which this node will be drawn; dotted
     401                  names will be contextualized to this context.
     402              `collapsed` : ``bool``
     403                  If true, then display this node as a simple box.
     404              `bgcolor` : ```str```
     405                  The background color for this node.
     406              `options` : ``dict``
     407                  A set of options used to control how the node should
     408                  be displayed.
     409
     410          :Keywords:
     411            - `show_private_vars`: If false, then private variables
     412              are filtered out of the attributes & operations lists.
     413              (Default: *False*)
     414            - `show_magic_vars`: If false, then magic variables
     415              (such as ``__init__`` and ``__add__``) are filtered out of
     416              the attributes & operations lists. (Default: *True*)
     417            - `show_inherited_vars`: If false, then inherited variables
     418              are filtered out of the attributes & operations lists.
     419              (Default: *False*)
     420            - `max_attributes`: The maximum number of attributes that
     421              should be listed in the attribute box.  If the class has
     422              more than this number of attributes, some will be
     423              ellided.  Ellipsis is marked with ``'...'``.
     424            - `max_operations`: The maximum number of operations that
     425              should be listed in the operation box.
     426            - `add_nodes_for_linked_attributes`: If true, then
     427              `link_attributes()` will create new a collapsed node for
     428              the types of a linked attributes if no node yet exists for
     429              that type.
     430          """
     431          if not isinstance(class_doc, ClassDoc):
     432              raise TypeError('Expected a ClassDoc as 1st argument')
     433
     434          self.class_doc = class_doc
     435          """The class represented by this node."""
     436
     437          self.linker = linker
     438          """Used to look up URLs for classes."""
     439
     440          self.context = context
     441          """The context in which the node will be drawn."""
     442
     443          self.bgcolor = bgcolor
     444          """The background color of the node."""
     445
     446          self.options = options
     447          """Options used to control how the node is displayed."""
     448
     449          self.collapsed = collapsed
     450          """If true, then draw this node as a simple box."""
     451
     452          self.attributes = []
     453          """The list of VariableDocs for attributes"""
     454
     455          self.operations = []
     456          """The list of VariableDocs for operations"""
     457
     458          self.qualifiers = []
     459          """List of (key_label, port) tuples."""
     460
     461          self.edges = []
     462          """List of edges used to represent this node's attributes.
     463          These should not be added to the `DotGraph`; this node will
     464          generate their dotfile code directly."""
     465
     466          # Initialize operations & attributes lists.
     467          show_private = options.get('show_private_vars', False)
     468          show_magic = options.get('show_magic_vars', True)
     469          show_inherited = options.get('show_inherited_vars', False)
     470          for var in class_doc.sorted_variables:
     471              name = var.canonical_name[-1]
     472              if ((not show_private and var.is_public == False) or
     473                  (not show_magic and re.match('__\w+__$', name)) or
     474                  (not show_inherited and var.container != class_doc)):
     475                  pass
     476              elif isinstance(var.value, RoutineDoc):
     477                  self.operations.append(var)
     478              else:
     479                  self.attributes.append(var)
     480
     481          # Initialize our dot node settings.
     482          tooltip = self._summary(class_doc)
     483          if tooltip:
     484              # dot chokes on a \n in the attribute...
     485              tooltip = " ".join(tooltip.split())
     486          else:
     487              tooltip = class_doc.canonical_name
     488          DotGraphNode.__init__(self, tooltip=tooltip,
     489                                width=0, height=0, shape='plaintext',
     490                                href=linker.url_for(class_doc) or NOOP_URL)
     491
     492      #/////////////////////////////////////////////////////////////////
     493      #{ Attribute Linking
     494      #/////////////////////////////////////////////////////////////////
     495
     496      SIMPLE_TYPE_RE = re.compile(
     497          r'^([\w\.]+)$')
     498      """A regular expression that matches descriptions of simple types."""
     499
     500      COLLECTION_TYPE_RE = re.compile(
     501          r'^(list|set|sequence|tuple|collection) of ([\w\.]+)$')
     502      """A regular expression that matches descriptions of collection types."""
     503
     504      MAPPING_TYPE_RE = re.compile(
     505          r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ([\w\.]+)$')
     506      """A regular expression that matches descriptions of mapping types."""
     507
     508      MAPPING_TO_COLLECTION_TYPE_RE = re.compile(
     509          r'^(dict|dictionary|map|mapping) from ([\w\.]+) to '
     510          r'(list|set|sequence|tuple|collection) of ([\w\.]+)$')
     511      """A regular expression that matches descriptions of mapping types
     512      whose value type is a collection."""
     513
     514      OPTIONAL_TYPE_RE = re.compile(
     515          r'^(None or|optional) ([\w\.]+)$|^([\w\.]+) or None$')
     516      """A regular expression that matches descriptions of optional types."""
     517
     518 -    def link_attributes(self, nodes):
     519          """
     520          Convert any attributes with type descriptions corresponding to
     521          documented classes to edges.  The following type descriptions
     522          are currently handled:
     523
     524            - Dotted names: Create an attribute edge to the named type,
     525              labelled with the variable name.
     526            - Collections: Create an attribute edge to the named type,
     527              labelled with the variable name, and marked with '*' at the
     528              type end of the edge.
     529            - Mappings: Create an attribute edge to the named type,
     530              labelled with the variable name, connected to the class by
     531              a qualifier box that contains the key type description.
     532            - Optional: Create an attribute edge to the named type,
     533              labelled with the variable name, and marked with '0..1' at
     534              the type end of the edge.
     535
     536          The edges created by `link_attributes()` are handled internally
     537          by `DotGraphUmlClassNode`; they should *not* be added directly
     538          to the `DotGraph`.
     539
     540          :param nodes: A dictionary mapping from `ClassDoc`\s to
     541              `DotGraphUmlClassNode`\s, used to look up the nodes for
     542              attribute types.  If the ``add_nodes_for_linked_attributes``
     543              option is used, then new nodes will be added to this
     544              dictionary for any types that are not already listed.
     545              These added nodes must be added to the `DotGraph`.
     546          """
     547          # Try to convert each attribute var into a graph edge.  If
     548          # _link_attribute returns true, then it succeeded, so remove
     549          # that var from our attribute list; otherwise, leave that var
     550          # in our attribute list.
     551          self.attributes = [var for var in self.attributes
     552                             if not self._link_attribute(var, nodes)]
     553
     554 -    def _link_attribute(self, var, nodes):
     555          """
     556          Helper for `link_attributes()`: try to convert the attribute
     557          variable `var` into an edge, and add that edge to
     558          `self.edges`.  Return ``True`` iff the variable was
     559          successfully converted to an edge (in which case, it should be
     560          removed from the attributes list).
     561          """
     562          type_descr = self._type_descr(var) or self._type_descr(var.value)
     563
     564          # Simple type.
     565          m = self.SIMPLE_TYPE_RE.match(type_descr)
     566          if m and self._add_attribute_edge(var, nodes, m.group(1)):
     567              return True
     568
     569          # Collection type.
     570          m = self.COLLECTION_TYPE_RE.match(type_descr)
     571          if m and self._add_attribute_edge(var, nodes, m.group(2),
     572                                            headlabel='*'):
     573              return True
     574
     575          # Optional type.
     576          m = self.OPTIONAL_TYPE_RE.match(type_descr)
     577          if m and self._add_attribute_edge(var, nodes, m.group(2) or m.group(3),
     578                                            headlabel='0..1'):
     579              return True
     580
     581          # Mapping type.
     582          m = self.MAPPING_TYPE_RE.match(type_descr)
     583          if m:
     584              port = 'qualifier_%s' % var.name
     585              if self._add_attribute_edge(var, nodes, m.group(3),
     586                                          tailport='%s:e' % port):
     587                  self.qualifiers.append( (m.group(2), port) )
     588                  return True
     589
     590          # Mapping to collection type.
     591          m = self.MAPPING_TO_COLLECTION_TYPE_RE.match(type_descr)
     592          if m:
     593              port = 'qualifier_%s' % var.name
     594              if self._add_attribute_edge(var, nodes, m.group(4), headlabel='*',
     595                                          tailport='%s:e' % port):
     596                  self.qualifiers.append( (m.group(2), port) )
     597                  return True
     598
     599          # We were unable to link this attribute.
     600          return False
     601
     602 -    def _add_attribute_edge(self, var, nodes, type_str, **attribs):
     603          """
     604          Helper for `link_attributes()`: try to add an edge for the
     605          given attribute variable `var`.  Return ``True`` if
     606          successful.
     607          """
     608          # Use the type string to look up a corresponding ValueDoc.
     609          type_doc = self.linker.docindex.find(type_str, var)
     610          if not type_doc: return False
     611
     612          # Make sure the type is a class.
     613          if not isinstance(type_doc, ClassDoc): return False
     614
     615          # Get the type ValueDoc's node.  If it doesn't have one (and
     616          # add_nodes_for_linked_attributes=True), then create it.
     617          type_node = nodes.get(type_doc)
     618          if not type_node:
     619              if self.options.get('add_nodes_for_linked_attributes', True):
     620                  type_node = DotGraphUmlClassNode(type_doc, self.linker,
     621                                                   self.context, collapsed=True)
     622                  nodes[type_doc] = type_node
     623              else:
     624                  return False
     625
     626          # Add an edge from self to the target type node.
     627          # [xx] should I set constraint=false here?
     628          attribs.setdefault('headport', 'body')
     629          attribs.setdefault('tailport', 'body')
     630          url = self.linker.url_for(var) or NOOP_URL
     631          self.edges.append(DotGraphEdge(self, type_node, label=var.name,
     632                          arrowhead='open', href=url,
     633                          tooltip=var.canonical_name, labeldistance=1.5,
     634                          **attribs))
     635          return True
     636
     637      #/////////////////////////////////////////////////////////////////
     638      #{ Helper Methods
     639      #/////////////////////////////////////////////////////////////////
     640 -    def _summary(self, api_doc):
     641          """Return a plaintext summary for `api_doc`"""
     642          if not isinstance(api_doc, APIDoc): return ''
     643          if api_doc.summary in (None, UNKNOWN): return ''
     644          summary = api_doc.summary.to_plaintext(None).strip()
     645          return plaintext_to_html(summary)
     646
     647      _summary = classmethod(_summary)
     648
     649 -    def _type_descr(self, api_doc):
     650          """Return a plaintext type description for `api_doc`"""
     651          if not hasattr(api_doc, 'type_descr'): return ''
     652          if api_doc.type_descr in (None, UNKNOWN): return ''
     653          type_descr = api_doc.type_descr.to_plaintext(self.linker).strip()
     654          return plaintext_to_html(type_descr)
     655
     656 -    def _tooltip(self, var_doc):
     657          """Return a tooltip for `var_doc`."""
     658          return (self._summary(var_doc) or
     659                  self._summary(var_doc.value) or
     660                  var_doc.canonical_name)
     661
     662      #/////////////////////////////////////////////////////////////////
     663      #{ Rendering
     664      #/////////////////////////////////////////////////////////////////
     665
     666 -    def _attribute_cell(self, var_doc):
     667          # Construct the label
     668          label = var_doc.name
     669          type_descr = (self._type_descr(var_doc) or
     670                        self._type_descr(var_doc.value))
     671          if type_descr: label += ': %s' % type_descr
     672          # Get the URL
     673          url = self.linker.url_for(var_doc) or NOOP_URL
     674          # Construct & return the pseudo-html code
     675          return self._ATTRIBUTE_CELL % (url, self._tooltip(var_doc), label)
     676
     677 -    def _operation_cell(self, var_doc):
     678          """
     679          :todo: do 'word wrapping' on the signature, by starting a new
     680                 row in the table, if necessary.  How to indent the new
     681                 line?  Maybe use align=right?  I don't think dot has a
     682                 &nbsp;.
     683          :todo: Optionally add return type info?
     684          """
     685          # Construct the label (aka function signature)
     686          func_doc = var_doc.value
     687          args = [self._operation_arg(n, d, func_doc) for (n, d)
     688                  in zip(func_doc.posargs, func_doc.posarg_defaults)]
     689          args = [plaintext_to_html(arg) for arg in args]
     690          if func_doc.vararg: args.append('*'+func_doc.vararg)
     691          if func_doc.kwarg: args.append('**'+func_doc.kwarg)
     692          label = '%s(%s)' % (var_doc.name, ', '.join(args))
     693          # Get the URL
     694          url = self.linker.url_for(var_doc) or NOOP_URL
     695          # Construct & return the pseudo-html code
     696          return self._OPERATION_CELL % (url, self._tooltip(var_doc), label)
     697
     698 -    def _operation_arg(self, name, default, func_doc):
     699          """
     700          :todo: Handle tuple args better
     701          :todo: Optionally add type info?
     702          """
     703          if default is None:
     704              return '%s' % name
     705          else:
     706              pyval_repr = default.summary_pyval_repr().to_plaintext(None)
     707              return '%s=%s' % (name, pyval_repr)
     708
     709 -    def _qualifier_cell(self, key_label, port):
     710          return self._QUALIFIER_CELL  % (port, self.bgcolor, key_label)
     711
     712      #: args: (url, tooltip, label)
     713      _ATTRIBUTE_CELL = '''
     714      <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR>
     715      '''
     716
     717      #: args: (url, tooltip, label)
     718      _OPERATION_CELL = '''
     719      <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR>
     720      '''
     721
     722      #: args: (port, bgcolor, label)
     723      _QUALIFIER_CELL = '''
     724      <TR><TD VALIGN="BOTTOM" PORT="%s" BGCOLOR="%s" BORDER="1">%s</TD></TR>
     725      '''
     726
     727      _QUALIFIER_DIV = '''
     728      <TR><TD VALIGN="BOTTOM" HEIGHT="10" WIDTH="10" FIXEDSIZE="TRUE"></TD></TR>
     729      '''
     730
     731      #: Args: (rowspan, bgcolor, classname, attributes, operations, qualifiers)
     732      _LABEL = '''
     733      <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0">
     734        <TR><TD ROWSPAN="%s">
     735          <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"
     736                 CELLPADDING="0" PORT="body" BGCOLOR="%s">
     737            <TR><TD>%s</TD></TR>
     738            <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
     739              %s</TABLE></TD></TR>
     740            <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
     741              %s</TABLE></TD></TR>
     742          </TABLE>
     743        </TD></TR>
     744        %s
     745      </TABLE>'''
     746
     747      _COLLAPSED_LABEL = '''
     748      <TABLE CELLBORDER="0" BGCOLOR="%s" PORT="body">
     749        <TR><TD>%s</TD></TR>
     750      </TABLE>'''
     751
     752 -    def _get_html_label(self):
     753          # Get the class name & contextualize it.
     754          classname = self.class_doc.canonical_name
     755          classname = classname.contextualize(self.context.canonical_name)
     756
     757          # If we're collapsed, display the node as a single box.
     758          if self.collapsed:
     759              return self._COLLAPSED_LABEL % (self.bgcolor, classname)
     760
     761          # Construct the attribute list.  (If it's too long, truncate)
     762          attrib_cells = [self._attribute_cell(a) for a in self.attributes]
     763          max_attributes = self.options.get('max_attributes', 15)
     764          if len(attrib_cells) == 0:
     765              attrib_cells = ['<TR><TD></TD></TR>']
     766          elif len(attrib_cells) > max_attributes:
     767              attrib_cells[max_attributes-2:-1] = ['<TR><TD>...</TD></TR>']
     768          attributes = ''.join(attrib_cells)
     769
     770          # Construct the operation list.  (If it's too long, truncate)
     771          oper_cells = [self._operation_cell(a) for a in self.operations]
     772          max_operations = self.options.get('max_operations', 15)
     773          if len(oper_cells) == 0:
     774              oper_cells = ['<TR><TD></TD></TR>']
     775          elif len(oper_cells) > max_operations:
     776              oper_cells[max_operations-2:-1] = ['<TR><TD>...</TD></TR>']
     777          operations = ''.join(oper_cells)
     778
     779          # Construct the qualifier list & determine the rowspan.
     780          if self.qualifiers:
     781              rowspan = len(self.qualifiers)*2+2
     782              div = self._QUALIFIER_DIV
     783              qualifiers = div+div.join([self._qualifier_cell(l,p) for
     784                                       (l,p) in self.qualifiers])+div
     785          else:
     786              rowspan = 1
     787              qualifiers = ''
     788
     789          # Put it all together.
     790          return self._LABEL % (rowspan, self.bgcolor, classname,
     791                                attributes, operations, qualifiers)
     792
     793 -    def to_dotfile(self):
     794          attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()]
     795          attribs.append('label=<%s>' % self._get_html_label())
     796          s = 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
     797          if not self.collapsed:
     798              for edge in self.edges:
     799                  s += '\n' + edge.to_dotfile()
     800          return s
     801
     802 -class DotGraphUmlModuleNode(DotGraphNode):
     803      """
     804      A specialized dot grah node used to display `ModuleDoc`\s using
     805      UML notation.  Simple module nodes look like::
     806
     807          .----.
     808          +------------+
     809          | modulename |
     810          +------------+
     811
     812      Packages nodes are drawn with their modules & subpackages nested
     813      inside::
     814
     815          .----.
     816          +----------------------------------------+
     817          | packagename                            |
     818          |                                        |
     819          |  .----.       .----.       .----.      |
     820          |  +---------+  +---------+  +---------+ |
     821          |  | module1 |  | module2 |  | module3 | |
     822          |  +---------+  +---------+  +---------+ |
     823          |                                        |
     824          +----------------------------------------+
     825
     826      """
     827 -    def __init__(self, module_doc, linker, context, collapsed=False,
     828                   excluded_submodules=(), **options):
     829          self.module_doc = module_doc
     830          self.linker = linker
     831          self.context = context
     832          self.collapsed = collapsed
     833          self.options = options
     834          self.excluded_submodules = excluded_submodules
     835          DotGraphNode.__init__(self, shape='plaintext',
     836                                href=linker.url_for(module_doc) or NOOP_URL,
     837                                tooltip=module_doc.canonical_name)
     838
     839      #: Expects: (color, color, url, tooltip, body)
     840      _MODULE_LABEL = '''
     841      <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" ALIGN="LEFT">
     842      <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16"
     843              FIXEDSIZE="true" BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR>
     844      <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" WIDTH="20"
     845              PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR>
     846      </TABLE>'''
     847
     848      #: Expects: (name, body_rows)
     849      _NESTED_BODY = '''
     850      <TABLE BORDER="0" CELLBORDER="0" CELLPADDING="0" CELLSPACING="0">
     851      <TR><TD ALIGN="LEFT">%s</TD></TR>
     852      %s
     853      </TABLE>'''
     854
     855      #: Expects: (cells,)
     856      _NESTED_BODY_ROW = '''
     857      <TR><TD>
     858        <TABLE BORDER="0" CELLBORDER="0"><TR>%s</TR></TABLE>
     859      </TD></TR>'''
     860
     861 -    def _get_html_label(self, package):
     862          """
     863          :Return: (label, depth, width) where:
     864
     865            - ``label`` is the HTML label
     866            - ``depth`` is the depth of the package tree (for coloring)
     867            - ``width`` is the max width of the HTML label, roughly in
     868              units of characters.
     869          """
     870          MAX_ROW_WIDTH = 80 # unit is roughly characters.
     871          pkg_name = package.canonical_name
     872          pkg_url = self.linker.url_for(package) or NOOP_URL
     873
     874          if (not package.is_package or len(package.submodules) == 0 or
     875              self.collapsed):
     876              pkg_color = self._color(package, 1)
     877              label = self._MODULE_LABEL % (pkg_color, pkg_color,
     878                                            pkg_url, pkg_name, pkg_name[-1])
     879              return (label, 1, len(pkg_name[-1])+3)
     880
     881          # Get the label for each submodule, and divide them into rows.
     882          row_list = ['']
     883          row_width = 0
     884          max_depth = 0
     885          max_row_width = len(pkg_name[-1])+3
     886          for submodule in package.submodules:
     887              if submodule in self.excluded_submodules: continue
     888              # Get the submodule's label.
     889              label, depth, width = self._get_html_label(submodule)
     890              # Check if we should start a new row.
     891              if row_width > 0 and width+row_width > MAX_ROW_WIDTH:
     892                  row_list.append('')
     893                  row_width = 0
     894              # Add the submodule's label to the row.
     895              row_width += width
     896              row_list[-1] += '<TD ALIGN="LEFT">%s</TD>' % label
     897              # Update our max's.
     898              max_depth = max(depth, max_depth)
     899              max_row_width = max(row_width, max_row_width)
     900
     901          # Figure out which color to use.
     902          pkg_color = self._color(package, depth+1)
     903
     904          # Assemble & return the label.
     905          rows = ''.join([self._NESTED_BODY_ROW % r for r in row_list])
     906          body = self._NESTED_BODY % (pkg_name, rows)
     907          label = self._MODULE_LABEL % (pkg_color, pkg_color,
     908                                        pkg_url, pkg_name, body)
     909          return label, max_depth+1, max_row_width
     910
     911      _COLOR_DIFF = 24
     912 -    def _color(self, package, depth):
     913          if package == self.context: return SELECTED_BG
     914          else:
     915              # Parse the base color.
     916              if re.match(MODULE_BG, 'r#[0-9a-fA-F]{6}$'):
     917                  base = int(MODULE_BG[1:], 16)
     918              else:
     919                  base = int('d8e8ff', 16)
     920              red = (base & 0xff0000) >> 16
     921              green = (base & 0x00ff00) >> 8
     922              blue = (base & 0x0000ff)
     923              # Make it darker with each level of depth. (but not *too*
     924              # dark -- package name needs to be readable)
     925              red = max(64, red-(depth-1)*self._COLOR_DIFF)
     926              green = max(64, green-(depth-1)*self._COLOR_DIFF)
     927              blue = max(64, blue-(depth-1)*self._COLOR_DIFF)
     928              # Convert it back to a color string
     929              return '#%06x' % ((red<<16)+(green<<8)+blue)
     930
     931 -    def to_dotfile(self):
     932          attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()]
     933          label, depth, width = self._get_html_label(self.module_doc)
     934          attribs.append('label=<%s>' % label)
     935          return 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
     936
     937
     938
     939  ######################################################################
     940  #{ Graph Generation Functions
     941  ######################################################################
     942
     943 -def package_tree_graph(packages, linker, context=None, **options):
     944      """
     945      Return a `DotGraph` that graphically displays the package
     946      hierarchies for the given packages.
     947      """
     948      if options.get('style', 'uml') == 'uml': # default to uml style?
     949          if get_dot_version() >= [2]:
     950              return uml_package_tree_graph(packages, linker, context,
     951                                               **options)
     952          elif 'style' in options:
     953              log.warning('UML style package trees require dot version 2.0+')
     954
     955      graph = DotGraph('Package Tree for %s' % name_list(packages, context),
     956                       body='ranksep=.3\n;nodesep=.1\n',
     957                       edge_defaults={'dir':'none'})
     958
     959      # Options
     960      if options.get('dir', 'TB') != 'TB': # default: top-to-bottom
     961          graph.body += 'rankdir=%s\n' % options.get('dir', 'TB')
     962
     963      # Get a list of all modules in the package.
     964      queue = list(packages)
     965      modules = set(packages)
     966      for module in queue:
     967          queue.extend(module.submodules)
     968          modules.update(module.submodules)
     969
     970      # Add a node for each module.
     971      nodes = add_valdoc_nodes(graph, modules, linker, context)
     972
     973      # Add an edge for each package/submodule relationship.
     974      for module in modules:
     975          for submodule in module.submodules:
     976              graph.edges.append(DotGraphEdge(nodes[module], nodes[submodule],
     977                                              headport='tab'))
     978
     979      return graph
     980
     981 -def uml_package_tree_graph(packages, linker, context=None, **options):
     982      """
     983      Return a `DotGraph` that graphically displays the package
     984      hierarchies for the given packages as a nested set of UML
     985      symbols.
     986      """
     987      graph = DotGraph('Package Tree for %s' % name_list(packages, context))
     988      # Remove any packages whose containers are also in the list.
     989      root_packages = []
     990      for package1 in packages:
     991          for package2 in packages:
     992              if (package1 is not package2 and
     993                  package2.canonical_name.dominates(package1.canonical_name)):
     994                  break
     995          else:
     996              root_packages.append(package1)
     997      # If the context is a variable, then get its value.
     998      if isinstance(context, VariableDoc) and context.value is not UNKNOWN:
     999          context = context.value
    1000      # Return a graph with one node for each root package.
    1001      for package in root_packages:
    1002          graph.nodes.append(DotGraphUmlModuleNode(package, linker, context))
    1003      return graph
    1004
    1005  ######################################################################
    1006 -def class_tree_graph(bases, linker, context=None, **options):
    1007      """
    1008      Return a `DotGraph` that graphically displays the class
    1009      hierarchy for the given classes.  Options:
    1010
    1011        - exclude
    1012        - dir: LR|RL|BT requests a left-to-right, right-to-left, or
    1013          bottom-to- top, drawing.  (corresponds to the dot option
    1014          'rankdir'
    1015      """
    1016      if isinstance(bases, ClassDoc): bases = [bases]
    1017      graph = DotGraph('Class Hierarchy for %s' % name_list(bases, context),
    1018                       body='ranksep=0.3\n',
    1019                       edge_defaults={'sametail':True, 'dir':'none'})
    1020
    1021      # Options
    1022      if options.get('dir', 'TB') != 'TB': # default: top-down
    1023          graph.body += 'rankdir=%s\n' % options.get('dir', 'TB')
    1024      exclude = options.get('exclude', ())
    1025
    1026      # Find all superclasses & subclasses of the given classes.
    1027      classes = set(bases)
    1028      queue = list(bases)
    1029      for cls in queue:
    1030          if isinstance(cls, ClassDoc):
    1031              if cls.subclasses not in (None, UNKNOWN):
    1032                  subclasses = cls.subclasses
    1033                  if exclude:
    1034                      subclasses = [d for d in subclasses if d not in exclude]
    1035                  queue.extend(subclasses)
    1036                  classes.update(subclasses)
    1037      queue = list(bases)
    1038      for cls in queue:
    1039          if isinstance(cls, ClassDoc):
    1040              if cls.bases not in (None, UNKNOWN):
    1041                  bases = cls.bases
    1042                  if exclude:
    1043                      bases = [d for d in bases if d not in exclude]
    1044                  queue.extend(bases)
    1045                  classes.update(bases)
    1046
    1047      # Add a node for each cls.
    1048      classes = [d for d in classes if isinstance(d, ClassDoc)
    1049                 if d.pyval is not object]
    1050      nodes = add_valdoc_nodes(graph, classes, linker, context)
    1051
    1052      # Add an edge for each package/subclass relationship.
    1053      edges = set()
    1054      for cls in classes:
    1055          for subcls in cls.subclasses:
    1056              if cls in nodes and subcls in nodes:
    1057                  edges.add((nodes[cls], nodes[subcls]))
    1058      graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
    1059
    1060      return graph
    1061
    1062  ######################################################################
    1063 -def uml_class_tree_graph(class_doc, linker, context=None, **options):
    1064      """
    1065      Return a `DotGraph` that graphically displays the class hierarchy
    1066      for the given class, using UML notation.  Options:
    1067
    1068        - max_attributes
    1069        - max_operations
    1070        - show_private_vars
    1071        - show_magic_vars
    1072        - link_attributes
    1073      """
    1074      nodes = {} # ClassDoc -> DotGraphUmlClassNode
    1075      exclude = options.get('exclude', ())
    1076
    1077      # Create nodes for class_doc and all its bases.
    1078      for cls in class_doc.mro():
    1079          if cls.pyval is object: continue # don't include `object`.
    1080          if cls in exclude: break # stop if we get to an excluded class.
    1081          if cls == class_doc: color = SELECTED_BG
    1082          else: color = BASECLASS_BG
    1083          nodes[cls] = DotGraphUmlClassNode(cls, linker, context,
    1084                                            show_inherited_vars=False,
    1085                                            collapsed=False, bgcolor=color)
    1086
    1087      # Create nodes for all class_doc's subclasses.
    1088      queue = [class_doc]
    1089      for cls in queue:
    1090          if (isinstance(cls, ClassDoc) and
    1091              cls.subclasses not in (None, UNKNOWN)):
    1092              for subcls in cls.subclasses:
    1093                  subcls_name = subcls.canonical_name[-1]
    1094                  if subcls not in nodes and subcls not in exclude:
    1095                      queue.append(subcls)
    1096                      nodes[subcls] = DotGraphUmlClassNode(
    1097                          subcls, linker, context, collapsed=True,
    1098                          bgcolor=SUBCLASS_BG)
    1099
    1100      # Only show variables in the class where they're defined for
    1101      # *class_doc*.
    1102      mro = class_doc.mro()
    1103      for name, var in class_doc.variables.items():
    1104          i = mro.index(var.container)
    1105          for base in mro[i+1:]:
    1106              if base.pyval is object: continue # don't include `object`.
    1107              overridden_var = base.variables.get(name)
    1108              if overridden_var and overridden_var.container == base:
    1109                  try:
    1110                      if isinstance(overridden_var.value, RoutineDoc):
    1111                          nodes[base].operations.remove(overridden_var)
    1112                      else:
    1113                          nodes[base].attributes.remove(overridden_var)
    1114                  except ValueError:
    1115                      pass # var is filtered (eg private or magic)
    1116
    1117      # Keep track of which nodes are part of the inheritance graph
    1118      # (since link_attributes might add new nodes)
    1119      inheritance_nodes = set(nodes.values())
    1120
    1121      # Turn attributes into links.
    1122      if options.get('link_attributes', True):
    1123          for node in nodes.values():
    1124              node.link_attributes(nodes)
    1125              # Make sure that none of the new attribute edges break the
    1126              # rank ordering assigned by inheritance.
    1127              for edge in node.edges:
    1128                  if edge.end in inheritance_nodes:
    1129                      edge['constraint'] = 'False'
    1130
    1131      # Construct the graph.
    1132      graph = DotGraph('UML class diagram for %s' % class_doc.canonical_name,
    1133                       body='ranksep=.2\n;nodesep=.3\n')
    1134      graph.nodes = nodes.values()
    1135
    1136      # Add inheritance edges.
    1137      for node in inheritance_nodes:
    1138          for base in node.class_doc.bases:
    1139              if base in nodes:
    1140                  graph.edges.append(DotGraphEdge(nodes[base], node,
    1141                                dir='back', arrowtail='empty',
    1142                                headport='body', tailport='body',
    1143                                color=INH_LINK_COLOR, weight=100,
    1144                                style='bold'))
    1145
    1146      # And we're done!
    1147      return graph
    1148
    1149  ######################################################################
    1150 -def import_graph(modules, docindex, linker, context=None, **options):
    1151      graph = DotGraph('Import Graph', body='ranksep=.3\n;nodesep=.3\n')
    1152
    1153      # Options
    1154      if options.get('dir', 'RL') != 'TB': # default: right-to-left.
    1155          graph.body += 'rankdir=%s\n' % options.get('dir', 'RL')
    1156
    1157      # Add a node for each module.
    1158      nodes = add_valdoc_nodes(graph, modules, linker, context)
    1159
    1160      # Edges.
    1161      edges = set()
    1162      for dst in modules:
    1163          if dst.imports in (None, UNKNOWN): continue
    1164          for var_name in dst.imports:
    1165              for i in range(len(var_name), 0, -1):
    1166                  val_doc = docindex.find(var_name[:i], context)
    1167                  if isinstance(val_doc, ModuleDoc):
    1168                      if val_doc in nodes and dst in nodes:
    1169                          edges.add((nodes[val_doc], nodes[dst]))
    1170                      break
    1171      graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
    1172
    1173      return graph
    1174
    1175  ######################################################################
    1176 -def call_graph(api_docs, docindex, linker, context=None, **options):
    1177      """
    1178      :param options:
    1179          - ``dir``: rankdir for the graph.  (default=LR)
    1180          - ``add_callers``: also include callers for any of the
    1181            routines in ``api_docs``.  (default=False)
    1182          - ``add_callees``: also include callees for any of the
    1183            routines in ``api_docs``.  (default=False)
    1184      :todo: Add an ``exclude`` option?
    1185      """
    1186      if docindex.callers is None:
    1187          log.warning("No profiling information for call graph!")
    1188          return DotGraph('Call Graph') # return None instead?
    1189
    1190      if isinstance(context, VariableDoc):
    1191          context = context.value
    1192
    1193      # Get the set of requested functions.
    1194      functions = []
    1195      for api_doc in api_docs:
    1196          # If it's a variable, get its value.
    1197          if isinstance(api_doc, VariableDoc):
    1198              api_doc = api_doc.value
    1199          # Add the value to the functions list.
    1200          if isinstance(api_doc, RoutineDoc):
    1201              functions.append(api_doc)
    1202          elif isinstance(api_doc, NamespaceDoc):
    1203              for vardoc in api_doc.variables.values():
    1204                  if isinstance(vardoc.value, RoutineDoc):
    1205                      functions.append(vardoc.value)
    1206
    1207      # Filter out functions with no callers/callees?
    1208      # [xx] this isnt' quite right, esp if add_callers or add_callees
    1209      # options are fales.
    1210      functions = [f for f in functions if
    1211                   (f in docindex.callers) or (f in docindex.callees)]
    1212
    1213      # Add any callers/callees of the selected functions
    1214      func_set = set(functions)
    1215      if options.get('add_callers', False) or options.get('add_callees', False):
    1216          for func_doc in functions:
    1217              if options.get('add_callers', False):
    1218                  func_set.update(docindex.callers.get(func_doc, ()))
    1219              if options.get('add_callees', False):
    1220                  func_set.update(docindex.callees.get(func_doc, ()))
    1221
    1222      graph = DotGraph('Call Graph for %s' % name_list(api_docs, context),
    1223                       node_defaults={'shape':'box', 'width': 0, 'height': 0})
    1224
    1225      # Options
    1226      if options.get('dir', 'LR') != 'TB': # default: left-to-right
    1227          graph.body += 'rankdir=%s\n' % options.get('dir', 'LR')
    1228
    1229      nodes = add_valdoc_nodes(graph, func_set, linker, context)
    1230
    1231      # Find the edges.
    1232      edges = set()
    1233      for func_doc in functions:
    1234          for caller in docindex.callers.get(func_doc, ()):
    1235              if caller in nodes:
    1236                  edges.add( (nodes[caller], nodes[func_doc]) )
    1237          for callee in docindex.callees.get(func_doc, ()):
    1238              if callee in nodes:
    1239                  edges.add( (nodes[func_doc], nodes[callee]) )
    1240      graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
    1241
    1242      return graph
    1243
    1244  ######################################################################
    1245  #{ Dot Version
    1246  ######################################################################
    1247
    1248  _dot_version = None
    1249  _DOT_VERSION_RE = re.compile(r'dot version ([\d\.]+)')
    1250 -def get_dot_version():
    1251      global _dot_version
    1252      if _dot_version is None:
    1253          try:
    1254              out, err = run_subprocess([DOT_COMMAND, '-V'])
    1255              version_info = err or out
    1256              m = _DOT_VERSION_RE.match(version_info)
    1257              if m:
    1258                  _dot_version = [int(x) for x in m.group(1).split('.')]
    1259              else:
    1260                  _dot_version = (0,)
    1261          except OSError, e:
    1262              _dot_version = (0,)
    1263          log.info('Detected dot version %s' % _dot_version)
    1264      return _dot_version
    1265
    1266  ######################################################################
    1267  #{ Helper Functions
    1268  ######################################################################
    1269
    1270 -def add_valdoc_nodes(graph, val_docs, linker, context):
    1271      """
    1272      :todo: Use different node styles for different subclasses of APIDoc
    1273      """
    1274      nodes = {}
    1275      val_docs = sorted(val_docs, key=lambda d:d.canonical_name)
    1276      for i, val_doc in enumerate(val_docs):
    1277          label = val_doc.canonical_name.contextualize(context.canonical_name)
    1278          node = nodes[val_doc] = DotGraphNode(label)
    1279          graph.nodes.append(node)
    1280          specialize_valdoc_node(node, val_doc, context, linker.url_for(val_doc))
    1281      return nodes
    1282
    1283  NOOP_URL = 'javascript:void(0);'
    1284  MODULE_NODE_HTML = '''
    1285    <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"
    1286           CELLPADDING="0" PORT="table" ALIGN="LEFT">
    1287    <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" FIXEDSIZE="true"
    1288            BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR>
    1289    <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1"
    1290            PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR>
    1291    </TABLE>'''.strip()
    1292
    1293 -def specialize_valdoc_node(node, val_doc, context, url):
    1294      """
    1295      Update the style attributes of `node` to reflext its type
    1296      and context.
    1297      """
    1298      # We can only use html-style nodes if dot_version>2.
    1299      dot_version = get_dot_version()
    1300
    1301      # If val_doc or context is a variable, get its value.
    1302      if isinstance(val_doc, VariableDoc) and val_doc.value is not UNKNOWN:
    1303          val_doc = val_doc.value
    1304      if isinstance(context, VariableDoc) and context.value is not UNKNOWN:
    1305          context = context.value
    1306
    1307      # Set the URL.  (Do this even if it points to the page we're
    1308      # currently on; otherwise, the tooltip is ignored.)
    1309      node['href'] = url or NOOP_URL
    1310
    1311      if isinstance(val_doc, ModuleDoc) and dot_version >= [2]:
    1312          node['shape'] = 'plaintext'
    1313          if val_doc == context: color = SELECTED_BG
    1314          else: color = MODULE_BG
    1315          node['tooltip'] = node['label']
    1316          node['html_label'] = MODULE_NODE_HTML % (color, color, url,
    1317                                                   val_doc.canonical_name,
    1318                                                   node['label'])
    1319          node['width'] = node['height'] = 0
    1320          node.port = 'body'
    1321
    1322      elif isinstance(val_doc, RoutineDoc):
    1323          node['shape'] = 'box'
    1324          node['style'] = 'rounded'
    1325          node['width'] = 0
    1326          node['height'] = 0
    1327          node['label'] = '%s()' % node['label']
    1328          node['tooltip'] = node['label']
    1329          if val_doc == context:
    1330              node['fillcolor'] = SELECTED_BG
    1331              node['style'] = 'filled,rounded,bold'
    1332
    1333      else:
    1334          node['shape'] = 'box'
    1335          node['width'] = 0
    1336          node['height'] = 0
    1337          node['tooltip'] = node['label']
    1338          if val_doc == context:
    1339              node['fillcolor'] = SELECTED_BG
    1340              node['style'] = 'filled,bold'
    1341
    1342 -def name_list(api_docs, context=None):
    1343      if context is not None:
    1344          context = context.canonical_name
    1345      names = [str(d.canonical_name.contextualize(context)) for d in api_docs]
    1346      if len(names) == 0: return ''
    1347      if len(names) == 1: return '%s' % names[0]
    1348      elif len(names) == 2: return '%s and %s' % (names[0], names[1])
    1349      else:
    1350          return '%s, and %s' % (', '.join(names[:-1]), names[-1])
    1351