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 1805 2008-02-27 20:24:06Z 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  import tempfile 
  26  from epydoc import log 
  27  from epydoc.apidoc import * 
  28  from epydoc.util import * 
  29  from epydoc.compat import * # Backwards compatibility 
  30   
  31  #: Should the dot2tex module be used to render dot graphs to latex 
  32  #: (if it's available)?  This is experimental, and not yet working, 
  33  #: so it should be left False for now. 
  34  USE_DOT2TEX = False 
  35   
  36  #: colors for graphs of APIDocs 
  37  COLOR = dict( 
  38      MODULE_BG = '#d8e8ff', 
  39      CLASS_BG = '#d8ffe8', 
  40      SELECTED_BG = '#ffd0d0', 
  41      BASECLASS_BG = '#e0b0a0', 
  42      SUBCLASS_BG = '#e0b0a0', 
  43      UNDOCUMENTED_BG = '#c0c0c0', 
  44      ROUTINE_BG = '#e8d0b0', # not used 
  45      INH_LINK = '#800000', 
  46      ) 
  47   
  48  ###################################################################### 
  49  #{ Dot Graphs 
  50  ###################################################################### 
  51   
  52  DOT_COMMAND = 'dot' 
  53  """The command that should be used to spawn dot""" 
  54   
55 -class DotGraph(object):
56 """ 57 A ``dot`` directed graph. The contents of the graph are 58 constructed from the following instance variables: 59 60 - `nodes`: A list of `DotGraphNode`\\s, encoding the nodes 61 that are present in the graph. Each node is characterized 62 a set of attributes, including an optional label. 63 - `edges`: A list of `DotGraphEdge`\\s, encoding the edges 64 that are present in the graph. Each edge is characterized 65 by a set of attributes, including an optional label. 66 - `node_defaults`: Default attributes for nodes. 67 - `edge_defaults`: Default attributes for edges. 68 - `body`: A string that is appended as-is in the body of 69 the graph. This can be used to build more complex dot 70 graphs. 71 72 The `link()` method can be used to resolve crossreference links 73 within the graph. In particular, if the 'href' attribute of any 74 node or edge is assigned a value of the form ``<name>``, then it 75 will be replaced by the URL of the object with that name. This 76 applies to the `body` as well as the `nodes` and `edges`. 77 78 To render the graph, use the methods `write()` and `render()`. 79 Usually, you should call `link()` before you render the graph. 80 """ 81 _uids = set() 82 """A set of all uids that that have been generated, used to ensure 83 that each new graph has a unique uid.""" 84 85 DEFAULT_NODE_DEFAULTS={'fontsize':10, 'fontname': 'Helvetica'} 86 DEFAULT_EDGE_DEFAULTS={'fontsize':10, 'fontname': 'Helvetica'} 87 88 DEFAULT_LATEX_SIZE="6.25,8" 89 """The default minimum size in inches (width,height) for graphs 90 when rendering with `to_latex()`""" 91 92 DEFAULT_HTML_SIZE="10,20" 93 """The default minimum size in inches (width,height) for graphs 94 when rendering with `to_html()`""" 95 96 DEFAULT_HTML_IMAGE_FORMAT = 'gif' 97 """The default format used to generate images by `to_html()`""" 98
99 - def __init__(self, title, body='', node_defaults=None, 100 edge_defaults=None, caption=None):
101 """ 102 Create a new `DotGraph`. 103 """ 104 self.title = title 105 """The title of the graph.""" 106 107 self.caption = caption 108 """A caption for the graph.""" 109 110 self.nodes = [] 111 """A list of the nodes that are present in the graph. 112 113 :type: ``list`` of `DotGraphNode`""" 114 115 self.edges = [] 116 """A list of the edges that are present in the graph. 117 118 :type: ``list`` of `DotGraphEdge`""" 119 120 self.body = body 121 """A string that should be included as-is in the body of the 122 graph. 123 124 :type: ``str``""" 125 126 self.node_defaults = node_defaults or self.DEFAULT_NODE_DEFAULTS 127 """Default attribute values for nodes.""" 128 129 self.edge_defaults = edge_defaults or self.DEFAULT_EDGE_DEFAULTS 130 """Default attribute values for edges.""" 131 132 self.uid = re.sub(r'\W', '_', title).lower() 133 """A unique identifier for this graph. This can be used as a 134 filename when rendering the graph. No two `DotGraph`\s will 135 have the same uid.""" 136 137 # Encode the title, if necessary. 138 if isinstance(self.title, unicode): 139 self.title = self.title.encode('ascii', 'xmlcharrefreplace') 140 141 # Make sure the UID isn't too long. 142 self.uid = self.uid[:30] 143 144 # Make sure the UID is unique 145 if self.uid in self._uids: 146 n = 2 147 while ('%s_%s' % (self.uid, n)) in self._uids: n += 1 148 self.uid = '%s_%s' % (self.uid, n) 149 self._uids.add(self.uid)
150
151 - def to_latex(self, directory, center=True, size=None):
152 """ 153 Return the LaTeX code that should be used to display this 154 graph. Two image files will be written: image_file+'.eps' 155 and image_file+'.pdf'. 156 157 :param size: The maximum size for the generated image, in 158 inches. In particular, if ``size`` is ``\"w,h\"``, then 159 this will add a line ``size=\"w,h\"`` to the dot graph. 160 Defaults to `DEFAULT_LATEX_SIZE`. 161 :type size: ``str`` 162 """ 163 eps_file = os.path.join(directory, self.uid+'.eps') 164 pdf_file = os.path.join(directory, self.uid+'.pdf') 165 size = size or self.DEFAULT_LATEX_SIZE 166 # Use dot2tex if requested (and if it's available). 167 # Otherwise, render it to an image file & use \includgraphics. 168 if USE_DOT2TEX and dot2tex is not None: 169 try: return self._to_dot2tex(center, size) 170 except KeyboardInterrupt: raise 171 except: 172 raise 173 log.warning('dot2tex failed; using dot instead') 174 175 # Render the graph in postscript. 176 ps = self._run_dot('-Tps', size=size) 177 # Write the postscript output. 178 psfile = open(eps_file, 'wb') 179 psfile.write('%!PS-Adobe-2.0 EPSF-1.2\n') 180 psfile.write(ps) 181 psfile.close() 182 # Use ps2pdf to generate the pdf output. 183 try: run_subprocess(('ps2pdf', '-dEPSCrop', eps_file, pdf_file)) 184 except RunSubprocessError, e: 185 log.warning("Unable to render Graphviz dot graph (%s):\n" 186 "ps2pdf failed." % self.title) 187 return None 188 189 # Generate the latex code to display the graph. 190 s = ' \\includegraphics{%s}\n' % self.uid 191 if center: s = '\\begin{center}\n%s\\end{center}\n' % s 192 return s
193
194 - def _to_dot2tex(self, center=True, size=None):
195 # requires: pgf, latex-xcolor. 196 from dot2tex import dot2tex 197 if 0: # DEBUG 198 import logging 199 log = logging.getLogger("dot2tex") 200 log.setLevel(logging.DEBUG) 201 console = logging.StreamHandler() 202 formatter = logging.Formatter('%(levelname)-8s %(message)s') 203 console.setFormatter(formatter) 204 log.addHandler(console) 205 options = dict(crop=True, autosize=True, figonly=True, debug=True) 206 conv = dot2tex.Dot2PGFConv(options) 207 s = conv.convert(self.to_dotfile(size=size)) 208 conv.dopreproc = False 209 s = conv.convert(s) 210 if center: s = '\\begin{center}\n%s\\end{center}\n' % s 211 return s
212
213 - def to_html(self, directory, center=True, size=None):
214 """ 215 Return the HTML code that should be uesd to display this graph 216 (including a client-side image map). 217 218 :param image_url: The URL of the image file for this graph; 219 this should be generated separately with the `write()` method. 220 :param size: The maximum size for the generated image, in 221 inches. In particular, if ``size`` is ``\"w,h\"``, then 222 this will add a line ``size=\"w,h\"`` to the dot graph. 223 Defaults to `DEFAULT_HTML_SIZE`. 224 :type size: ``str`` 225 """ 226 image_url = '%s.%s' % (self.uid, self.DEFAULT_HTML_IMAGE_FORMAT) 227 image_file = os.path.join(directory, image_url) 228 size = size or self.DEFAULT_HTML_SIZE 229 # If dotversion >1.8.10, then we can generate the image and 230 # the cmapx with a single call to dot. Otherwise, we need to 231 # run dot twice. 232 if get_dot_version() > [1,8,10]: 233 cmapx = self._run_dot('-T%s' % self._pick_language(image_file), 234 '-o%s' % image_file, 235 '-Tcmapx', size=size) 236 if cmapx is None: return '' # failed to render 237 else: 238 if not self.write(image_file): 239 return '' # failed to render 240 cmapx = self.render('cmapx') or '' 241 242 # Decode the cmapx (dot uses utf-8) 243 try: 244 cmapx = cmapx.decode('utf-8') 245 except UnicodeDecodeError: 246 log.debug('%s: unable to decode cmapx from dot; graph will ' 247 'not have clickable regions' % image_file) 248 cmapx = '' 249 250 title = plaintext_to_html(self.title or '') 251 caption = plaintext_to_html(self.caption or '') 252 if title or caption: 253 css_class = 'graph-with-title' 254 else: 255 css_class = 'graph-without-title' 256 if len(title)+len(caption) > 80: 257 title_align = 'left' 258 table_width = ' width="600"' 259 else: 260 title_align = 'center' 261 table_width = '' 262 263 if center: s = '<center>' 264 if title or caption: 265 s += ('<table border="0" cellpadding="0" cellspacing="0" ' 266 'class="graph"%s>\n <tr><td align="center">\n' % 267 table_width) 268 s += (' %s\n <img src="%s" alt=%r usemap="#%s" ' 269 'ismap="ismap" class="%s" />\n' % 270 (cmapx.strip(), image_url, title, self.uid, css_class)) 271 if title or caption: 272 s += ' </td></tr>\n <tr><td align=%r>\n' % title_align 273 if title: 274 s += '<span class="graph-title">%s</span>' % title 275 if title and caption: 276 s += ' -- ' 277 if caption: 278 s += '<span class="graph-caption">%s</span>' % caption 279 s += '\n </td></tr>\n</table><br />' 280 if center: s += '</center>' 281 return s
282 304 self.body = re.sub("href\s*=\s*['\"]?<([\w\.]+)>['\"]?\s*(,?)", 305 subfunc, self.body)
306 316
317 - def write(self, filename, language=None, size=None):
318 """ 319 Render the graph using the output format `language`, and write 320 the result to `filename`. 321 322 :return: True if rendering was successful. 323 :param size: The maximum size for the generated image, in 324 inches. In particular, if ``size`` is ``\"w,h\"``, then 325 this will add a line ``size=\"w,h\"`` to the dot graph. 326 If not specified, no size line will be added. 327 :type size: ``str`` 328 """ 329 if language is None: language = self._pick_language(filename) 330 result = self._run_dot('-T%s' % language, 331 '-o%s' % filename, 332 size=size) 333 # Decode into unicode, if necessary. 334 if language == 'cmapx' and result is not None: 335 result = result.decode('utf-8') 336 return (result is not None)
337
338 - def _pick_language(self, filename):
339 ext = os.path.splitext(filename)[1] 340 if ext in ('.gif', '.png', '.jpg', '.jpeg'): 341 return ext[1:] 342 else: 343 return 'gif'
344
345 - def render(self, language=None, size=None):
346 """ 347 Use the ``dot`` command to render this graph, using the output 348 format `language`. Return the result as a string, or ``None`` 349 if the rendering failed. 350 351 :param size: The maximum size for the generated image, in 352 inches. In particular, if ``size`` is ``\"w,h\"``, then 353 this will add a line ``size=\"w,h\"`` to the dot graph. 354 If not specified, no size line will be added. 355 :type size: ``str`` 356 """ 357 return self._run_dot('-T%s' % language, size=size)
358
359 - def _run_dot(self, *options, **kwparam):
360 if get_dot_version() == (0,): return None 361 try: 362 result, err = run_subprocess((DOT_COMMAND,)+options, 363 self.to_dotfile(**kwparam)) 364 if err: log.warning("Graphviz dot warning(s):\n%s" % err) 365 except OSError, e: 366 log.warning("Unable to render Graphviz dot graph (%s):\n%s" % 367 (self.title, e)) 368 import tempfile, epydoc 369 if epydoc.DEBUG: 370 filename = tempfile.mktemp('.dot') 371 out = open(filename, 'wb') 372 out.write(self.to_dotfile(**kwparam)) 373 out.close() 374 log.debug('Failed dot graph written to %s' % filename) 375 return None 376 377 return result
378
379 - def to_dotfile(self, size=None):
380 """ 381 Return the string contents of the dot file that should be used 382 to render this graph. 383 384 :param size: The maximum size for the generated image, in 385 inches. In particular, if ``size`` is ``\"w,h\"``, then 386 this will add a line ``size=\"w,h\"`` to the dot graph. 387 If not specified, no size line will be added. 388 :type size: ``str`` 389 """ 390 lines = ['digraph %s {' % self.uid, 391 'node [%s]' % ','.join(['%s="%s"' % (k,v) for (k,v) 392 in self.node_defaults.items()]), 393 'edge [%s]' % ','.join(['%s="%s"' % (k,v) for (k,v) 394 in self.edge_defaults.items()])] 395 if size: 396 lines.append('size="%s"' % size) 397 if self.body: 398 lines.append(self.body) 399 lines.append('/* Nodes */') 400 for node in self.nodes: 401 lines.append(node.to_dotfile()) 402 lines.append('/* Edges */') 403 for edge in self.edges: 404 lines.append(edge.to_dotfile()) 405 lines.append('}') 406 407 # Default dot input encoding is UTF-8 408 return u'\n'.join(lines).encode('utf-8')
409
410 -class DotGraphNode(object):
411 _next_id = 0
412 - def __init__(self, label=None, html_label=None, **attribs):
413 if label is not None and html_label is not None: 414 raise ValueError('Use label or html_label, not both.') 415 if label is not None: attribs['label'] = label 416 self._html_label = html_label 417 self._attribs = attribs 418 self.id = DotGraphNode._next_id 419 DotGraphNode._next_id += 1 420 self.port = None
421
422 - def __getitem__(self, attr):
423 return self._attribs[attr]
424
425 - def __setitem__(self, attr, val):
426 if attr == 'html_label': 427 self._attribs.pop('label') 428 self._html_label = val 429 else: 430 if attr == 'label': self._html_label = None 431 self._attribs[attr] = val
432
433 - def to_dotfile(self):
434 """ 435 Return the dot commands that should be used to render this node. 436 """ 437 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items() 438 if v is not None] 439 if self._html_label: 440 attribs.insert(0, 'label=<%s>' % (self._html_label,)) 441 if attribs: attribs = ' [%s]' % (','.join(attribs)) 442 return 'node%d%s' % (self.id, attribs)
443
444 -class DotGraphEdge(object):
445 - def __init__(self, start, end, label=None, **attribs):
446 """ 447 :type start: `DotGraphNode` 448 :type end: `DotGraphNode` 449 """ 450 assert isinstance(start, DotGraphNode) 451 assert isinstance(end, DotGraphNode) 452 if label is not None: attribs['label'] = label 453 self.start = start #: :type: `DotGraphNode` 454 self.end = end #: :type: `DotGraphNode` 455 self._attribs = attribs
456
457 - def __getitem__(self, attr):
458 return self._attribs[attr]
459
460 - def __setitem__(self, attr, val):
461 self._attribs[attr] = val
462
463 - def to_dotfile(self):
464 """ 465 Return the dot commands that should be used to render this edge. 466 """ 467 # Set head & tail ports, if the nodes have preferred ports. 468 attribs = self._attribs.copy() 469 if (self.start.port is not None and 'headport' not in attribs): 470 attribs['headport'] = self.start.port 471 if (self.end.port is not None and 'tailport' not in attribs): 472 attribs['tailport'] = self.end.port 473 # Convert attribs to a string 474 attribs = ','.join(['%s="%s"' % (k,v) for (k,v) in attribs.items() 475 if v is not None]) 476 if attribs: attribs = ' [%s]' % attribs 477 # Return the dotfile edge. 478 return 'node%d -> node%d%s' % (self.start.id, self.end.id, attribs)
479 480 ###################################################################### 481 #{ Specialized Nodes for UML Graphs 482 ###################################################################### 483
484 -class DotGraphUmlClassNode(DotGraphNode):
485 """ 486 A specialized dot graph node used to display `ClassDoc`\s using 487 UML notation. The node is rendered as a table with three cells: 488 the top cell contains the class name; the middle cell contains a 489 list of attributes; and the bottom cell contains a list of 490 operations:: 491 492 +-------------+ 493 | ClassName | 494 +-------------+ 495 | x: int | 496 | ... | 497 +-------------+ 498 | f(self, x) | 499 | ... | 500 +-------------+ 501 502 `DotGraphUmlClassNode`\s may be *collapsed*, in which case they are 503 drawn as a simple box containing the class name:: 504 505 +-------------+ 506 | ClassName | 507 +-------------+ 508 509 Attributes with types corresponding to documented classes can 510 optionally be converted into edges, using `link_attributes()`. 511 512 :todo: Add more options? 513 - show/hide operation signature 514 - show/hide operation signature types 515 - show/hide operation signature return type 516 - show/hide attribute types 517 - use qualifiers 518 """
519 - def __init__(self, class_doc, linker, context, collapsed=False, 520 bgcolor=COLOR['CLASS_BG'], **options):
521 """ 522 Create a new `DotGraphUmlClassNode` based on the class 523 `class_doc`. 524 525 :Parameters: 526 `linker` : `markup.DocstringLinker` 527 Used to look up URLs for classes. 528 `context` : `APIDoc` 529 The context in which this node will be drawn; dotted 530 names will be contextualized to this context. 531 `collapsed` : ``bool`` 532 If true, then display this node as a simple box. 533 `bgcolor` : ```str``` 534 The background color for this node. 535 `options` : ``dict`` 536 A set of options used to control how the node should 537 be displayed. 538 539 :Keywords: 540 - `show_private_vars`: If false, then private variables 541 are filtered out of the attributes & operations lists. 542 (Default: *False*) 543 - `show_magic_vars`: If false, then magic variables 544 (such as ``__init__`` and ``__add__``) are filtered out of 545 the attributes & operations lists. (Default: *True*) 546 - `show_inherited_vars`: If false, then inherited variables 547 are filtered out of the attributes & operations lists. 548 (Default: *False*) 549 - `max_attributes`: The maximum number of attributes that 550 should be listed in the attribute box. If the class has 551 more than this number of attributes, some will be 552 ellided. Ellipsis is marked with ``'...'``. (Default: 10) 553 - `max_operations`: The maximum number of operations that 554 should be listed in the operation box. (Default: 5) 555 - `add_nodes_for_linked_attributes`: If true, then 556 `link_attributes()` will create new a collapsed node for 557 the types of a linked attributes if no node yet exists for 558 that type. 559 - `show_signature_defaults`: If true, then show default 560 parameter values in method signatures; if false, then 561 hide them. (Default: *False*) 562 - `max_signature_width`: The maximum width (in chars) for 563 method signatures. If the signature is longer than this, 564 then it will be trunctated (with ``'...'``). (Default: 565 *60*) 566 """ 567 if not isinstance(class_doc, ClassDoc): 568 raise TypeError('Expected a ClassDoc as 1st argument') 569 570 self.class_doc = class_doc 571 """The class represented by this node.""" 572 573 self.linker = linker 574 """Used to look up URLs for classes.""" 575 576 self.context = context 577 """The context in which the node will be drawn.""" 578 579 self.bgcolor = bgcolor 580 """The background color of the node.""" 581 582 self.options = options 583 """Options used to control how the node is displayed.""" 584 585 self.collapsed = collapsed 586 """If true, then draw this node as a simple box.""" 587 588 self.attributes = [] 589 """The list of VariableDocs for attributes""" 590 591 self.operations = [] 592 """The list of VariableDocs for operations""" 593 594 self.qualifiers = [] 595 """List of (key_label, port) tuples.""" 596 597 self.edges = [] 598 """List of edges used to represent this node's attributes. 599 These should not be added to the `DotGraph`; this node will 600 generate their dotfile code directly.""" 601 602 self.same_rank = [] 603 """List of nodes that should have the same rank as this one. 604 (Used for nodes that are created by _link_attributes).""" 605 606 # Keyword options: 607 self._show_signature_defaults = options.get( 608 'show_signature_defaults', False) 609 self._max_signature_width = options.get( 610 'max_signature_width', 60) 611 612 # Initialize operations & attributes lists. 613 show_private = options.get('show_private_vars', False) 614 show_magic = options.get('show_magic_vars', True) 615 show_inherited = options.get('show_inherited_vars', False) 616 if class_doc.sorted_variables not in (None, UNKNOWN): 617 for var in class_doc.sorted_variables: 618 name = var.canonical_name[-1] 619 if ((not show_private and var.is_public == False) or 620 (not show_magic and re.match('__\w+__$', name)) or 621 (not show_inherited and var.container != class_doc)): 622 pass 623 elif isinstance(var.value, RoutineDoc): 624 self.operations.append(var) 625 else: 626 self.attributes.append(var) 627 628 # Initialize our dot node settings. 629 tooltip = self._summary(class_doc) 630 if tooltip: 631 # dot chokes on a \n in the attribute... 632 tooltip = " ".join(tooltip.split()) 633 else: 634 tooltip = class_doc.canonical_name 635 try: url = linker.url_for(class_doc) or NOOP_URL 636 except NotImplementedError: url = NOOP_URL 637 DotGraphNode.__init__(self, tooltip=tooltip, width=0, height=0, 638 shape='plaintext', href=url)
639 640 #///////////////////////////////////////////////////////////////// 641 #{ Attribute Linking 642 #///////////////////////////////////////////////////////////////// 643 644 SIMPLE_TYPE_RE = re.compile( 645 r'^([\w\.]+)$') 646 """A regular expression that matches descriptions of simple types.""" 647 648 COLLECTION_TYPE_RE = re.compile( 649 r'^(list|set|sequence|tuple|collection) of ([\w\.]+)$') 650 """A regular expression that matches descriptions of collection types.""" 651 652 MAPPING_TYPE_RE = re.compile( 653 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ([\w\.]+)$') 654 """A regular expression that matches descriptions of mapping types.""" 655 656 MAPPING_TO_COLLECTION_TYPE_RE = re.compile( 657 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ' 658 r'(list|set|sequence|tuple|collection) of ([\w\.]+)$') 659 """A regular expression that matches descriptions of mapping types 660 whose value type is a collection.""" 661 662 OPTIONAL_TYPE_RE = re.compile( 663 r'^(None or|optional) ([\w\.]+)$|^([\w\.]+) or None$') 664 """A regular expression that matches descriptions of optional types.""" 665 701 751
752 - def _add_attribute_edge(self, var, graph, nodes, type_str, **attribs):
753 """ 754 Helper for `link_attributes()`: try to add an edge for the 755 given attribute variable `var`. Return ``True`` if 756 successful. 757 """ 758 # Use the type string to look up a corresponding ValueDoc. 759 if not hasattr(self.linker, 'docindex'): return False 760 type_doc = self.linker.docindex.find(type_str, var) 761 if not type_doc: return False 762 763 # Make sure the type is a class. 764 if not isinstance(type_doc, ClassDoc): return False 765 766 # Get the type ValueDoc's node. If it doesn't have one (and 767 # add_nodes_for_linked_attributes=True), then create it. 768 type_node = nodes.get(type_doc) 769 if not type_node: 770 if self.options.get('add_nodes_for_linked_attributes', True): 771 type_node = DotGraphUmlClassNode(type_doc, self.linker, 772 self.context, collapsed=True) 773 self.same_rank.append(type_node) 774 nodes[type_doc] = type_node 775 graph.nodes.append(type_node) 776 else: 777 return False 778 779 # Add an edge from self to the target type node. 780 # [xx] should I set constraint=false here? 781 attribs.setdefault('headport', 'body') 782 attribs.setdefault('tailport', 'body') 783 try: url = self.linker.url_for(var) or NOOP_URL 784 except NotImplementedError: url = NOOP_URL 785 self.edges.append(DotGraphEdge(self, type_node, label=var.name, 786 arrowtail='odiamond', arrowhead='none', href=url, 787 tooltip=var.canonical_name, labeldistance=1.5, 788 **attribs)) 789 return True
790 791 #///////////////////////////////////////////////////////////////// 792 #{ Helper Methods 793 #/////////////////////////////////////////////////////////////////
794 - def _summary(self, api_doc):
795 """Return a plaintext summary for `api_doc`""" 796 if not isinstance(api_doc, APIDoc): return '' 797 if api_doc.summary in (None, UNKNOWN): return '' 798 summary = api_doc.summary.to_plaintext(None).strip() 799 return plaintext_to_html(summary)
800 801 _summary = classmethod(_summary) 802
803 - def _type_descr(self, api_doc):
804 """Return a plaintext type description for `api_doc`""" 805 if not hasattr(api_doc, 'type_descr'): return '' 806 if api_doc.type_descr in (None, UNKNOWN): return '' 807 type_descr = api_doc.type_descr.to_plaintext(self.linker).strip() 808 return plaintext_to_html(type_descr)
809
810 - def _tooltip(self, var_doc):
811 """Return a tooltip for `var_doc`.""" 812 return (self._summary(var_doc) or 813 self._summary(var_doc.value) or 814 var_doc.canonical_name)
815 816 #///////////////////////////////////////////////////////////////// 817 #{ Rendering 818 #///////////////////////////////////////////////////////////////// 819
820 - def _attribute_cell(self, var_doc):
821 # Construct the label 822 label = var_doc.name 823 type_descr = (self._type_descr(var_doc) or 824 self._type_descr(var_doc.value)) 825 if type_descr: label += ': %s' % type_descr 826 # Get the URL 827 try: url = self.linker.url_for(var_doc) or NOOP_URL 828 except NotImplementedError: url = NOOP_URL 829 # Construct & return the pseudo-html code 830 return self._ATTRIBUTE_CELL % (url, self._tooltip(var_doc), label)
831
832 - def _operation_cell(self, var_doc):
833 """ 834 :todo: do 'word wrapping' on the signature, by starting a new 835 row in the table, if necessary. How to indent the new 836 line? Maybe use align=right? I don't think dot has a 837 &nbsp;. 838 :todo: Optionally add return type info? 839 """ 840 # Construct the label (aka function signature) 841 func_doc = var_doc.value 842 args = [self._operation_arg(n, d, func_doc) for (n, d) 843 in zip(func_doc.posargs, func_doc.posarg_defaults)] 844 args = [plaintext_to_html(arg) for arg in args] 845 if func_doc.vararg: args.append('*'+func_doc.vararg) 846 if func_doc.kwarg: args.append('**'+func_doc.kwarg) 847 label = '%s(%s)' % (var_doc.name, ', '.join(args)) 848 if len(label) > self._max_signature_width: 849 label = label[:self._max_signature_width-4]+'...)' 850 # Get the URL 851 try: url = self.linker.url_for(var_doc) or NOOP_URL 852 except NotImplementedError: url = NOOP_URL 853 # Construct & return the pseudo-html code 854 return self._OPERATION_CELL % (url, self._tooltip(var_doc), label)
855
856 - def _operation_arg(self, name, default, func_doc):
857 """ 858 :todo: Handle tuple args better 859 :todo: Optionally add type info? 860 """ 861 if default is None or not self._show_signature_defaults: 862 return '%s' % name 863 else: 864 pyval_repr = default.summary_pyval_repr().to_plaintext(None) 865 return '%s=%s' % (name, pyval_repr)
866
867 - def _qualifier_cell(self, key_label, port):
868 return self._QUALIFIER_CELL % (port, self.bgcolor, key_label)
869 870 #: args: (url, tooltip, label) 871 _ATTRIBUTE_CELL = ''' 872 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR> 873 ''' 874 875 #: args: (url, tooltip, label) 876 _OPERATION_CELL = ''' 877 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR> 878 ''' 879 880 #: args: (port, bgcolor, label) 881 _QUALIFIER_CELL = ''' 882 <TR><TD VALIGN="BOTTOM" PORT="%s" BGCOLOR="%s" BORDER="1">%s</TD></TR> 883 ''' 884 885 _QUALIFIER_DIV = ''' 886 <TR><TD VALIGN="BOTTOM" HEIGHT="10" WIDTH="10" FIXEDSIZE="TRUE"></TD></TR> 887 ''' 888 889 #: Args: (rowspan, bgcolor, classname, attributes, operations, qualifiers) 890 _LABEL = ''' 891 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0"> 892 <TR><TD ROWSPAN="%s"> 893 <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" WIDTH="100" 894 CELLPADDING="0" PORT="body" BGCOLOR="%s"> 895 <TR><TD WIDTH="100">%s</TD></TR> 896 <TR><TD WIDTH="100"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> 897 %s</TABLE></TD></TR> 898 <TR><TD WIDTH="100"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> 899 %s</TABLE></TD></TR> 900 </TABLE> 901 </TD></TR> 902 %s 903 </TABLE>''' 904 905 _COLLAPSED_LABEL = ''' 906 <TABLE CELLBORDER="0" BGCOLOR="%s" PORT="body"> 907 <TR><TD WIDTH="50">%s</TD></TR> 908 </TABLE>''' 909
910 - def _get_html_label(self):
911 # Get the class name & contextualize it. 912 classname = self.class_doc.canonical_name 913 if self.context is not None: 914 classname = classname.contextualize(self.context.canonical_name) 915 916 # If we're collapsed, display the node as a single box. 917 if self.collapsed: 918 return self._COLLAPSED_LABEL % (self.bgcolor, classname) 919 920 # Construct the attribute list. (If it's too long, truncate) 921 attrib_cells = [self._attribute_cell(a) for a in self.attributes] 922 max_attributes = self.options.get('max_attributes', 10) 923 if len(attrib_cells) == 0: 924 attrib_cells = ['<TR><TD></TD></TR>'] 925 elif len(attrib_cells) > max_attributes: 926 attrib_cells[max_attributes-2:-1] = ['<TR><TD>...</TD></TR>'] 927 attributes = ''.join(attrib_cells) 928 929 # Construct the operation list. (If it's too long, truncate) 930 oper_cells = [self._operation_cell(a) for a in self.operations] 931 max_operations = self.options.get('max_operations', 5) 932 if len(oper_cells) == 0: 933 oper_cells = ['<TR><TD></TD></TR>'] 934 elif len(oper_cells) > max_operations: 935 oper_cells[max_operations-2:-1] = ['<TR><TD>...</TD></TR>'] 936 operations = ''.join(oper_cells) 937 938 # Construct the qualifier list & determine the rowspan. 939 if self.qualifiers: 940 rowspan = len(self.qualifiers)*2+2 941 div = self._QUALIFIER_DIV 942 qualifiers = div+div.join([self._qualifier_cell(l,p) for 943 (l,p) in self.qualifiers])+div 944 else: 945 rowspan = 1 946 qualifiers = '' 947 948 # Put it all together. 949 return self._LABEL % (rowspan, self.bgcolor, classname, 950 attributes, operations, qualifiers)
951
952 - def to_dotfile(self):
953 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()] 954 attribs.append('label=<%s>' % self._get_html_label()) 955 s = 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs))) 956 if not self.collapsed: 957 for edge in self.edges: 958 s += '\n' + edge.to_dotfile() 959 if self.same_rank: 960 sr_nodes = ''.join(['node%s; ' % node.id 961 for node in self.same_rank]) 962 # [xx] This can cause dot to crash! not sure why! 963 #s += '{rank=same; node%s; %s}' % (self.id, sr_nodes) 964 return s
965
966 -class DotGraphUmlModuleNode(DotGraphNode):
967 """ 968 A specialized dot grah node used to display `ModuleDoc`\s using 969 UML notation. Simple module nodes look like:: 970 971 .----. 972 +------------+ 973 | modulename | 974 +------------+ 975 976 Packages nodes are drawn with their modules & subpackages nested 977 inside:: 978 979 .----. 980 +----------------------------------------+ 981 | packagename | 982 | | 983 | .----. .----. .----. | 984 | +---------+ +---------+ +---------+ | 985 | | module1 | | module2 | | module3 | | 986 | +---------+ +---------+ +---------+ | 987 | | 988 +----------------------------------------+ 989 990 """
991 - def __init__(self, module_doc, linker, context, collapsed=False, 992 excluded_submodules=(), **options):
993 self.module_doc = module_doc 994 self.linker = linker 995 self.context = context 996 self.collapsed = collapsed 997 self.options = options 998 self.excluded_submodules = excluded_submodules 999 try: url = linker.url_for(module_doc) or NOOP_URL 1000 except NotImplementedError: url = NOOP_URL 1001 DotGraphNode.__init__(self, shape='plaintext', href=url, 1002 tooltip=module_doc.canonical_name)
1003 1004 #: Expects: (color, color, url, tooltip, body) 1005 _MODULE_LABEL = ''' 1006 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" ALIGN="LEFT"> 1007 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" 1008 FIXEDSIZE="true" BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR> 1009 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" WIDTH="20" 1010 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR> 1011 </TABLE>''' 1012 1013 #: Expects: (name, body_rows) 1014 _NESTED_BODY = ''' 1015 <TABLE BORDER="0" CELLBORDER="0" CELLPADDING="0" CELLSPACING="0"> 1016 <TR><TD ALIGN="LEFT">%s</TD></TR> 1017 %s 1018 </TABLE>''' 1019 1020 #: Expects: (cells,) 1021 _NESTED_BODY_ROW = ''' 1022 <TR><TD> 1023 <TABLE BORDER="0" CELLBORDER="0"><TR>%s</TR></TABLE> 1024 </TD></TR>''' 1025
1026 - def _get_html_label(self, package):
1027 """ 1028 :Return: (label, depth, width) where: 1029 1030 - ``label`` is the HTML label 1031 - ``depth`` is the depth of the package tree (for coloring) 1032 - ``width`` is the max width of the HTML label, roughly in 1033 units of characters. 1034 """ 1035 MAX_ROW_WIDTH = 80 # unit is roughly characters. 1036 pkg_name = package.canonical_name 1037 try: pkg_url = self.linker.url_for(package) or NOOP_URL 1038 except NotImplementedError: pkg_url = NOOP_URL 1039 1040 if (not package.is_package or len(package.submodules) == 0 or 1041 self.collapsed): 1042 pkg_color = self._color(package, 1) 1043 label = self._MODULE_LABEL % (pkg_color, pkg_color, 1044 pkg_url, pkg_name, pkg_name[-1]) 1045 return (label, 1, len(pkg_name[-1])+3) 1046 1047 # Get the label for each submodule, and divide them into rows. 1048 row_list = [''] 1049 row_width = 0 1050 max_depth = 0 1051 max_row_width = len(pkg_name[-1])+3 1052 for submodule in package.submodules: 1053 if submodule in self.excluded_submodules: continue 1054 # Get the submodule's label. 1055 label, depth, width = self._get_html_label(submodule) 1056 # Check if we should start a new row. 1057 if row_width > 0 and width+row_width > MAX_ROW_WIDTH: 1058 row_list.append('') 1059 row_width = 0 1060 # Add the submodule's label to the row. 1061 row_width += width 1062 row_list[-1] += '<TD ALIGN="LEFT">%s</TD>' % label 1063 # Update our max's. 1064 max_depth = max(depth, max_depth) 1065 max_row_width = max(row_width, max_row_width) 1066 1067 # Figure out which color to use. 1068 pkg_color = self._color(package, depth+1) 1069 1070 # Assemble & return the label. 1071 rows = ''.join([self._NESTED_BODY_ROW % r for r in row_list]) 1072 body = self._NESTED_BODY % (pkg_name, rows) 1073 label = self._MODULE_LABEL % (pkg_color, pkg_color, 1074 pkg_url, pkg_name, body) 1075 return label, max_depth+1, max_row_width
1076 1077 _COLOR_DIFF = 24
1078 - def _color(self, package, depth):
1079 if package == self.context: return COLOR['SELECTED_BG'] 1080 else: 1081 # Parse the base color. 1082 if re.match(COLOR['MODULE_BG'], 'r#[0-9a-fA-F]{6}$'): 1083 base = int(COLOR['MODULE_BG'][1:], 16) 1084 else: 1085 base = int('d8e8ff', 16) 1086 red = (base & 0xff0000) >> 16 1087 green = (base & 0x00ff00) >> 8 1088 blue = (base & 0x0000ff) 1089 # Make it darker with each level of depth. (but not *too* 1090 # dark -- package name needs to be readable) 1091 red = max(64, red-(depth-1)*self._COLOR_DIFF) 1092 green = max(64, green-(depth-1)*self._COLOR_DIFF) 1093 blue = max(64, blue-(depth-1)*self._COLOR_DIFF) 1094 # Convert it back to a color string 1095 return '#%06x' % ((red<<16)+(green<<8)+blue)
1096
1097 - def to_dotfile(self):
1098 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()] 1099 label, depth, width = self._get_html_label(self.module_doc) 1100 attribs.append('label=<%s>' % label) 1101 return 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
1102 1103 1104 1105 ###################################################################### 1106 #{ Graph Generation Functions 1107 ###################################################################### 1108
1109 -def package_tree_graph(packages, linker, context=None, **options):
1110 """ 1111 Return a `DotGraph` that graphically displays the package 1112 hierarchies for the given packages. 1113 """ 1114 if options.get('style', 'uml') == 'uml': # default to uml style? 1115 if get_dot_version() >= [2]: 1116 return uml_package_tree_graph(packages, linker, context, 1117 **options) 1118 elif 'style' in options: 1119 log.warning('UML style package trees require dot version 2.0+') 1120 1121 graph = DotGraph('Package Tree for %s' % name_list(packages, context), 1122 body='ranksep=.3\nnodesep=.1\n', 1123 edge_defaults={'dir':'none'}) 1124 1125 # Options 1126 if options.get('dir', 'TB') != 'TB': # default: top-to-bottom 1127 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB') 1128 1129 # Get a list of all modules in the package. 1130 queue = list(packages) 1131 modules = set(packages) 1132 for module in queue: 1133 queue.extend(module.submodules) 1134 modules.update(module.submodules) 1135 1136 # Add a node for each module. 1137 nodes = add_valdoc_nodes(graph, modules, linker, context) 1138 1139 # Add an edge for each package/submodule relationship. 1140 for module in modules: 1141 for submodule in module.submodules: 1142 graph.edges.append(DotGraphEdge(nodes[module], nodes[submodule], 1143 headport='tab')) 1144 1145 return graph
1146
1147 -def uml_package_tree_graph(packages, linker, context=None, **options):
1148 """ 1149 Return a `DotGraph` that graphically displays the package 1150 hierarchies for the given packages as a nested set of UML 1151 symbols. 1152 """ 1153 graph = DotGraph('Package Tree for %s' % name_list(packages, context)) 1154 # Remove any packages whose containers are also in the list. 1155 root_packages = [] 1156 for package1 in packages: 1157 for package2 in packages: 1158 if (package1 is not package2 and 1159 package2.canonical_name.dominates(package1.canonical_name)): 1160 break 1161 else: 1162 root_packages.append(package1) 1163 # If the context is a variable, then get its value. 1164 if isinstance(context, VariableDoc) and context.value is not UNKNOWN: 1165 context = context.value 1166 # Return a graph with one node for each root package. 1167 for package in root_packages: 1168 graph.nodes.append(DotGraphUmlModuleNode(package, linker, context)) 1169 return graph
1170 1171 ######################################################################
1172 -def class_tree_graph(classes, linker, context=None, **options):
1173 """ 1174 Return a `DotGraph` that graphically displays the class 1175 hierarchy for the given classes. Options: 1176 1177 - exclude: A list of classes that should be excluded 1178 - dir: LR|RL|BT requests a left-to-right, right-to-left, or 1179 bottom-to- top, drawing. (corresponds to the dot option 1180 'rankdir' 1181 - max_subclass_depth: The maximum depth to which subclasses 1182 will be drawn. 1183 - max_subclasses: A list of ints, specifying how many 1184 subclasses should be drawn per class at each level of the 1185 graph. E.g., [5,3,1] means draw up to 5 subclasses for the 1186 specified classes; up to 3 subsubclasses for each of those (up 1187 to) 5 subclasses; and up to 1 subclass for each of those. 1188 """ 1189 # Callbacks: 1190 def mknode(cls, nodetype, linker, context, options): 1191 return mk_valdoc_node(cls, linker, context)
1192 def mkedge(start, end, edgetype, options): 1193 return DotGraphEdge(start, end) 1194 1195 if isinstance(classes, ClassDoc): classes = [classes] 1196 # [xx] this should be done earlier, and should generate a warning: 1197 classes = [c for c in classes if c is not None] 1198 graph = DotGraph('Class Hierarchy for %s' % name_list(classes, context), 1199 body='ranksep=0.3\n', 1200 edge_defaults={'sametail':True, 'dir':'none'}) 1201 _class_tree_graph(graph, classes, mknode, mkedge, linker, 1202 context, options, cls2node={}) 1203 return graph 1204
1205 -def _class_tree_graph(graph, classes, mknode, mkedge, linker, 1206 context, options, cls2node):
1207 """ 1208 A helper function that is used by both `class_tree_graph()` and 1209 `uml_class_tree_graph()` to draw class trees. To abstract over 1210 the differences between the two, this function takes two callback 1211 functions that create graph nodes and edges: 1212 1213 - ``mknode(base, nodetype, linker, context, options)``: Returns 1214 a `DotGraphNode`. ``nodetype`` is one of: subclass, superclass, 1215 selected, undocumented. 1216 - ``mkedge(begin, end, edgetype, options)``: Returns a 1217 `DotGraphEdge`. ``edgetype`` is one of: subclass, 1218 truncate-subclass. 1219 """ 1220 rankdir = options.get('dir', 'TB') 1221 graph.body += 'rankdir=%s\n' % rankdir 1222 truncated = set() # Classes whose subclasses were truncated 1223 _add_class_tree_superclasses(graph, classes, mknode, mkedge, linker, 1224 context, options, cls2node) 1225 _add_class_tree_subclasses(graph, classes, mknode, mkedge, linker, 1226 context, options, cls2node, truncated) 1227 _add_class_tree_inheritance(graph, classes, mknode, mkedge, linker, 1228 context, options, cls2node, truncated)
1229
1230 -def _add_class_tree_superclasses(graph, classes, mknode, mkedge, linker, 1231 context, options, cls2node):
1232 exclude = options.get('exclude', ()) 1233 1234 # Create nodes for all bases. 1235 for cls in classes: 1236 for base in cls.mro(): 1237 # Don't include 'object' 1238 if base.canonical_name == DottedName('object'): continue 1239 # Stop if we reach an excluded class. 1240 if base in exclude: break 1241 # Don't do the same class twice. 1242 if base in cls2node: continue 1243 # Decide if the base is documented. 1244 try: documented = (linker.url_for(base) is not None) 1245 except: documented = True 1246 # Make the node. 1247 if base in classes: typ = 'selected' 1248 elif not documented: typ = 'undocumented' 1249 else: typ = 'superclass' 1250 cls2node[base] = mknode(base, typ, linker, context, options) 1251 graph.nodes.append(cls2node[base])
1252
1253 -def _add_class_tree_subclasses(graph, classes, mknode, mkedge, linker, 1254 context, options, cls2node, truncated):
1255 exclude = options.get('exclude', ()) 1256 max_subclass_depth = options.get('max_subclass_depth', 3) 1257 max_subclasses = list(options.get('max_subclasses', (5,3,2,1))) 1258 max_subclasses += len(classes)*max_subclasses[-1:] # repeat last num 1259 1260 # Find the depth of each subclass (for truncation) 1261 subclass_depth = _get_subclass_depth_map(classes) 1262 1263 queue = list(classes) 1264 for cls in queue: 1265 # If there are no subclasses, then we're done. 1266 if not isinstance(cls, ClassDoc): continue 1267 if cls.subclasses in (None, UNKNOWN, (), []): continue 1268 # Get the list of subclasses. 1269 subclasses = [subcls for subcls in cls.subclasses 1270 if subcls not in cls2node and subcls not in exclude] 1271 # If the subclass list is too long, then truncate it. 1272 if len(subclasses) > max_subclasses[subclass_depth[cls]]: 1273 subclasses = subclasses[:max_subclasses[subclass_depth[cls]]] 1274 truncated.add(cls) 1275 # Truncate any classes that are too deep. 1276 num_subclasses = len(subclasses) 1277 subclasses = [subcls for subcls in subclasses 1278 if subclass_depth[subcls] <= max_subclass_depth] 1279 if len(subclasses) < num_subclasses: truncated.add(cls) 1280 # Add a node for each subclass. 1281 for subcls in subclasses: 1282 cls2node[subcls] = mknode(subcls, 'subclass', linker, 1283 context, options) 1284 graph.nodes.append(cls2node[subcls]) 1285 # Add the subclasses to our queue. 1286 queue.extend(subclasses)
1287
1288 -def _add_class_tree_inheritance(graph, classes, mknode, mkedge, linker, 1289 context, options, cls2node, truncated):
1290 # Add inheritance edges. 1291 for (cls, node) in cls2node.items(): 1292 if cls.bases is UNKNOWN: continue 1293 for base in cls.bases: 1294 if base in cls2node: 1295 graph.edges.append(mkedge(cls2node[base], node, 1296 'subclass', options)) 1297 # Mark truncated classes 1298 for cls in truncated: 1299 ellipsis = DotGraphNode('...', shape='plaintext', 1300 width='0', height='0') 1301 graph.nodes.append(ellipsis) 1302 graph.edges.append(mkedge(cls2node[cls], ellipsis, 1303 'truncate-subclass', options))
1304
1305 -def _get_subclass_depth_map(classes):
1306 subclass_depth = dict([(cls,0) for cls in classes]) 1307 queue = list(classes) 1308 for cls in queue: 1309 if (isinstance(cls, ClassDoc) and 1310 cls.subclasses not in (None, UNKNOWN)): 1311 for subcls in cls.subclasses: 1312 subclass_depth[subcls] = max(subclass_depth.get(subcls,0), 1313 subclass_depth[cls]+1) 1314 queue.append(subcls) 1315 return subclass_depth
1316 1317 1318 1319 ######################################################################
1320 -def uml_class_tree_graph(classes, linker, context=None, **options):
1321 """ 1322 Return a `DotGraph` that graphically displays the class hierarchy 1323 for the given class, using UML notation. Options: 1324 1325 - exclude: A list of classes that should be excluded 1326 - dir: LR|RL|BT requests a left-to-right, right-to-left, or 1327 bottom-to- top, drawing. (corresponds to the dot option 1328 'rankdir' 1329 - max_subclass_depth: The maximum depth to which subclasses 1330 will be drawn. 1331 - max_subclasses: A list of ints, specifying how many 1332 subclasses should be drawn per class at each level of the 1333 graph. E.g., [5,3,1] means draw up to 5 subclasses for the 1334 specified classes; up to 3 subsubclasses for each of those (up 1335 to) 5 subclasses; and up to 1 subclass for each of those. 1336 - max_attributes 1337 - max_operations 1338 - show_private_vars 1339 - show_magic_vars 1340 - link_attributes 1341 - show_signature_defaults 1342 - max_signature_width 1343 """ 1344 cls2node = {} 1345 1346 # Draw the basic graph: 1347 if isinstance(classes, ClassDoc): classes = [classes] 1348 graph = DotGraph('UML class diagram for %s' % name_list(classes, context), 1349 body='ranksep=.2\n;nodesep=.3\n') 1350 _class_tree_graph(graph, classes, _uml_mknode, _uml_mkedge, 1351 linker, context, options, cls2node) 1352 1353 # Turn attributes into links (optional): 1354 inheritance_nodes = set(graph.nodes) 1355 if options.get('link_attributes', True): 1356 for cls in classes: 1357 for base in cls.mro(): 1358 node = cls2node.get(base) 1359 if node is None: continue 1360 node.link_attributes(graph, cls2node) 1361 # Make sure that none of the new attribute edges break 1362 # the rank ordering assigned by inheritance. 1363 for edge in node.edges: 1364 if edge.end in inheritance_nodes: 1365 edge['constraint'] = 'False' 1366 1367 return graph
1368 1369 # A callback to make graph nodes:
1370 -def _uml_mknode(cls, nodetype, linker, context, options):
1371 if nodetype == 'subclass': 1372 return DotGraphUmlClassNode( 1373 cls, linker, context, collapsed=True, 1374 bgcolor=COLOR['SUBCLASS_BG'], **options) 1375 elif nodetype in ('selected', 'superclass', 'undocumented'): 1376 if nodetype == 'selected': bgcolor = COLOR['SELECTED_BG'] 1377 if nodetype == 'superclass': bgcolor = COLOR['BASECLASS_BG'] 1378 if nodetype == 'undocumented': bgcolor = COLOR['UNDOCUMENTED_BG'] 1379 return DotGraphUmlClassNode( 1380 cls, linker, context, show_inherited_vars=False, 1381 collapsed=False, bgcolor=bgcolor, **options) 1382 assert 0, 'bad nodetype'
1383 1384 # A callback to make graph edges:
1385 -def _uml_mkedge(start, end, edgetype, options):
1386 if edgetype == 'subclass': 1387 return DotGraphEdge( 1388 start, end, dir='back', arrowtail='empty', 1389 headport='body', tailport='body', color=COLOR['INH_LINK'], 1390 weight=100, style='bold') 1391 if edgetype == 'truncate-subclass': 1392 return DotGraphEdge( 1393 start, end, dir='back', arrowtail='empty', 1394 tailport='body', color=COLOR['INH_LINK'], 1395 weight=100, style='bold') 1396 assert 0, 'bad edgetype'
1397 1398 ######################################################################
1399 -def import_graph(modules, docindex, linker, context=None, **options):
1400 graph = DotGraph('Import Graph', body='ranksep=.3\n;nodesep=.3\n') 1401 1402 # Options 1403 if options.get('dir', 'RL') != 'TB': # default: right-to-left. 1404 graph.body += 'rankdir=%s\n' % options.get('dir', 'RL') 1405 1406 # Add a node for each module. 1407 nodes = add_valdoc_nodes(graph, modules, linker, context) 1408 1409 # Edges. 1410 edges = set() 1411 for dst in modules: 1412 if dst.imports in (None, UNKNOWN): continue 1413 for var_name in dst.imports: 1414 for i in range(len(var_name), 0, -1): 1415 val_doc = docindex.find(var_name[:i], context) 1416 if isinstance(val_doc, ModuleDoc): 1417 if val_doc in nodes and dst in nodes: 1418 edges.add((nodes[val_doc], nodes[dst])) 1419 break 1420 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges] 1421 1422 return graph
1423 1424 ######################################################################
1425 -def call_graph(api_docs, docindex, linker, context=None, **options):
1426 """ 1427 :param options: 1428 - ``dir``: rankdir for the graph. (default=LR) 1429 - ``add_callers``: also include callers for any of the 1430 routines in ``api_docs``. (default=False) 1431 - ``add_callees``: also include callees for any of the 1432 routines in ``api_docs``. (default=False) 1433 :todo: Add an ``exclude`` option? 1434 """ 1435 if docindex.callers is None: 1436 log.warning("No profiling information for call graph!") 1437 return DotGraph('Call Graph') # return None instead? 1438 1439 if isinstance(context, VariableDoc): 1440 context = context.value 1441 1442 # Get the set of requested functions. 1443 functions = [] 1444 for api_doc in api_docs: 1445 # If it's a variable, get its value. 1446 if isinstance(api_doc, VariableDoc): 1447 api_doc = api_doc.value 1448 # Add the value to the functions list. 1449 if isinstance(api_doc, RoutineDoc): 1450 functions.append(api_doc) 1451 elif isinstance(api_doc, NamespaceDoc): 1452 for vardoc in api_doc.variables.values(): 1453 if isinstance(vardoc.value, RoutineDoc): 1454 functions.append(vardoc.value) 1455 1456 # Filter out functions with no callers/callees? 1457 # [xx] this isnt' quite right, esp if add_callers or add_callees 1458 # options are fales. 1459 functions = [f for f in functions if 1460 (f in docindex.callers) or (f in docindex.callees)] 1461 1462 # Add any callers/callees of the selected functions 1463 func_set = set(functions) 1464 if options.get('add_callers', False) or options.get('add_callees', False): 1465 for func_doc in functions: 1466 if options.get('add_callers', False): 1467 func_set.update(docindex.callers.get(func_doc, ())) 1468 if options.get('add_callees', False): 1469 func_set.update(docindex.callees.get(func_doc, ())) 1470 1471 graph = DotGraph('Call Graph for %s' % name_list(api_docs, context), 1472 node_defaults={'shape':'box', 'width': 0, 'height': 0}) 1473 1474 # Options 1475 if options.get('dir', 'LR') != 'TB': # default: left-to-right 1476 graph.body += 'rankdir=%s\n' % options.get('dir', 'LR') 1477 1478 nodes = add_valdoc_nodes(graph, func_set, linker, context) 1479 1480 # Find the edges. 1481 edges = set() 1482 for func_doc in functions: 1483 for caller in docindex.callers.get(func_doc, ()): 1484 if caller in nodes: 1485 edges.add( (nodes[caller], nodes[func_doc]) ) 1486 for callee in docindex.callees.get(func_doc, ()): 1487 if callee in nodes: 1488 edges.add( (nodes[func_doc], nodes[callee]) ) 1489 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges] 1490 1491 return graph
1492 1493 ###################################################################### 1494 #{ Dot Version 1495 ###################################################################### 1496 1497 _dot_version = None 1498 _DOT_VERSION_RE = re.compile(r'dot version ([\d\.]+)')
1499 -def get_dot_version():
1500 global _dot_version 1501 if _dot_version is None: 1502 try: 1503 out, err = run_subprocess([DOT_COMMAND, '-V']) 1504 version_info = err or out 1505 m = _DOT_VERSION_RE.match(version_info) 1506 if m: 1507 _dot_version = [int(x) for x in m.group(1).split('.')] 1508 else: 1509 _dot_version = (0,) 1510 except OSError, e: 1511 log.error('dot executable not found; graphs will not be ' 1512 'generated. Adjust your shell\'s path, or use ' 1513 '--dotpath to specify the path to the dot ' 1514 'executable.' % DOT_COMMAND) 1515 _dot_version = (0,) 1516 log.info('Detected dot version %s' % _dot_version) 1517 return _dot_version
1518 1519 ###################################################################### 1520 #{ Helper Functions 1521 ###################################################################### 1522
1523 -def add_valdoc_nodes(graph, val_docs, linker, context):
1524 """ 1525 :todo: Use different node styles for different subclasses of APIDoc 1526 """ 1527 nodes = {} 1528 for val_doc in sorted(val_docs, key=lambda d:d.canonical_name): 1529 nodes[val_doc] = mk_valdoc_node(val_doc, linker, context) 1530 graph.nodes.append(nodes[val_doc]) 1531 return nodes
1532
1533 -def mk_valdoc_node(val_doc, linker, context):
1534 label = val_doc.canonical_name 1535 if context is not None: 1536 label = label.contextualize(context.canonical_name) 1537 node = DotGraphNode(label) 1538 specialize_valdoc_node(node, val_doc, context, linker) 1539 return node
1540 1541 NOOP_URL = 'javascript:void(0);' 1542 MODULE_NODE_HTML = ''' 1543 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" 1544 CELLPADDING="0" PORT="table" ALIGN="LEFT"> 1545 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" FIXEDSIZE="true" 1546 BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR> 1547 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" 1548 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR> 1549 </TABLE>'''.strip() 1550
1551 -def specialize_valdoc_node(node, val_doc, context, linker):
1552 """ 1553 Update the style attributes of `node` to reflext its type 1554 and context. 1555 """ 1556 # We can only use html-style nodes if dot_version>2. 1557 dot_version = get_dot_version() 1558 1559 # If val_doc or context is a variable, get its value. 1560 if isinstance(val_doc, VariableDoc) and val_doc.value is not UNKNOWN: 1561 val_doc = val_doc.value 1562 if isinstance(context, VariableDoc) and context.value is not UNKNOWN: 1563 context = context.value 1564 1565 # Set the URL. (Do this even if it points to the page we're 1566 # currently on; otherwise, the tooltip is ignored.) 1567 try: url = linker.url_for(val_doc) or NOOP_URL 1568 except NotImplementedError: url = NOOP_URL 1569 node['href'] = url 1570 1571 if (url is None and 1572 hasattr(linker, 'docindex') and 1573 linker.docindex.find(identifier, self.container) is None): 1574 node['fillcolor'] = COLOR['UNDOCUMENTED_BG'] 1575 node['style'] = 'filled' 1576 1577 if isinstance(val_doc, ModuleDoc) and dot_version >= [2]: 1578 node['shape'] = 'plaintext' 1579 if val_doc == context: color = COLOR['SELECTED_BG'] 1580 else: color = COLOR['MODULE_BG'] 1581 node['tooltip'] = node['label'] 1582 node['html_label'] = MODULE_NODE_HTML % (color, color, url, 1583 val_doc.canonical_name, 1584 node['label']) 1585 node['width'] = node['height'] = 0 1586 node.port = 'body' 1587 1588 elif isinstance(val_doc, RoutineDoc): 1589 node['shape'] = 'box' 1590 node['style'] = 'rounded' 1591 node['width'] = 0 1592 node['height'] = 0 1593 node['label'] = '%s()' % node['label'] 1594 node['tooltip'] = node['label'] 1595 if val_doc == context: 1596 node['fillcolor'] = COLOR['SELECTED_BG'] 1597 node['style'] = 'filled,rounded,bold' 1598 1599 else: 1600 node['shape'] = 'box' 1601 node['width'] = 0 1602 node['height'] = 0 1603 node['tooltip'] = node['label'] 1604 if val_doc == context: 1605 node['fillcolor'] = COLOR['SELECTED_BG'] 1606 node['style'] = 'filled,bold'
1607
1608 -def name_list(api_docs, context=None):
1609 names = [d.canonical_name for d in api_docs] 1610 if context is not None: 1611 names = [name.contextualize(context.canonical_name) for name in names] 1612 if len(names) == 0: return '' 1613 if len(names) == 1: return '%s' % names[0] 1614 elif len(names) == 2: return '%s and %s' % (names[0], names[1]) 1615 else: 1616 names = ['%s' % name for name in names] 1617 return '%s, and %s' % (', '.join(names[:-1]), names[-1])
1618