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 <[email protected]> 
   5  # URL: <http://epydoc.sf.net> 
   6  # 
   7  # $Id: dotgraph.py 1510 2007-02-16 01:28:57Z 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 name, var in class_doc.variables.iteritems(): 471 if ((not show_private and var.is_public == False) or 472 (not show_magic and re.match('__\w+__$', name)) or 473 (not show_inherited and var.container != class_doc)): 474 pass 475 elif isinstance(var.value, RoutineDoc): 476 self.operations.append(var) 477 else: 478 self.attributes.append(var) 479 480 # Initialize our dot node settings. 481 tooltip = self._summary(class_doc) 482 if tooltip: 483 # dot chokes on a \n in the attribute... 484 tooltip = " ".join(tooltip.split()) 485 else: 486 tooltip = class_doc.canonical_name 487 DotGraphNode.__init__(self, tooltip=tooltip, 488 width=0, height=0, shape='plaintext', 489 href=linker.url_for(class_doc) or NOOP_URL)
490 491 #///////////////////////////////////////////////////////////////// 492 #{ Attribute Linking 493 #///////////////////////////////////////////////////////////////// 494 495 SIMPLE_TYPE_RE = re.compile( 496 r'^([\w\.]+)$') 497 """A regular expression that matches descriptions of simple types.""" 498 499 COLLECTION_TYPE_RE = re.compile( 500 r'^(list|set|sequence|tuple|collection) of ([\w\.]+)$') 501 """A regular expression that matches descriptions of collection types.""" 502 503 MAPPING_TYPE_RE = re.compile( 504 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ([\w\.]+)$') 505 """A regular expression that matches descriptions of mapping types.""" 506 507 MAPPING_TO_COLLECTION_TYPE_RE = re.compile( 508 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ' 509 r'(list|set|sequence|tuple|collection) of ([\w\.]+)$') 510 """A regular expression that matches descriptions of mapping types 511 whose value type is a collection.""" 512 513 OPTIONAL_TYPE_RE = re.compile( 514 r'^(None or|optional) ([\w\.]+)$|^([\w\.]+) or None$') 515 """A regular expression that matches descriptions of optional types.""" 516 552 600
601 - def _add_attribute_edge(self, var, nodes, type_str, **attribs):
602 """ 603 Helper for `link_attributes()`: try to add an edge for the 604 given attribute variable `var`. Return ``True`` if 605 successful. 606 """ 607 # Use the type string to look up a corresponding ValueDoc. 608 type_doc = self.linker.docindex.find(type_str, var) 609 if not type_doc: return False 610 611 # Make sure the type is a class. 612 if not isinstance(type_doc, ClassDoc): return False 613 614 # Get the type ValueDoc's node. If it doesn't have one (and 615 # add_nodes_for_linked_attributes=True), then create it. 616 type_node = nodes.get(type_doc) 617 if not type_node: 618 if self.options.get('add_nodes_for_linked_attributes', True): 619 type_node = DotGraphUmlClassNode(type_doc, self.linker, 620 self.context, collapsed=True) 621 nodes[type_doc] = type_node 622 else: 623 return False 624 625 # Add an edge from self to the target type node. 626 # [xx] should I set constraint=false here? 627 attribs.setdefault('headport', 'body') 628 attribs.setdefault('tailport', 'body') 629 url = self.linker.url_for(var) or NOOP_URL 630 self.edges.append(DotGraphEdge(self, type_node, label=var.name, 631 arrowhead='open', href=url, 632 tooltip=var.canonical_name, labeldistance=1.5, 633 **attribs)) 634 return True
635 636 #///////////////////////////////////////////////////////////////// 637 #{ Helper Methods 638 #/////////////////////////////////////////////////////////////////
639 - def _summary(self, api_doc):
640 """Return a plaintext summary for `api_doc`""" 641 if not isinstance(api_doc, APIDoc): return '' 642 if api_doc.summary in (None, UNKNOWN): return '' 643 summary = api_doc.summary.to_plaintext(None).strip() 644 return plaintext_to_html(summary)
645 646 _summary = classmethod(_summary) 647
648 - def _type_descr(self, api_doc):
649 """Return a plaintext type description for `api_doc`""" 650 if not hasattr(api_doc, 'type_descr'): return '' 651 if api_doc.type_descr in (None, UNKNOWN): return '' 652 type_descr = api_doc.type_descr.to_plaintext(self.linker).strip() 653 return plaintext_to_html(type_descr)
654
655 - def _tooltip(self, var_doc):
656 """Return a tooltip for `var_doc`.""" 657 return (self._summary(var_doc) or 658 self._summary(var_doc.value) or 659 var_doc.canonical_name)
660 661 #///////////////////////////////////////////////////////////////// 662 #{ Rendering 663 #///////////////////////////////////////////////////////////////// 664
665 - def _attribute_cell(self, var_doc):
666 # Construct the label 667 label = var_doc.name 668 type_descr = (self._type_descr(var_doc) or 669 self._type_descr(var_doc.value)) 670 if type_descr: label += ': %s' % type_descr 671 # Get the URL 672 url = self.linker.url_for(var_doc) or NOOP_URL 673 # Construct & return the pseudo-html code 674 return self._ATTRIBUTE_CELL % (url, self._tooltip(var_doc), label)
675
676 - def _operation_cell(self, var_doc):
677 """ 678 :todo: do 'word wrapping' on the signature, by starting a new 679 row in the table, if necessary. How to indent the new 680 line? Maybe use align=right? I don't think dot has a 681 &nbsp;. 682 :todo: Optionally add return type info? 683 """ 684 # Construct the label (aka function signature) 685 func_doc = var_doc.value 686 args = [self._operation_arg(n, d, func_doc) for (n, d) 687 in zip(func_doc.posargs, func_doc.posarg_defaults)] 688 args = [plaintext_to_html(arg) for arg in args] 689 if func_doc.vararg: args.append('*'+func_doc.vararg) 690 if func_doc.kwarg: args.append('**'+func_doc.kwarg) 691 label = '%s(%s)' % (var_doc.name, ', '.join(args)) 692 # Get the URL 693 url = self.linker.url_for(var_doc) or NOOP_URL 694 # Construct & return the pseudo-html code 695 return self._OPERATION_CELL % (url, self._tooltip(var_doc), label)
696
697 - def _operation_arg(self, name, default, func_doc):
698 """ 699 :todo: Handle tuple args better 700 :todo: Optionally add type info? 701 """ 702 if default is None: 703 return '%s' % name 704 else: 705 pyval_repr = default.summary_pyval_repr().to_plaintext(None) 706 return '%s=%s' % (name, pyval_repr)
707
708 - def _qualifier_cell(self, key_label, port):
709 return self._QUALIFIER_CELL % (port, self.bgcolor, key_label)
710 711 #: args: (url, tooltip, label) 712 _ATTRIBUTE_CELL = ''' 713 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR> 714 ''' 715 716 #: args: (url, tooltip, label) 717 _OPERATION_CELL = ''' 718 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR> 719 ''' 720 721 #: args: (port, bgcolor, label) 722 _QUALIFIER_CELL = ''' 723 <TR><TD VALIGN="BOTTOM" PORT="%s" BGCOLOR="%s" BORDER="1">%s</TD></TR> 724 ''' 725 726 _QUALIFIER_DIV = ''' 727 <TR><TD VALIGN="BOTTOM" HEIGHT="10" WIDTH="10" FIXEDSIZE="TRUE"></TD></TR> 728 ''' 729 730 #: Args: (rowspan, bgcolor, classname, attributes, operations, qualifiers) 731 _LABEL = ''' 732 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0"> 733 <TR><TD ROWSPAN="%s"> 734 <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" 735 CELLPADDING="0" PORT="body" BGCOLOR="%s"> 736 <TR><TD>%s</TD></TR> 737 <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> 738 %s</TABLE></TD></TR> 739 <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> 740 %s</TABLE></TD></TR> 741 </TABLE> 742 </TD></TR> 743 %s 744 </TABLE>''' 745 746 _COLLAPSED_LABEL = ''' 747 <TABLE CELLBORDER="0" BGCOLOR="%s" PORT="body"> 748 <TR><TD>%s</TD></TR> 749 </TABLE>''' 750
751 - def _get_html_label(self):
752 # Get the class name & contextualize it. 753 classname = self.class_doc.canonical_name 754 classname = classname.contextualize(self.context.canonical_name) 755 756 # If we're collapsed, display the node as a single box. 757 if self.collapsed: 758 return self._COLLAPSED_LABEL % (self.bgcolor, classname) 759 760 # Construct the attribute list. (If it's too long, truncate) 761 attrib_cells = [self._attribute_cell(a) for a in self.attributes] 762 max_attributes = self.options.get('max_attributes', 15) 763 if len(attrib_cells) == 0: 764 attrib_cells = ['<TR><TD></TD></TR>'] 765 elif len(attrib_cells) > max_attributes: 766 attrib_cells[max_attributes-2:-1] = ['<TR><TD>...</TD></TR>'] 767 attributes = ''.join(attrib_cells) 768 769 # Construct the operation list. (If it's too long, truncate) 770 oper_cells = [self._operation_cell(a) for a in self.operations] 771 max_operations = self.options.get('max_operations', 15) 772 if len(oper_cells) == 0: 773 oper_cells = ['<TR><TD></TD></TR>'] 774 elif len(oper_cells) > max_operations: 775 oper_cells[max_operations-2:-1] = ['<TR><TD>...</TD></TR>'] 776 operations = ''.join(oper_cells) 777 778 # Construct the qualifier list & determine the rowspan. 779 if self.qualifiers: 780 rowspan = len(self.qualifiers)*2+2 781 div = self._QUALIFIER_DIV 782 qualifiers = div+div.join([self._qualifier_cell(l,p) for 783 (l,p) in self.qualifiers])+div 784 else: 785 rowspan = 1 786 qualifiers = '' 787 788 # Put it all together. 789 return self._LABEL % (rowspan, self.bgcolor, classname, 790 attributes, operations, qualifiers)
791
792 - def to_dotfile(self):
793 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()] 794 attribs.append('label=<%s>' % self._get_html_label()) 795 s = 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs))) 796 if not self.collapsed: 797 for edge in self.edges: 798 s += '\n' + edge.to_dotfile() 799 return s
800
801 -class DotGraphUmlModuleNode(DotGraphNode):
802 """ 803 A specialized dot grah node used to display `ModuleDoc`\s using 804 UML notation. Simple module nodes look like:: 805 806 .----. 807 +------------+ 808 | modulename | 809 +------------+ 810 811 Packages nodes are drawn with their modules & subpackages nested 812 inside:: 813 814 .----. 815 +----------------------------------------+ 816 | packagename | 817 | | 818 | .----. .----. .----. | 819 | +---------+ +---------+ +---------+ | 820 | | module1 | | module2 | | module3 | | 821 | +---------+ +---------+ +---------+ | 822 | | 823 +----------------------------------------+ 824 825 """
826 - def __init__(self, module_doc, linker, context, collapsed=False, 827 excluded_submodules=(), **options):
828 self.module_doc = module_doc 829 self.linker = linker 830 self.context = context 831 self.collapsed = collapsed 832 self.options = options 833 self.excluded_submodules = excluded_submodules 834 DotGraphNode.__init__(self, shape='plaintext', 835 href=linker.url_for(module_doc) or NOOP_URL, 836 tooltip=module_doc.canonical_name)
837 838 #: Expects: (color, color, url, tooltip, body) 839 _MODULE_LABEL = ''' 840 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" ALIGN="LEFT"> 841 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" 842 FIXEDSIZE="true" BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR> 843 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" WIDTH="20" 844 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR> 845 </TABLE>''' 846 847 #: Expects: (name, body_rows) 848 _NESTED_BODY = ''' 849 <TABLE BORDER="0" CELLBORDER="0" CELLPADDING="0" CELLSPACING="0"> 850 <TR><TD ALIGN="LEFT">%s</TD></TR> 851 %s 852 </TABLE>''' 853 854 #: Expects: (cells,) 855 _NESTED_BODY_ROW = ''' 856 <TR><TD> 857 <TABLE BORDER="0" CELLBORDER="0"><TR>%s</TR></TABLE> 858 </TD></TR>''' 859
860 - def _get_html_label(self, package):
861 """ 862 :Return: (label, depth, width) where: 863 864 - ``label`` is the HTML label 865 - ``depth`` is the depth of the package tree (for coloring) 866 - ``width`` is the max width of the HTML label, roughly in 867 units of characters. 868 """ 869 MAX_ROW_WIDTH = 80 # unit is roughly characters. 870 pkg_name = package.canonical_name 871 pkg_url = self.linker.url_for(package) or NOOP_URL 872 873 if (not package.is_package or len(package.submodules) == 0 or 874 self.collapsed): 875 pkg_color = self._color(package, 1) 876 label = self._MODULE_LABEL % (pkg_color, pkg_color, 877 pkg_url, pkg_name, pkg_name[-1]) 878 return (label, 1, len(pkg_name[-1])+3) 879 880 # Get the label for each submodule, and divide them into rows. 881 row_list = [''] 882 row_width = 0 883 max_depth = 0 884 max_row_width = len(pkg_name[-1])+3 885 for submodule in package.submodules: 886 if submodule in self.excluded_submodules: continue 887 # Get the submodule's label. 888 label, depth, width = self._get_html_label(submodule) 889 # Check if we should start a new row. 890 if row_width > 0 and width+row_width > MAX_ROW_WIDTH: 891 row_list.append('') 892 row_width = 0 893 # Add the submodule's label to the row. 894 row_width += width 895 row_list[-1] += '<TD ALIGN="LEFT">%s</TD>' % label 896 # Update our max's. 897 max_depth = max(depth, max_depth) 898 max_row_width = max(row_width, max_row_width) 899 900 # Figure out which color to use. 901 pkg_color = self._color(package, depth+1) 902 903 # Assemble & return the label. 904 rows = ''.join([self._NESTED_BODY_ROW % r for r in row_list]) 905 body = self._NESTED_BODY % (pkg_name, rows) 906 label = self._MODULE_LABEL % (pkg_color, pkg_color, 907 pkg_url, pkg_name, body) 908 return label, max_depth+1, max_row_width
909 910 _COLOR_DIFF = 24
911 - def _color(self, package, depth):
912 if package == self.context: return SELECTED_BG 913 else: 914 # Parse the base color. 915 if re.match(MODULE_BG, 'r#[0-9a-fA-F]{6}$'): 916 base = int(MODULE_BG[1:], 16) 917 else: 918 base = int('d8e8ff', 16) 919 red = (base & 0xff0000) >> 16 920 green = (base & 0x00ff00) >> 8 921 blue = (base & 0x0000ff) 922 # Make it darker with each level of depth. (but not *too* 923 # dark -- package name needs to be readable) 924 red = max(64, red-(depth-1)*self._COLOR_DIFF) 925 green = max(64, green-(depth-1)*self._COLOR_DIFF) 926 blue = max(64, blue-(depth-1)*self._COLOR_DIFF) 927 # Convert it back to a color string 928 return '#%06x' % ((red<<16)+(green<<8)+blue)
929
930 - def to_dotfile(self):
931 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()] 932 label, depth, width = self._get_html_label(self.module_doc) 933 attribs.append('label=<%s>' % label) 934 return 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
935 936 937 938 ###################################################################### 939 #{ Graph Generation Functions 940 ###################################################################### 941
942 -def package_tree_graph(packages, linker, context=None, **options):
943 """ 944 Return a `DotGraph` that graphically displays the package 945 hierarchies for the given packages. 946 """ 947 if options.get('style', 'uml') == 'uml': # default to uml style? 948 if get_dot_version() >= [2]: 949 return uml_package_tree_graph(packages, linker, context, 950 **options) 951 elif 'style' in options: 952 log.warning('UML style package trees require dot version 2.0+') 953 954 graph = DotGraph('Package Tree for %s' % name_list(packages, context), 955 body='ranksep=.3\n;nodesep=.1\n', 956 edge_defaults={'dir':'none'}) 957 958 # Options 959 if options.get('dir', 'TB') != 'TB': # default: top-to-bottom 960 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB') 961 962 # Get a list of all modules in the package. 963 queue = list(packages) 964 modules = set(packages) 965 for module in queue: 966 queue.extend(module.submodules) 967 modules.update(module.submodules) 968 969 # Add a node for each module. 970 nodes = add_valdoc_nodes(graph, modules, linker, context) 971 972 # Add an edge for each package/submodule relationship. 973 for module in modules: 974 for submodule in module.submodules: 975 graph.edges.append(DotGraphEdge(nodes[module], nodes[submodule], 976 headport='tab')) 977 978 return graph
979
980 -def uml_package_tree_graph(packages, linker, context=None, **options):
981 """ 982 Return a `DotGraph` that graphically displays the package 983 hierarchies for the given packages as a nested set of UML 984 symbols. 985 """ 986 graph = DotGraph('Package Tree for %s' % name_list(packages, context)) 987 # Remove any packages whose containers are also in the list. 988 root_packages = [] 989 for package1 in packages: 990 for package2 in packages: 991 if (package1 is not package2 and 992 package2.canonical_name.dominates(package1.canonical_name)): 993 break 994 else: 995 root_packages.append(package1) 996 # If the context is a variable, then get its value. 997 if isinstance(context, VariableDoc) and context.value is not UNKNOWN: 998 context = context.value 999 # Return a graph with one node for each root package. 1000 for package in root_packages: 1001 graph.nodes.append(DotGraphUmlModuleNode(package, linker, context)) 1002 return graph
1003 1004 ######################################################################
1005 -def class_tree_graph(bases, linker, context=None, **options):
1006 """ 1007 Return a `DotGraph` that graphically displays the package 1008 hierarchies for the given packages. 1009 """ 1010 graph = DotGraph('Class Hierarchy for %s' % name_list(bases, context), 1011 body='ranksep=0.3\n', 1012 edge_defaults={'sametail':True, 'dir':'none'}) 1013 1014 # Options 1015 if options.get('dir', 'TB') != 'TB': # default: top-down 1016 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB') 1017 1018 # Find all superclasses & subclasses of the given classes. 1019 classes = set(bases) 1020 queue = list(bases) 1021 for cls in queue: 1022 if isinstance(cls, ClassDoc): 1023 if cls.subclasses not in (None, UNKNOWN): 1024 queue.extend(cls.subclasses) 1025 classes.update(cls.subclasses) 1026 queue = list(bases) 1027 for cls in queue: 1028 if isinstance(cls, ClassDoc): 1029 if cls.bases not in (None, UNKNOWN): 1030 queue.extend(cls.bases) 1031 classes.update(cls.bases) 1032 1033 # Add a node for each cls. 1034 classes = [d for d in classes if isinstance(d, ClassDoc) 1035 if d.pyval is not object] 1036 nodes = add_valdoc_nodes(graph, classes, linker, context) 1037 1038 # Add an edge for each package/subclass relationship. 1039 edges = set() 1040 for cls in classes: 1041 for subcls in cls.subclasses: 1042 if cls in nodes and subcls in nodes: 1043 edges.add((nodes[cls], nodes[subcls])) 1044 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges] 1045 1046 return graph
1047 1048 ######################################################################
1049 -def uml_class_tree_graph(class_doc, linker, context=None, **options):
1050 """ 1051 Return a `DotGraph` that graphically displays the class hierarchy 1052 for the given class, using UML notation. Options: 1053 1054 - max_attributes 1055 - max_operations 1056 - show_private_vars 1057 - show_magic_vars 1058 - link_attributes 1059 """ 1060 nodes = {} # ClassDoc -> DotGraphUmlClassNode 1061 1062 # Create nodes for class_doc and all its bases. 1063 for cls in class_doc.mro(): 1064 if cls.pyval is object: continue # don't include `object`. 1065 if cls == class_doc: color = SELECTED_BG 1066 else: color = BASECLASS_BG 1067 nodes[cls] = DotGraphUmlClassNode(cls, linker, context, 1068 show_inherited_vars=False, 1069 collapsed=False, bgcolor=color) 1070 1071 # Create nodes for all class_doc's subclasses. 1072 queue = [class_doc] 1073 for cls in queue: 1074 if (isinstance(cls, ClassDoc) and 1075 cls.subclasses not in (None, UNKNOWN)): 1076 queue.extend(cls.subclasses) 1077 for cls in cls.subclasses: 1078 if cls not in nodes: 1079 nodes[cls] = DotGraphUmlClassNode(cls, linker, context, 1080 collapsed=True, 1081 bgcolor=SUBCLASS_BG) 1082 1083 # Only show variables in the class where they're defined for 1084 # *class_doc*. 1085 mro = class_doc.mro() 1086 for name, var in class_doc.variables.items(): 1087 i = mro.index(var.container) 1088 for base in mro[i+1:]: 1089 if base.pyval is object: continue # don't include `object`. 1090 overridden_var = base.variables.get(name) 1091 if overridden_var and overridden_var.container == base: 1092 try: 1093 if isinstance(overridden_var.value, RoutineDoc): 1094 nodes[base].operations.remove(overridden_var) 1095 else: 1096 nodes[base].attributes.remove(overridden_var) 1097 except ValueError: 1098 pass # var is filtered (eg private or magic) 1099 1100 # Keep track of which nodes are part of the inheritance graph 1101 # (since link_attributes might add new nodes) 1102 inheritance_nodes = set(nodes.values()) 1103 1104 # Turn attributes into links. 1105 if options.get('link_attributes', True): 1106 for node in nodes.values(): 1107 node.link_attributes(nodes) 1108 # Make sure that none of the new attribute edges break the 1109 # rank ordering assigned by inheritance. 1110 for edge in node.edges: 1111 if edge.end in inheritance_nodes: 1112 edge['constraint'] = 'False' 1113 1114 # Construct the graph. 1115 graph = DotGraph('UML class diagram for %s' % class_doc.canonical_name, 1116 body='ranksep=.2\n;nodesep=.3\n') 1117 graph.nodes = nodes.values() 1118 1119 # Add inheritance edges. 1120 for node in inheritance_nodes: 1121 for base in node.class_doc.bases: 1122 if base in nodes: 1123 graph.edges.append(DotGraphEdge(nodes[base], node, 1124 dir='back', arrowtail='empty', 1125 headport='body', tailport='body', 1126 color=INH_LINK_COLOR, weight=100, 1127 style='bold')) 1128 1129 # And we're done! 1130 return graph
1131 1132 ######################################################################
1133 -def import_graph(modules, docindex, linker, context=None, **options):
1134 graph = DotGraph('Import Graph', body='ranksep=.3\n;nodesep=.3\n') 1135 1136 # Options 1137 if options.get('dir', 'RL') != 'TB': # default: right-to-left. 1138 graph.body += 'rankdir=%s\n' % options.get('dir', 'RL') 1139 1140 # Add a node for each module. 1141 nodes = add_valdoc_nodes(graph, modules, linker, context) 1142 1143 # Edges. 1144 edges = set() 1145 for dst in modules: 1146 if dst.imports in (None, UNKNOWN): continue 1147 for var_name in dst.imports: 1148 for i in range(len(var_name), 0, -1): 1149 val_doc = docindex.get_valdoc(var_name[:i]) 1150 if isinstance(val_doc, ModuleDoc): 1151 if val_doc in nodes and dst in nodes: 1152 edges.add((nodes[val_doc], nodes[dst])) 1153 break 1154 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges] 1155 1156 return graph
1157 1158 ######################################################################
1159 -def call_graph(api_docs, docindex, linker, context=None, **options):
1160 """ 1161 :param options: 1162 - ``dir``: rankdir for the graph. (default=LR) 1163 - ``add_callers``: also include callers for any of the 1164 routines in ``api_docs``. (default=False) 1165 - ``add_callees``: also include callees for any of the 1166 routines in ``api_docs``. (default=False) 1167 :todo: Add an ``exclude`` option? 1168 """ 1169 if docindex.callers is None: 1170 log.warning("No profiling information for call graph!") 1171 return DotGraph('Call Graph') # return None instead? 1172 1173 if isinstance(context, VariableDoc): 1174 context = context.value 1175 1176 # Get the set of requested functions. 1177 functions = [] 1178 for api_doc in api_docs: 1179 # If it's a variable, get its value. 1180 if isinstance(api_doc, VariableDoc): 1181 api_doc = api_doc.value 1182 # Add the value to the functions list. 1183 if isinstance(api_doc, RoutineDoc): 1184 functions.append(api_doc) 1185 elif isinstance(api_doc, NamespaceDoc): 1186 for vardoc in api_doc.variables.values(): 1187 if isinstance(vardoc.value, RoutineDoc): 1188 functions.append(vardoc.value) 1189 1190 # Filter out functions with no callers/callees? 1191 # [xx] this isnt' quite right, esp if add_callers or add_callees 1192 # options are fales. 1193 functions = [f for f in functions if 1194 (f in docindex.callers) or (f in docindex.callees)] 1195 1196 # Add any callers/callees of the selected functions 1197 func_set = set(functions) 1198 if options.get('add_callers', False) or options.get('add_callees', False): 1199 for func_doc in functions: 1200 if options.get('add_callers', False): 1201 func_set.update(docindex.callers.get(func_doc, ())) 1202 if options.get('add_callees', False): 1203 func_set.update(docindex.callees.get(func_doc, ())) 1204 1205 graph = DotGraph('Call Graph for %s' % name_list(api_docs, context), 1206 node_defaults={'shape':'box', 'width': 0, 'height': 0}) 1207 1208 # Options 1209 if options.get('dir', 'LR') != 'TB': # default: left-to-right 1210 graph.body += 'rankdir=%s\n' % options.get('dir', 'LR') 1211 1212 nodes = add_valdoc_nodes(graph, func_set, linker, context) 1213 1214 # Find the edges. 1215 edges = set() 1216 for func_doc in functions: 1217 for caller in docindex.callers.get(func_doc, ()): 1218 if caller in nodes: 1219 edges.add( (nodes[caller], nodes[func_doc]) ) 1220 for callee in docindex.callees.get(func_doc, ()): 1221 if callee in nodes: 1222 edges.add( (nodes[func_doc], nodes[callee]) ) 1223 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges] 1224 1225 return graph
1226 1227 ###################################################################### 1228 #{ Dot Version 1229 ###################################################################### 1230 1231 _dot_version = None 1232 _DOT_VERSION_RE = re.compile(r'dot version ([\d\.]+)')
1233 -def get_dot_version():
1234 global _dot_version 1235 if _dot_version is None: 1236 try: 1237 out, err = run_subprocess([DOT_COMMAND, '-V']) 1238 version_info = err or out 1239 m = _DOT_VERSION_RE.match(version_info) 1240 if m: 1241 _dot_version = [int(x) for x in m.group(1).split('.')] 1242 else: 1243 _dot_version = (0,) 1244 except OSError, e: 1245 _dot_version = (0,) 1246 log.info('Detected dot version %s' % _dot_version) 1247 return _dot_version
1248 1249 ###################################################################### 1250 #{ Helper Functions 1251 ###################################################################### 1252
1253 -def add_valdoc_nodes(graph, val_docs, linker, context):
1254 """ 1255 :todo: Use different node styles for different subclasses of APIDoc 1256 """ 1257 nodes = {} 1258 val_docs = sorted(val_docs, key=lambda d:d.canonical_name) 1259 for i, val_doc in enumerate(val_docs): 1260 label = val_doc.canonical_name.contextualize(context.canonical_name) 1261 node = nodes[val_doc] = DotGraphNode(label) 1262 graph.nodes.append(node) 1263 specialize_valdoc_node(node, val_doc, context, linker.url_for(val_doc)) 1264 return nodes
1265 1266 NOOP_URL = 'javascript: void(0);' 1267 MODULE_NODE_HTML = ''' 1268 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" 1269 CELLPADDING="0" PORT="table" ALIGN="LEFT"> 1270 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" FIXEDSIZE="true" 1271 BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR> 1272 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" 1273 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR> 1274 </TABLE>'''.strip() 1275
1276 -def specialize_valdoc_node(node, val_doc, context, url):
1277 """ 1278 Update the style attributes of `node` to reflext its type 1279 and context. 1280 """ 1281 # We can only use html-style nodes if dot_version>2. 1282 dot_version = get_dot_version() 1283 1284 # If val_doc or context is a variable, get its value. 1285 if isinstance(val_doc, VariableDoc) and val_doc.value is not UNKNOWN: 1286 val_doc = val_doc.value 1287 if isinstance(context, VariableDoc) and context.value is not UNKNOWN: 1288 context = context.value 1289 1290 # Set the URL. (Do this even if it points to the page we're 1291 # currently on; otherwise, the tooltip is ignored.) 1292 node['href'] = url or NOOP_URL 1293 1294 if isinstance(val_doc, ModuleDoc) and dot_version >= [2]: 1295 node['shape'] = 'plaintext' 1296 if val_doc == context: color = SELECTED_BG 1297 else: color = MODULE_BG 1298 node['tooltip'] = node['label'] 1299 node['html_label'] = MODULE_NODE_HTML % (color, color, url, 1300 val_doc.canonical_name, 1301 node['label']) 1302 node['width'] = node['height'] = 0 1303 node.port = 'body' 1304 1305 elif isinstance(val_doc, RoutineDoc): 1306 node['shape'] = 'box' 1307 node['style'] = 'rounded' 1308 node['width'] = 0 1309 node['height'] = 0 1310 node['label'] = '%s()' % node['label'] 1311 node['tooltip'] = node['label'] 1312 if val_doc == context: 1313 node['fillcolor'] = SELECTED_BG 1314 node['style'] = 'filled,rounded,bold' 1315 1316 else: 1317 node['shape'] = 'box' 1318 node['width'] = 0 1319 node['height'] = 0 1320 node['tooltip'] = node['label'] 1321 if val_doc == context: 1322 node['fillcolor'] = SELECTED_BG 1323 node['style'] = 'filled,bold'
1324
1325 -def name_list(api_docs, context=None):
1326 if context is not None: 1327 context = context.canonical_name 1328 names = [str(d.canonical_name.contextualize(context)) for d in api_docs] 1329 if len(names) == 0: return '' 1330 if len(names) == 1: return '%s' % names[0] 1331 elif len(names) == 2: return '%s and %s' % (names[0], names[1]) 1332 else: 1333 return '%s, and %s' % (', '.join(names[:-1]), names[-1])
1334