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 211 self.body = re.sub("href\s*=\s*['\"]?<([\w\.]+)>['\"]?\s*(,?)", 212 subfunc, self.body)
213 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 553 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