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 1210 2006-04-10 13:25:50Z edloper $ 
   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 title = plaintext_to_html(self.title or '') 151 caption = plaintext_to_html(self.caption or '') 152 if title or caption: 153 css_class = 'graph-with-title' 154 else: 155 css_class = 'graph-without-title' 156 if len(title)+len(caption) > 80: 157 title_align = 'left' 158 table_width = ' width="600"' 159 else: 160 title_align = 'center' 161 table_width = '' 162 163 if center: s = '<center>' 164 if title or caption: 165 s += ('<p><table border="0" cellpadding="0" cellspacing="0" ' 166 'class="graph"%s>\n <tr><td align="center">\n' % 167 table_width) 168 s += (' %s\n <img src="%s" alt=%r usemap="#%s" ' 169 'ismap="ismap" class="%s">\n' % 170 (cmapx.strip(), image_url, title, self.uid, css_class)) 171 if title or caption: 172 s += ' </td></tr>\n <tr><td align=%r>\n' % title_align 173 if title: 174 s += '<span class="graph-title">%s</span>' % title 175 if title and caption: 176 s += ' -- ' 177 if caption: 178 s += '<span class="graph-caption">%s</span>' % caption 179 s += '\n </th></tr>\n</table></p>' 180 if center: s += '</center>' 181 return s
182 205 214
215 - def write(self, filename, language='gif'):
216 """ 217 Render the graph using the output format `language`, and write 218 the result to `filename`. 219 220 :return: True if rendering was successful. 221 """ 222 result = self._run_dot('-T%s' % language, 223 '-o%s' % filename) 224 # Decode into unicode, if necessary. 225 if language == 'cmapx' and result is not None: 226 result = result.decode('utf-8') 227 return (result is not None)
228
229 - def render(self, language='gif'):
230 """ 231 Use the ``dot`` command to render this graph, using the output 232 format `language`. Return the result as a string, or `None` 233 if the rendering failed. 234 """ 235 return self._run_dot('-T%s' % language)
236
237 - def _run_dot(self, *options):
238 try: 239 result, err = run_subprocess((DOT_COMMAND,)+options, 240 self.to_dotfile()) 241 if err: log.warning("Graphviz dot warning(s):\n%s" % err) 242 except OSError, e: 243 log.warning("Unable to render Graphviz dot graph:\n%s" % e) 244 #log.debug(self.to_dotfile()) 245 return None 246 247 return result
248
249 - def to_dotfile(self):
250 """ 251 Return the string contents of the dot file that should be used 252 to render this graph. 253 """ 254 lines = ['digraph %s {' % self.uid, 255 'node [%s]' % ','.join(['%s="%s"' % (k,v) for (k,v) 256 in self.node_defaults.items()]), 257 'edge [%s]' % ','.join(['%s="%s"' % (k,v) for (k,v) 258 in self.edge_defaults.items()])] 259 if self.body: 260 lines.append(self.body) 261 lines.append('/* Nodes */') 262 for node in self.nodes: 263 lines.append(node.to_dotfile()) 264 lines.append('/* Edges */') 265 for edge in self.edges: 266 lines.append(edge.to_dotfile()) 267 lines.append('}') 268 269 # Default dot input encoding is UTF-8 270 return u'\n'.join(lines).encode('utf-8')
271
272 -class DotGraphNode:
273 _next_id = 0
274 - def __init__(self, label=None, html_label=None, **attribs):
275 if label is not None and html_label is not None: 276 raise ValueError('Use label or html_label, not both.') 277 if label is not None: attribs['label'] = label 278 self._html_label = html_label 279 self._attribs = attribs 280 self.id = self.__class__._next_id 281 self.__class__._next_id += 1 282 self.port = None
283
284 - def __getitem__(self, attr):
285 return self._attribs[attr]
286
287 - def __setitem__(self, attr, val):
288 if attr == 'html_label': 289 self._attribs.pop('label') 290 self._html_label = val 291 else: 292 if attr == 'label': self._html_label = None 293 self._attribs[attr] = val
294
295 - def to_dotfile(self):
296 """ 297 Return the dot commands that should be used to render this node. 298 """ 299 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items() 300 if v is not None] 301 if self._html_label: 302 attribs.insert(0, 'label=<%s>' % (self._html_label,)) 303 if attribs: attribs = ' [%s]' % (','.join(attribs)) 304 return 'node%d%s' % (self.id, attribs)
305
306 -class DotGraphEdge:
307 - def __init__(self, start, end, label=None, **attribs):
308 """ 309 :type start: `DotGraphNode` 310 :type end: `DotGraphNode` 311 """ 312 assert isinstance(start, DotGraphNode) 313 assert isinstance(end, DotGraphNode) 314 if label is not None: attribs['label'] = label 315 self.start = start #: :type: `DotGraphNode` 316 self.end = end #: :type: `DotGraphNode` 317 self._attribs = attribs
318
319 - def __getitem__(self, attr):
320 return self._attribs[attr]
321
322 - def __setitem__(self, attr, val):
323 self._attribs[attr] = val
324
325 - def to_dotfile(self):
326 """ 327 Return the dot commands that should be used to render this edge. 328 """ 329 # Set head & tail ports, if the nodes have preferred ports. 330 attribs = self._attribs.copy() 331 if (self.start.port is not None and 'headport' not in attribs): 332 attribs['headport'] = self.start.port 333 if (self.end.port is not None and 'tailport' not in attribs): 334 attribs['tailport'] = self.end.port 335 # Convert attribs to a string 336 attribs = ','.join(['%s="%s"' % (k,v) for (k,v) in attribs.items() 337 if v is not None]) 338 if attribs: attribs = ' [%s]' % attribs 339 # Return the dotfile edge. 340 return 'node%d -> node%d%s' % (self.start.id, self.end.id, attribs)
341 342 ###################################################################### 343 #{ Specialized Nodes for UML Graphs 344 ###################################################################### 345
346 -class DotGraphUmlClassNode(DotGraphNode):
347 """ 348 A specialized dot graph node used to display `ClassDoc`\s using 349 UML notation. The node is rendered as a table with three cells: 350 the top cell contains the class name; the middle cell contains a 351 list of attributes; and the bottom cell contains a list of 352 operations:: 353 354 +-------------+ 355 | ClassName | 356 +-------------+ 357 | x: int | 358 | ... | 359 +-------------+ 360 | f(self, x) | 361 | ... | 362 +-------------+ 363 364 `DotGraphUmlClassNode`\s may be *collapsed*, in which case they are 365 drawn as a simple box containing the class name:: 366 367 +-------------+ 368 | ClassName | 369 +-------------+ 370 371 Attributes with types corresponding to documented classes can 372 optionally be converted into edges, using `link_attributes()`. 373 374 :todo: Add more options? 375 - show/hide operation signature 376 - show/hide operation signature types 377 - show/hide operation signature return type 378 - show/hide attribute types 379 - use qualifiers 380 """ 381
382 - def __init__(self, class_doc, linker, context, collapsed=False, 383 bgcolor=CLASS_BG, **options):
384 """ 385 Create a new `DotGraphUmlClassNode` based on the class 386 `class_doc`. 387 388 :Parameters: 389 `linker` : `DocstringLinker<markup.DocstringLinker>` 390 Used to look up URLs for classes. 391 `context` : `APIDoc` 392 The context in which this node will be drawn; dotted 393 names will be contextualized to this context. 394 `collapsed` : ``bool`` 395 If true, then display this node as a simple box. 396 `bgcolor` : ``str`` 397 The background color for this node. 398 `options` : ``dict`` 399 A set of options used to control how the node should 400 be displayed. 401 402 :Keywords: 403 - `show_private_vars`: If false, then private variables 404 are filtered out of the attributes & operations lists. 405 (Default: *False*) 406 - `show_magic_vars`: If false, then magic variables 407 (such as ``__init__`` and ``__add__``) are filtered out of 408 the attributes & operations lists. (Default: *True*) 409 - `show_inherited_vars`: If false, then inherited variables 410 are filtered out of the attributes & operations lists. 411 (Default: *False*) 412 - `max_attributes`: The maximum number of attributes that 413 should be listed in the attribute box. If the class has 414 more than this number of attributes, some will be 415 ellided. Ellipsis is marked with ``'...'``. 416 - `max_operations`: The maximum number of operations that 417 should be listed in the operation box. 418 - `add_nodes_for_linked_attributes`: If true, then 419 `link_attributes()` will create new a collapsed node for 420 the types of a linked attributes if no node yet exists for 421 that type. 422 """ 423 self.class_doc = class_doc 424 """The class represented by this node.""" 425 426 self.linker = linker 427 """Used to look up URLs for classes.""" 428 429 self.context = context 430 """The context in which the node will be drawn.""" 431 432 self.bgcolor = bgcolor 433 """The background color of the node.""" 434 435 self.options = options 436 """Options used to control how the node is displayed.""" 437 438 self.collapsed = collapsed 439 """If true, then draw this node as a simple box.""" 440 441 self.attributes = [] 442 """The list of VariableDocs for attributes""" 443 444 self.operations = [] 445 """The list of VariableDocs for operations""" 446 447 self.qualifiers = [] 448 """List of (key_label, port) tuples.""" 449 450 self.edges = [] 451 """List of edges used to represent this node's attributes. 452 These should not be added to the `DotGraph`; this node will 453 generate their dotfile code directly.""" 454 455 # Initialize operations & attributes lists. 456 show_private = options.get('show_private_vars', False) 457 show_magic = options.get('show_magic_vars', True) 458 show_inherited = options.get('show_inherited_vars', False) 459 for name, var in class_doc.variables.iteritems(): 460 if ((not show_private and var.is_public == False) or 461 (not show_magic and re.match('__\w+__$', name)) or 462 (not show_inherited and var.container != class_doc)): 463 pass 464 elif isinstance(var.value, RoutineDoc): 465 self.operations.append(var) 466 else: 467 self.attributes.append(var) 468 469 # Initialize our dot node settings. 470 DotGraphNode.__init__(self, tooltip=class_doc.canonical_name, 471 width=0, height=0, shape='plaintext', 472 href=linker.url_for(class_doc) or NOOP_URL)
473 474 #///////////////////////////////////////////////////////////////// 475 #{ Attribute Linking 476 #///////////////////////////////////////////////////////////////// 477 478 SIMPLE_TYPE_RE = re.compile( 479 r'^([\w\.]+)$') 480 """A regular expression that matches descriptions of simple types.""" 481 482 COLLECTION_TYPE_RE = re.compile( 483 r'^(list|set|sequence|tuple|collection) of ([\w\.]+)$') 484 """A regular expression that matches descriptions of collection types.""" 485 486 MAPPING_TYPE_RE = re.compile( 487 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ([\w\.]+)$') 488 """A regular expression that matches descriptions of mapping types.""" 489 490 MAPPING_TO_COLLECTION_TYPE_RE = re.compile( 491 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ' 492 r'(list|set|sequence|tuple|collection) of ([\w\.]+)$') 493 """A regular expression that matches descriptions of mapping types 494 whose value type is a collection.""" 495 496 OPTIONAL_TYPE_RE = re.compile( 497 r'^(None or|optional) ([\w\.]+)$|^([\w\.]+) or None$') 498 """A regular expression that matches descriptions of optional types.""" 499 535 583
584 - def _add_attribute_edge(self, var, nodes, type_str, **attribs):
585 """ 586 Helper for `link_attribute()`: try to add an edge for the 587 given attribute variable `var`. Return ``True`` if 588 successful. 589 """ 590 # Use the type string to look up a corresponding ValueDoc. 591 type_doc = self.linker.docindex.find(type_str, var) 592 if not type_doc: return False 593 594 # Get the type ValueDoc's node. If it doesn't have one (and 595 # add_nodes_for_linked_attributes=True), then create it. 596 type_node = nodes.get(type_doc) 597 if not type_node: 598 if self.options.get('add_nodes_for_linked_attributes', True): 599 type_node = DotGraphUmlClassNode(type_doc, self.linker, 600 self.context, collapsed=True) 601 nodes[type_doc] = type_node 602 else: 603 return False 604 605 # Add an edge from self to the target type node. 606 # [xx] should I set constraint=false here? 607 attribs.setdefault('headport', 'body') 608 attribs.setdefault('tailport', 'body') 609 url = self.linker.url_for(var) or NOOP_URL 610 self.edges.append(DotGraphEdge(self, type_node, label=var.name, 611 arrowhead='open', href=url, 612 tooltip=var.canonical_name, labeldistance=1.5, 613 **attribs)) 614 return True
615 616 #///////////////////////////////////////////////////////////////// 617 #{ Helper Methods 618 #/////////////////////////////////////////////////////////////////
619 - def _summary(self, api_doc):
620 """Return a plaintext summary for `api_doc`""" 621 if not isinstance(api_doc, APIDoc): return '' 622 if api_doc.summary in (None, UNKNOWN): return '' 623 summary = api_doc.summary.to_plaintext(self.linker).strip() 624 return plaintext_to_html(summary)
625
626 - def _type_descr(self, api_doc):
627 """Return a plaintext type description for `api_doc`""" 628 if not hasattr(api_doc, 'type_descr'): return '' 629 if api_doc.type_descr in (None, UNKNOWN): return '' 630 type_descr = api_doc.type_descr.to_plaintext(self.linker).strip() 631 return plaintext_to_html(type_descr)
632
633 - def _tooltip(self, var_doc):
634 """Return a tooltip for `var_doc`.""" 635 return (self._summary(var_doc) or 636 self._summary(var_doc.value) or 637 var_doc.canonical_name)
638 639 #///////////////////////////////////////////////////////////////// 640 #{ Rendering 641 #///////////////////////////////////////////////////////////////// 642
643 - def _attribute_cell(self, var_doc):
644 # Construct the label 645 label = var_doc.name 646 type_descr = (self._type_descr(var_doc) or 647 self._type_descr(var_doc.value)) 648 if type_descr: label += ': %s' % type_descr 649 # Get the URL 650 url = self.linker.url_for(var_doc) or NOOP_URL 651 # Construct & return the pseudo-html code 652 return self._ATTRIBUTE_CELL % (url, self._tooltip(var_doc), label)
653
654 - def _operation_cell(self, var_doc):
655 """ 656 :todo: do 'word wrapping' on the signature, by starting a new 657 row in the table, if necessary. How to indent the new 658 line? Maybe use align=right? I don't think dot has a 659 &nbsp;. 660 :todo: Optionally add return type info? 661 """ 662 # Construct the label (aka function signature) 663 func_doc = var_doc.value 664 args = [self._operation_arg(n, d, func_doc) for (n, d) 665 in zip(func_doc.posargs, func_doc.posarg_defaults)] 666 args = [plaintext_to_html(arg) for arg in args] 667 if func_doc.vararg: args.append('*'+func_doc.vararg) 668 if func_doc.kwarg: args.append('**'+func_doc.kwarg) 669 label = '%s(%s)' % (var_doc.name, ', '.join(args)) 670 # Get the URL 671 url = self.linker.url_for(var_doc) or NOOP_URL 672 # Construct & return the pseudo-html code 673 return self._OPERATION_CELL % (url, self._tooltip(var_doc), label)
674
675 - def _operation_arg(self, name, default, func_doc):
676 """ 677 :todo: Handle tuple args better 678 :todo: Optionally add type info? 679 """ 680 if default is None: 681 return '%s' % name 682 elif default.parse_repr is not UNKNOWN: 683 return '%s=%s' % (name, default.parse_repr) 684 else: 685 pyval_repr = default.pyval_repr() 686 if pyval_repr is not UNKNOWN: 687 return '%s=%s' % (name, pyval_repr) 688 else: 689 return '%s=??' % name
690
691 - def _qualifier_cell(self, key_label, port):
692 return self._QUALIFIER_CELL % (port, self.bgcolor, key_label)
693 694 #: args: (url, tooltip, label) 695 _ATTRIBUTE_CELL = ''' 696 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR> 697 ''' 698 699 #: args: (url, tooltip, label) 700 _OPERATION_CELL = ''' 701 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR> 702 ''' 703 704 #: args: (port, bgcolor, label) 705 _QUALIFIER_CELL = ''' 706 <TR><TD VALIGN="BOTTOM" PORT="%s" BGCOLOR="%s" BORDER="1">%s</TD></TR> 707 ''' 708 709 _QUALIFIER_DIV = ''' 710 <TR><TD VALIGN="BOTTOM" HEIGHT="10" WIDTH="10" FIXEDSIZE="TRUE"></TD></TR> 711 ''' 712 713 #: Args: (rowspan, bgcolor, classname, attributes, operations, qualifiers) 714 _LABEL = ''' 715 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0"> 716 <TR><TD ROWSPAN="%s"> 717 <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" 718 CELLPADDING="0" PORT="body" BGCOLOR="%s"> 719 <TR><TD>%s</TD></TR> 720 <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> 721 %s</TABLE></TD></TR> 722 <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> 723 %s</TABLE></TD></TR> 724 </TABLE> 725 </TD></TR> 726 %s 727 </TABLE>''' 728 729 _COLLAPSED_LABEL = ''' 730 <TABLE CELLBORDER="0" BGCOLOR="%s" PORT="body"> 731 <TR><TD>%s</TD></TR> 732 </TABLE>''' 733
734 - def _get_html_label(self):
735 # Get the class name & contextualize it. 736 classname = self.class_doc.canonical_name 737 classname = classname.contextualize(self.context.canonical_name) 738 739 # If we're collapsed, display the node as a single box. 740 if self.collapsed: 741 return self._COLLAPSED_LABEL % (self.bgcolor, classname) 742 743 # Construct the attribute list. (If it's too long, truncate) 744 attrib_cells = [self._attribute_cell(a) for a in self.attributes] 745 max_attributes = self.options.get('max_attributes', 15) 746 if len(attrib_cells) == 0: 747 attrib_cells = ['<TR><TD></TD></TR>'] 748 elif len(attrib_cells) > max_attributes: 749 attrib_cells[max_attributes-2:-1] = ['<TR><TD>...</TD></TR>'] 750 attributes = ''.join(attrib_cells) 751 752 # Construct the operation list. (If it's too long, truncate) 753 oper_cells = [self._operation_cell(a) for a in self.operations] 754 max_operations = self.options.get('max_operations', 15) 755 if len(oper_cells) == 0: 756 oper_cells = ['<TR><TD></TD></TR>'] 757 elif len(oper_cells) > max_operations: 758 oper_cells[max_operations-2:-1] = ['<TR><TD>...</TD></TR>'] 759 operations = ''.join(oper_cells) 760 761 # Construct the qualifier list & determine the rowspan. 762 if self.qualifiers: 763 rowspan = len(self.qualifiers)*2+2 764 div = self._QUALIFIER_DIV 765 qualifiers = div+div.join([self._qualifier_cell(l,p) for 766 (l,p) in self.qualifiers])+div 767 else: 768 rowspan = 1 769 qualifiers = '' 770 771 # Put it all together. 772 return self._LABEL % (rowspan, self.bgcolor, classname, 773 attributes, operations, qualifiers)
774
775 - def to_dotfile(self):
776 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()] 777 attribs.append('label=<%s>' % self._get_html_label()) 778 s = 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs))) 779 if not self.collapsed: 780 for edge in self.edges: 781 s += '\n' + edge.to_dotfile() 782 return s
783
784 -class DotGraphUmlModuleNode(DotGraphNode):
785 """ 786 A specialized dot grah node used to display `ModuleDoc`\s using 787 UML notation. Simple module nodes look like:: 788 789 .----. 790 +------------+ 791 | modulename | 792 +------------+ 793 794 Packages nodes are drawn with their modules & subpackages nested 795 inside:: 796 797 .----. 798 +----------------------------------------+ 799 | packagename | 800 | | 801 | .----. .----. .----. | 802 | +---------+ +---------+ +---------+ | 803 | | module1 | | module2 | | module3 | | 804 | +---------+ +---------+ +---------+ | 805 | | 806 +----------------------------------------+ 807 808 """
809 - def __init__(self, module_doc, linker, context, collapsed=False, 810 excluded_submodules=(), **options):
811 self.module_doc = module_doc 812 self.linker = linker 813 self.context = context 814 self.collapsed = collapsed 815 self.options = options 816 self.excluded_submodules = excluded_submodules 817 DotGraphNode.__init__(self, shape='plaintext', 818 href=linker.url_for(module_doc) or NOOP_URL, 819 tooltip=module_doc.canonical_name)
820 821 #: Expects: (color, color, url, tooltip, body) 822 _MODULE_LABEL = ''' 823 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" ALIGN="LEFT"> 824 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" 825 FIXEDSIZE="true" BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR> 826 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" WIDTH="20" 827 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR> 828 </TABLE>''' 829 830 #: Expects: (name, body_rows) 831 _NESTED_BODY = ''' 832 <TABLE BORDER="0" CELLBORDER="0" CELLPADDING="0" CELLSPACING="0"> 833 <TR><TD ALIGN="LEFT">%s</TD></TR> 834 %s 835 </TABLE>''' 836 837 #: Expects: (cells,) 838 _NESTED_BODY_ROW = ''' 839 <TR><TD> 840 <TABLE BORDER="0" CELLBORDER="0"><TR>%s</TR></TABLE> 841 </TD></TR>''' 842
843 - def _get_html_label(self, package):
844 """ 845 :Return: (label, depth, width) where: 846 847 - `label` is the HTML label 848 - `depth` is the depth of the package tree (for coloring) 849 - `width` is the max width of the HTML label, roughly in 850 units of characters. 851 """ 852 MAX_ROW_WIDTH = 80 # unit is roughly characters. 853 pkg_name = package.canonical_name 854 pkg_url = self.linker.url_for(package) or NOOP_URL 855 856 if (not package.is_package or len(package.submodules) == 0 or 857 self.collapsed): 858 pkg_color = self._color(package, 1) 859 label = self._MODULE_LABEL % (pkg_color, pkg_color, 860 pkg_url, pkg_name, pkg_name[-1]) 861 return (label, 1, len(pkg_name[-1])+3) 862 863 # Get the label for each submodule, and divide them into rows. 864 row_list = [''] 865 row_width = 0 866 max_depth = 0 867 max_row_width = len(pkg_name[-1])+3 868 for submodule in package.submodules: 869 if submodule in self.excluded_submodules: continue 870 # Get the submodule's label. 871 label, depth, width = self._get_html_label(submodule) 872 # Check if we should start a new row. 873 if row_width > 0 and width+row_width > MAX_ROW_WIDTH: 874 row_list.append('') 875 row_width = 0 876 # Add the submodule's label to the row. 877 row_width += width 878 row_list[-1] += '<TD ALIGN="LEFT">%s</TD>' % label 879 # Update our max's. 880 max_depth = max(depth, max_depth) 881 max_row_width = max(row_width, max_row_width) 882 883 # Figure out which color to use. 884 pkg_color = self._color(package, depth+1) 885 886 # Assemble & return the label. 887 rows = ''.join([self._NESTED_BODY_ROW % r for r in row_list]) 888 body = self._NESTED_BODY % (pkg_name, rows) 889 label = self._MODULE_LABEL % (pkg_color, pkg_color, 890 pkg_url, pkg_name, body) 891 return label, max_depth+1, max_row_width
892 893 _COLOR_DIFF = 24
894 - def _color(self, package, depth):
895 if package == self.context: return SELECTED_BG 896 else: 897 # Parse the base color. 898 if re.match(MODULE_BG, 'r#[0-9a-fA-F]{6}$'): 899 base = int(MODULE_BG[1:], 16) 900 else: 901 base = int('d8e8ff', 16) 902 red = (base & 0xff0000) >> 16 903 green = (base & 0x00ff00) >> 8 904 blue = (base & 0x0000ff) 905 # Make it darker with each level of depth. (but not *too* 906 # dark -- package name needs to be readable) 907 red = max(64, red-(depth-1)*self._COLOR_DIFF) 908 green = max(64, green-(depth-1)*self._COLOR_DIFF) 909 blue = max(64, blue-(depth-1)*self._COLOR_DIFF) 910 # Convert it back to a color string 911 return '#%06x' % ((red<<16)+(green<<8)+blue)
912
913 - def to_dotfile(self):
914 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()] 915 label, depth, width = self._get_html_label(self.module_doc) 916 attribs.append('label=<%s>' % label) 917 return 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
918 919 920 921 ###################################################################### 922 #{ Graph Generation Functions 923 ###################################################################### 924
925 -def package_tree_graph(packages, linker, context=None, **options):
926 """ 927 Return a `DotGraph` that graphically displays the package 928 hierarchies for the given packages. 929 """ 930 if options.get('style', 'uml') == 'uml': # default to uml style? 931 if get_dot_version() >= [2]: 932 return uml_package_tree_graph(packages, linker, context, 933 **options) 934 elif 'style' in options: 935 log.warning('UML style package trees require dot version 2.0+') 936 937 graph = DotGraph('Package Tree for %s' % name_list(packages, context), 938 body='ranksep=.3\n;nodesep=.1\n', 939 edge_defaults={'dir':'none'}) 940 941 # Options 942 if options.get('dir', 'TB') != 'TB': # default: top-to-bottom 943 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB') 944 945 # Get a list of all modules in the package. 946 queue = list(packages) 947 modules = set(packages) 948 for module in queue: 949 queue.extend(module.submodules) 950 modules.update(module.submodules) 951 952 # Add a node for each module. 953 nodes = add_valdoc_nodes(graph, modules, linker, context) 954 955 # Add an edge for each package/submodule relationship. 956 for module in modules: 957 for submodule in module.submodules: 958 graph.edges.append(DotGraphEdge(nodes[module], nodes[submodule], 959 headport='tab')) 960 961 return graph
962
963 -def uml_package_tree_graph(packages, linker, context=None, **options):
964 """ 965 Return a `DotGraph` that graphically displays the package 966 hierarchies for the given packages as a nested set of UML 967 symbols. 968 """ 969 graph = DotGraph('Package Tree for %s' % name_list(packages, context)) 970 # Remove any packages whose containers are also in the list. 971 root_packages = [] 972 for package1 in packages: 973 for package2 in packages: 974 if (package1 is not package2 and 975 package2.canonical_name.dominates(package1.canonical_name)): 976 break 977 else: 978 root_packages.append(package1) 979 # If the context is a variable, then get its value. 980 if isinstance(context, VariableDoc) and context.value is not UNKNOWN: 981 context = context.value 982 # Return a graph with one node for each root package. 983 for package in root_packages: 984 graph.nodes.append(DotGraphUmlModuleNode(package, linker, context)) 985 return graph
986 987 ######################################################################
988 -def class_tree_graph(bases, linker, context=None, **options):
989 """ 990 Return a `DotGraph` that graphically displays the package 991 hierarchies for the given packages. 992 """ 993 graph = DotGraph('Class Hierarchy for %s' % name_list(bases, context), 994 body='ranksep=0.3\n', 995 edge_defaults={'sametail':True, 'dir':'none'}) 996 997 # Options 998 if options.get('dir', 'TB') != 'TB': # default: top-down 999 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB') 1000 1001 # Find all superclasses & subclasses of the given classes. 1002 classes = set(bases) 1003 queue = list(bases) 1004 for cls in queue: 1005 if cls.subclasses not in (None, UNKNOWN): 1006 queue.extend(cls.subclasses) 1007 classes.update(cls.subclasses) 1008 queue = list(bases) 1009 for cls in queue: 1010 if cls.bases not in (None, UNKNOWN): 1011 queue.extend(cls.bases) 1012 classes.update(cls.bases) 1013 1014 # Add a node for each cls. 1015 classes = [d for d in classes if isinstance(d, ClassDoc) 1016 if d.pyval is not object] 1017 nodes = add_valdoc_nodes(graph, classes, linker, context) 1018 1019 # Add an edge for each package/subclass relationship. 1020 edges = set() 1021 for cls in classes: 1022 for subcls in cls.subclasses: 1023 if cls in nodes and subcls in nodes: 1024 edges.add((nodes[cls], nodes[subcls])) 1025 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges] 1026 1027 return graph
1028 1029 ######################################################################
1030 -def uml_class_tree_graph(class_doc, linker, context=None, **options):
1031 """ 1032 Return a `DotGraph` that graphically displays the class hierarchy 1033 for the given class, using UML notation. Options: 1034 1035 - max_attributes 1036 - max_operations 1037 - show_private_vars 1038 - show_magic_vars 1039 - link_attributes 1040 """ 1041 nodes = {} # ClassDoc -> DotGraphUmlClassNode 1042 1043 # Create nodes for class_doc and all its bases. 1044 for cls in class_doc.mro(): 1045 if cls.pyval is object: continue # don't include `object`. 1046 if cls == class_doc: color = SELECTED_BG 1047 else: color = BASECLASS_BG 1048 nodes[cls] = DotGraphUmlClassNode(cls, linker, context, 1049 show_inherited_vars=False, 1050 collapsed=False, bgcolor=color) 1051 1052 # Create nodes for all class_doc's subclasses. 1053 queue = [class_doc] 1054 for cls in queue: 1055 if cls.subclasses not in (None, UNKNOWN): 1056 queue.extend(cls.subclasses) 1057 for cls in cls.subclasses: 1058 if cls not in nodes: 1059 nodes[cls] = DotGraphUmlClassNode(cls, linker, context, 1060 collapsed=True, 1061 bgcolor=SUBCLASS_BG) 1062 1063 # Only show variables in the class where they're defined for 1064 # *class_doc*. 1065 mro = class_doc.mro() 1066 for name, var in class_doc.variables.items(): 1067 i = mro.index(var.container) 1068 for base in mro[i+1:]: 1069 if base.pyval is object: continue # don't include `object`. 1070 overridden_var = base.variables.get(name) 1071 if overridden_var and overridden_var.container == base: 1072 try: 1073 if isinstance(overridden_var.value, RoutineDoc): 1074 nodes[base].operations.remove(overridden_var) 1075 else: 1076 nodes[base].attributes.remove(overridden_var) 1077 except ValueError: 1078 pass # var is filtered (eg private or magic) 1079 1080 # Keep track of which nodes are part of the inheritance graph 1081 # (since link_attributes might add new nodes) 1082 inheritance_nodes = set(nodes.values()) 1083 1084 # Turn attributes into links. 1085 if options.get('link_attributes', True): 1086 for node in nodes.values(): 1087 node.link_attributes(nodes) 1088 # Make sure that none of the new attribute edges break the 1089 # rank ordering assigned by inheritance. 1090 for edge in node.edges: 1091 if edge.end in inheritance_nodes: 1092 edge['constraint'] = 'False' 1093 1094 # Construct the graph. 1095 graph = DotGraph('UML class diagram for %s' % class_doc, 1096 body='ranksep=.2\n;nodesep=.3\n') 1097 graph.nodes = nodes.values() 1098 1099 # Add inheritance edges. 1100 for node in inheritance_nodes: 1101 for base in node.class_doc.bases: 1102 if base in nodes: 1103 graph.edges.append(DotGraphEdge(nodes[base], node, 1104 dir='back', arrowtail='empty', 1105 headport='body', tailport='body', 1106 color=INH_LINK_COLOR, weight=100, 1107 style='bold')) 1108 1109 # And we're done! 1110 return graph
1111 1112 ######################################################################
1113 -def import_graph(modules, docindex, linker, context=None, **options):
1114 graph = DotGraph('Import Graph', body='ranksep=.3\n;nodesep=.3\n') 1115 1116 # Options 1117 if options.get('dir', 'RL') != 'TB': # default: right-to-left. 1118 graph.body += 'rankdir=%s\n' % options.get('dir', 'RL') 1119 1120 # Add a node for each module. 1121 nodes = add_valdoc_nodes(graph, modules, linker, context) 1122 1123 # Edges. 1124 edges = set() 1125 for dst in modules: 1126 if dst.imports in (None, UNKNOWN): continue 1127 for var_name in dst.imports: 1128 for i in range(len(var_name), 0, -1): 1129 val_doc = docindex.get_valdoc(var_name[:i]) 1130 if isinstance(val_doc, ModuleDoc): 1131 if val_doc in nodes and dst in nodes: 1132 edges.add((nodes[val_doc], nodes[dst])) 1133 break 1134 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges] 1135 1136 return graph
1137 1138 ######################################################################
1139 -def call_graph(api_docs, docindex, linker, context=None, **options):
1140 """ 1141 :param options: 1142 - `dir`: rankdir for the graph. (default=LR) 1143 - `add_callers`: also include callers for any of the 1144 routines in `api_docs`. (default=False) 1145 - `add_callees`: also include callees for any of the 1146 routines in `api_docs`. (default=False) 1147 :todo: Add an `exclude` option? 1148 """ 1149 if docindex.callers is None: 1150 log.warning("No profiling information for call graph!") 1151 return DotGraph('Call Graph') # return None instead? 1152 1153 if isinstance(context, VariableDoc): 1154 context = context.value 1155 1156 # Get the set of requested functions. 1157 functions = [] 1158 for api_doc in api_docs: 1159 # If it's a variable, get its value. 1160 if isinstance(api_doc, VariableDoc): 1161 api_doc = api_doc.value 1162 # Add the value to the functions list. 1163 if isinstance(api_doc, RoutineDoc): 1164 functions.append(api_doc) 1165 elif isinstance(api_doc, NamespaceDoc): 1166 for vardoc in api_doc.variables.values(): 1167 if isinstance(vardoc.value, RoutineDoc): 1168 functions.append(vardoc.value) 1169 1170 # Filter out functions with no callers/callees? 1171 # [xx] this isnt' quite right, esp if add_callers or add_callees 1172 # options are fales. 1173 functions = [f for f in functions if 1174 (f in docindex.callers) or (f in docindex.callees)] 1175 1176 # Add any callers/callees of the selected functions 1177 func_set = set(functions) 1178 if options.get('add_callers', False) or options.get('add_callees', False): 1179 for func_doc in functions: 1180 if options.get('add_callers', False): 1181 func_set.update(docindex.callers.get(func_doc, ())) 1182 if options.get('add_callees', False): 1183 func_set.update(docindex.callees.get(func_doc, ())) 1184 1185 graph = DotGraph('Call Graph for %s' % name_list(api_docs, context), 1186 node_defaults={'shape':'box', 'width': 0, 'height': 0}) 1187 1188 # Options 1189 if options.get('dir', 'LR') != 'TB': # default: left-to-right 1190 graph.body += 'rankdir=%s\n' % options.get('dir', 'LR') 1191 1192 nodes = add_valdoc_nodes(graph, func_set, linker, context) 1193 1194 # Find the edges. 1195 edges = set() 1196 for func_doc in functions: 1197 for caller in docindex.callers.get(func_doc, ()): 1198 if caller in nodes: 1199 edges.add( (nodes[caller], nodes[func_doc]) ) 1200 for callee in docindex.callees.get(func_doc, ()): 1201 if callee in nodes: 1202 edges.add( (nodes[func_doc], nodes[callee]) ) 1203 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges] 1204 1205 return graph
1206 1207 ###################################################################### 1208 #{ Dot Version 1209 ###################################################################### 1210 1211 _dot_version = None 1212 _DOT_VERSION_RE = re.compile(r'dot version ([\d\.]+)')
1213 -def get_dot_version():
1214 global _dot_version 1215 if _dot_version is None: 1216 try: 1217 out, err = run_subprocess([DOT_COMMAND, '-V']) 1218 version_info = err or out 1219 m = _DOT_VERSION_RE.match(version_info) 1220 if m: 1221 _dot_version = [int(x) for x in m.group(1).split('.')] 1222 else: 1223 _dot_version = (0,) 1224 except RunSubprocessError, e: 1225 _dot_version = (0,) 1226 log.info('Detected dot version %s' % _dot_version) 1227 return _dot_version
1228 1229 ###################################################################### 1230 #{ Helper Functions 1231 ###################################################################### 1232
1233 -def add_valdoc_nodes(graph, val_docs, linker, context):
1234 """ 1235 @todo: Use different node styles for different subclasses of APIDoc 1236 """ 1237 nodes = {} 1238 val_docs = sorted(val_docs, key=lambda d:d.canonical_name) 1239 for i, val_doc in enumerate(val_docs): 1240 label = val_doc.canonical_name.contextualize(context.canonical_name) 1241 node = nodes[val_doc] = DotGraphNode(label) 1242 graph.nodes.append(node) 1243 specialize_valdoc_node(node, val_doc, context, linker.url_for(val_doc)) 1244 return nodes
1245 1246 NOOP_URL = 'javascript: void(0);' 1247 MODULE_NODE_HTML = ''' 1248 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" 1249 CELLPADDING="0" PORT="table" ALIGN="LEFT"> 1250 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" FIXEDSIZE="true" 1251 BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR> 1252 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" 1253 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR> 1254 </TABLE>'''.strip() 1255
1256 -def specialize_valdoc_node(node, val_doc, context, url):
1257 """ 1258 Update the style attributes of `node` to reflext its type 1259 and context. 1260 """ 1261 # We can only use html-style nodes if dot_version>2. 1262 dot_version = get_dot_version() 1263 1264 # If val_doc or context is a variable, get its value. 1265 if isinstance(val_doc, VariableDoc) and val_doc.value is not UNKNOWN: 1266 val_doc = val_doc.value 1267 if isinstance(context, VariableDoc) and context.value is not UNKNOWN: 1268 context = context.value 1269 1270 # Set the URL. (Do this even if it points to the page we're 1271 # currently on; otherwise, the tooltip is ignored.) 1272 node['href'] = url or NOOP_URL 1273 1274 if isinstance(val_doc, ModuleDoc) and dot_version >= [2]: 1275 node['shape'] = 'plaintext' 1276 if val_doc == context: color = SELECTED_BG 1277 else: color = MODULE_BG 1278 node['tooltip'] = node['label'] 1279 node['html_label'] = MODULE_NODE_HTML % (color, color, url, 1280 val_doc.canonical_name, 1281 node['label']) 1282 node['width'] = node['height'] = 0 1283 node.port = 'body' 1284 1285 elif isinstance(val_doc, RoutineDoc): 1286 node['shape'] = 'box' 1287 node['style'] = 'rounded' 1288 node['width'] = 0 1289 node['height'] = 0 1290 node['label'] = '%s()' % node['label'] 1291 node['tooltip'] = node['label'] 1292 if val_doc == context: 1293 node['fillcolor'] = SELECTED_BG 1294 node['style'] = 'filled,rounded,bold' 1295 1296 else: 1297 node['shape'] = 'box' 1298 node['width'] = 0 1299 node['height'] = 0 1300 node['tooltip'] = node['label'] 1301 if val_doc == context: 1302 node['fillcolor'] = SELECTED_BG 1303 node['style'] = 'filled,bold'
1304
1305 -def name_list(api_docs, context=None):
1306 if context is not None: 1307 context = context.canonical_name 1308 names = [str(d.canonical_name.contextualize(context)) for d in api_docs] 1309 if len(names) == 0: return '' 1310 if len(names) == 1: return '%s' % names[0] 1311 elif len(names) == 2: return '%s and %s' % (names[0], names[1]) 1312 else: 1313 return '%s, and %s' % (', '.join(names[:-1]), names[-1])
1314