1
2
3
4
5
6
7
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 *
29
30
31 MODULE_BG = '#d8e8ff'
32 CLASS_BG = '#d8ffe8'
33 SELECTED_BG = '#ffd0d0'
34 BASECLASS_BG = '#e0b0a0'
35 SUBCLASS_BG = '#e0b0a0'
36 ROUTINE_BG = '#e8d0b0'
37 INH_LINK_COLOR = '#800000'
38
39
40
41
42
43 DOT_COMMAND = 'dot'
44 """The command that should be used to spawn dot"""
45
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
118 if isinstance(self.title, unicode):
119 self.title = self.title.encode('ascii', 'xmlcharrefreplace')
120
121
122 self.uid = self.uid[:30]
123
124
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
140
141
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 ''
145 else:
146 if not self.write(image_file):
147 return ''
148 cmapx = self.render('cmapx') or ''
149
150 title = plaintext_to_html(self.title or '')
151 caption = plaintext_to_html(self.caption or '')
152 if title or caption:
153 css_class = 'graph-with-title'
154 else:
155 css_class = 'graph-without-title'
156 if len(title)+len(caption) > 80:
157 title_align = 'left'
158 table_width = ' width="600"'
159 else:
160 title_align = 'center'
161 table_width = ''
162
163 if center: s = '<center>'
164 if title or caption:
165 s += ('<p><table border="0" cellpadding="0" cellspacing="0" '
166 'class="graph"%s>\n <tr><td align="center">\n' %
167 table_width)
168 s += (' %s\n <img src="%s" alt=%r usemap="#%s" '
169 'ismap="ismap" class="%s">\n' %
170 (cmapx.strip(), image_url, title, self.uid, css_class))
171 if title or caption:
172 s += ' </td></tr>\n <tr><td align=%r>\n' % title_align
173 if title:
174 s += '<span class="graph-title">%s</span>' % title
175 if title and caption:
176 s += ' -- '
177 if caption:
178 s += '<span class="graph-caption">%s</span>' % caption
179 s += '\n </th></tr>\n</table></p>'
180 if center: s += '</center>'
181 return s
182
183 - def link(self, docstring_linker):
184 """
185 Replace any href attributes whose value is <name> with
186 the url of the object whose name is <name>.
187 """
188
189 self._link_href(self.node_defaults, docstring_linker)
190 for node in self.nodes:
191 self._link_href(node.attribs, docstring_linker)
192
193
194 self._link_href(self.edge_defaults, docstring_linker)
195 for edge in self.nodes:
196 self._link_href(edge.attribs, docstring_linker)
197
198
200 url = docstring_linker.url_for(m.group(1))
201 if url: return 'href="%s"%s' % (url, m.group(2))
202 else: return ''
203 self.body = re.sub("href\s*=\s*['\"]?<([\w\.]+)>['\"]?\s*(,?)",
204 subfunc, self.body)
205
207 """Helper for `link()`"""
208 if 'href' in attribs:
209 m = re.match(r'^<([\w\.]+)>$', attribs['href'])
210 if m:
211 url = docstring_linker.url_for(m.group(1))
212 if url: attribs['href'] = url
213 else: del attribs['href']
214
215 - def write(self, filename, language='gif'):
216 """
217 Render the graph using the output format `language`, and write
218 the result to `filename`.
219
220 :return: True if rendering was successful.
221 """
222 result = self._run_dot('-T%s' % language,
223 '-o%s' % filename)
224
225 if language == 'cmapx' and result is not None:
226 result = result.decode('utf-8')
227 return (result is not None)
228
229 - def render(self, language='gif'):
230 """
231 Use the ``dot`` command to render this graph, using the output
232 format `language`. Return the result as a string, or `None`
233 if the rendering failed.
234 """
235 return self._run_dot('-T%s' % language)
236
238 try:
239 result, err = run_subprocess((DOT_COMMAND,)+options,
240 self.to_dotfile())
241 if err: log.warning("Graphviz dot warning(s):\n%s" % err)
242 except OSError, e:
243 log.warning("Unable to render Graphviz dot graph:\n%s" % e)
244
245 return None
246
247 return result
248
250 """
251 Return the string contents of the dot file that should be used
252 to render this graph.
253 """
254 lines = ['digraph %s {' % self.uid,
255 'node [%s]' % ','.join(['%s="%s"' % (k,v) for (k,v)
256 in self.node_defaults.items()]),
257 'edge [%s]' % ','.join(['%s="%s"' % (k,v) for (k,v)
258 in self.edge_defaults.items()])]
259 if self.body:
260 lines.append(self.body)
261 lines.append('/* Nodes */')
262 for node in self.nodes:
263 lines.append(node.to_dotfile())
264 lines.append('/* Edges */')
265 for edge in self.edges:
266 lines.append(edge.to_dotfile())
267 lines.append('}')
268
269
270 return u'\n'.join(lines).encode('utf-8')
271
273 _next_id = 0
274 - def __init__(self, label=None, html_label=None, **attribs):
275 if label is not None and html_label is not None:
276 raise ValueError('Use label or html_label, not both.')
277 if label is not None: attribs['label'] = label
278 self._html_label = html_label
279 self._attribs = attribs
280 self.id = self.__class__._next_id
281 self.__class__._next_id += 1
282 self.port = None
283
285 return self._attribs[attr]
286
288 if attr == 'html_label':
289 self._attribs.pop('label')
290 self._html_label = val
291 else:
292 if attr == 'label': self._html_label = None
293 self._attribs[attr] = val
294
296 """
297 Return the dot commands that should be used to render this node.
298 """
299 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()
300 if v is not None]
301 if self._html_label:
302 attribs.insert(0, 'label=<%s>' % (self._html_label,))
303 if attribs: attribs = ' [%s]' % (','.join(attribs))
304 return 'node%d%s' % (self.id, attribs)
305
307 - def __init__(self, start, end, label=None, **attribs):
308 """
309 :type start: `DotGraphNode`
310 :type end: `DotGraphNode`
311 """
312 assert isinstance(start, DotGraphNode)
313 assert isinstance(end, DotGraphNode)
314 if label is not None: attribs['label'] = label
315 self.start = start
316 self.end = end
317 self._attribs = attribs
318
320 return self._attribs[attr]
321
323 self._attribs[attr] = val
324
326 """
327 Return the dot commands that should be used to render this edge.
328 """
329
330 attribs = self._attribs.copy()
331 if (self.start.port is not None and 'headport' not in attribs):
332 attribs['headport'] = self.start.port
333 if (self.end.port is not None and 'tailport' not in attribs):
334 attribs['tailport'] = self.end.port
335
336 attribs = ','.join(['%s="%s"' % (k,v) for (k,v) in attribs.items()
337 if v is not None])
338 if attribs: attribs = ' [%s]' % attribs
339
340 return 'node%d -> node%d%s' % (self.start.id, self.end.id, attribs)
341
342
343
344
345
347 """
348 A specialized dot graph node used to display `ClassDoc`\s using
349 UML notation. The node is rendered as a table with three cells:
350 the top cell contains the class name; the middle cell contains a
351 list of attributes; and the bottom cell contains a list of
352 operations::
353
354 +-------------+
355 | ClassName |
356 +-------------+
357 | x: int |
358 | ... |
359 +-------------+
360 | f(self, x) |
361 | ... |
362 +-------------+
363
364 `DotGraphUmlClassNode`\s may be *collapsed*, in which case they are
365 drawn as a simple box containing the class name::
366
367 +-------------+
368 | ClassName |
369 +-------------+
370
371 Attributes with types corresponding to documented classes can
372 optionally be converted into edges, using `link_attributes()`.
373
374 :todo: Add more options?
375 - show/hide operation signature
376 - show/hide operation signature types
377 - show/hide operation signature return type
378 - show/hide attribute types
379 - use qualifiers
380 """
381
382 - def __init__(self, class_doc, linker, context, collapsed=False,
383 bgcolor=CLASS_BG, **options):
384 """
385 Create a new `DotGraphUmlClassNode` based on the class
386 `class_doc`.
387
388 :Parameters:
389 `linker` : `DocstringLinker<markup.DocstringLinker>`
390 Used to look up URLs for classes.
391 `context` : `APIDoc`
392 The context in which this node will be drawn; dotted
393 names will be contextualized to this context.
394 `collapsed` : ``bool``
395 If true, then display this node as a simple box.
396 `bgcolor` : ``str``
397 The background color for this node.
398 `options` : ``dict``
399 A set of options used to control how the node should
400 be displayed.
401
402 :Keywords:
403 - `show_private_vars`: If false, then private variables
404 are filtered out of the attributes & operations lists.
405 (Default: *False*)
406 - `show_magic_vars`: If false, then magic variables
407 (such as ``__init__`` and ``__add__``) are filtered out of
408 the attributes & operations lists. (Default: *True*)
409 - `show_inherited_vars`: If false, then inherited variables
410 are filtered out of the attributes & operations lists.
411 (Default: *False*)
412 - `max_attributes`: The maximum number of attributes that
413 should be listed in the attribute box. If the class has
414 more than this number of attributes, some will be
415 ellided. Ellipsis is marked with ``'...'``.
416 - `max_operations`: The maximum number of operations that
417 should be listed in the operation box.
418 - `add_nodes_for_linked_attributes`: If true, then
419 `link_attributes()` will create new a collapsed node for
420 the types of a linked attributes if no node yet exists for
421 that type.
422 """
423 self.class_doc = class_doc
424 """The class represented by this node."""
425
426 self.linker = linker
427 """Used to look up URLs for classes."""
428
429 self.context = context
430 """The context in which the node will be drawn."""
431
432 self.bgcolor = bgcolor
433 """The background color of the node."""
434
435 self.options = options
436 """Options used to control how the node is displayed."""
437
438 self.collapsed = collapsed
439 """If true, then draw this node as a simple box."""
440
441 self.attributes = []
442 """The list of VariableDocs for attributes"""
443
444 self.operations = []
445 """The list of VariableDocs for operations"""
446
447 self.qualifiers = []
448 """List of (key_label, port) tuples."""
449
450 self.edges = []
451 """List of edges used to represent this node's attributes.
452 These should not be added to the `DotGraph`; this node will
453 generate their dotfile code directly."""
454
455
456 show_private = options.get('show_private_vars', False)
457 show_magic = options.get('show_magic_vars', True)
458 show_inherited = options.get('show_inherited_vars', False)
459 for name, var in class_doc.variables.iteritems():
460 if ((not show_private and var.is_public == False) or
461 (not show_magic and re.match('__\w+__$', name)) or
462 (not show_inherited and var.container != class_doc)):
463 pass
464 elif isinstance(var.value, RoutineDoc):
465 self.operations.append(var)
466 else:
467 self.attributes.append(var)
468
469
470 DotGraphNode.__init__(self, tooltip=class_doc.canonical_name,
471 width=0, height=0, shape='plaintext',
472 href=linker.url_for(class_doc) or NOOP_URL)
473
474
475
476
477
478 SIMPLE_TYPE_RE = re.compile(
479 r'^([\w\.]+)$')
480 """A regular expression that matches descriptions of simple types."""
481
482 COLLECTION_TYPE_RE = re.compile(
483 r'^(list|set|sequence|tuple|collection) of ([\w\.]+)$')
484 """A regular expression that matches descriptions of collection types."""
485
486 MAPPING_TYPE_RE = re.compile(
487 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ([\w\.]+)$')
488 """A regular expression that matches descriptions of mapping types."""
489
490 MAPPING_TO_COLLECTION_TYPE_RE = re.compile(
491 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to '
492 r'(list|set|sequence|tuple|collection) of ([\w\.]+)$')
493 """A regular expression that matches descriptions of mapping types
494 whose value type is a collection."""
495
496 OPTIONAL_TYPE_RE = re.compile(
497 r'^(None or|optional) ([\w\.]+)$|^([\w\.]+) or None$')
498 """A regular expression that matches descriptions of optional types."""
499
501 """
502 Convert any attributes with type descriptions corresponding to
503 documented classes to edges. The following type descriptions
504 are currently handled:
505
506 - Dotted names: Create an attribute edge to the named type,
507 labelled with the variable name.
508 - Collections: Create an attribute edge to the named type,
509 labelled with the variable name, and marked with '*' at the
510 type end of the edge.
511 - Mappings: Create an attribute edge to the named type,
512 labelled with the variable name, connected to the class by
513 a qualifier box that contains the key type description.
514 - Optional: Create an attribute edge to the named type,
515 labelled with the variable name, and marked with '0..1' at
516 the type end of the edge.
517
518 The edges created by `link_attribute()` are handled internally
519 by `DotGraphUmlClassNode`; they should *not* be added directly
520 to the `DotGraph`.
521
522 :param nodes: A dictionary mapping from `ClassDoc`\s to
523 `DotGraphUmlClassNode`\s, used to look up the nodes for
524 attribute types. If the ``add_nodes_for_linked_attributes``
525 option is used, then new nodes will be added to this
526 dictionary for any types that are not already listed.
527 These added nodes must be added to the `DotGraph`.
528 """
529
530
531
532
533 self.attributes = [var for var in self.attributes
534 if not self._link_attribute(var, nodes)]
535
537 """
538 Helper for `link_attributes()`: try to convert the attribute
539 variable `var` into an edge, and add that edge to
540 `self.edges`. Return ``True`` iff the variable was
541 successfully converted to an edge (in which case, it should be
542 removed from the attributes list).
543 """
544 type_descr = self._type_descr(var) or self._type_descr(var.value)
545
546
547 m = self.SIMPLE_TYPE_RE.match(type_descr)
548 if m and self._add_attribute_edge(var, nodes, m.group(1)):
549 return True
550
551
552 m = self.COLLECTION_TYPE_RE.match(type_descr)
553 if m and self._add_attribute_edge(var, nodes, m.group(2),
554 headlabel='*'):
555 return True
556
557
558 m = self.OPTIONAL_TYPE_RE.match(type_descr)
559 if m and self._add_attribute_edge(var, nodes, m.group(2) or m.group(3),
560 headlabel='0..1'):
561 return True
562
563
564 m = self.MAPPING_TYPE_RE.match(type_descr)
565 if m:
566 port = 'qualifier_%s' % var.name
567 if self._add_attribute_edge(var, nodes, m.group(3),
568 tailport='%s:e' % port):
569 self.qualifiers.append( (m.group(2), port) )
570 return True
571
572
573 m = self.MAPPING_TO_COLLECTION_TYPE_RE.match(type_descr)
574 if m:
575 port = 'qualifier_%s' % var.name
576 if self._add_attribute_edge(var, nodes, m.group(4), headlabel='*',
577 tailport='%s:e' % port):
578 self.qualifiers.append( (m.group(2), port) )
579 return True
580
581
582 return False
583
585 """
586 Helper for `link_attribute()`: try to add an edge for the
587 given attribute variable `var`. Return ``True`` if
588 successful.
589 """
590
591 type_doc = self.linker.docindex.find(type_str, var)
592 if not type_doc: return False
593
594
595
596 type_node = nodes.get(type_doc)
597 if not type_node:
598 if self.options.get('add_nodes_for_linked_attributes', True):
599 type_node = DotGraphUmlClassNode(type_doc, self.linker,
600 self.context, collapsed=True)
601 nodes[type_doc] = type_node
602 else:
603 return False
604
605
606
607 attribs.setdefault('headport', 'body')
608 attribs.setdefault('tailport', 'body')
609 url = self.linker.url_for(var) or NOOP_URL
610 self.edges.append(DotGraphEdge(self, type_node, label=var.name,
611 arrowhead='open', href=url,
612 tooltip=var.canonical_name, labeldistance=1.5,
613 **attribs))
614 return True
615
616
617
618
625
632
638
639
640
641
642
653
655 """
656 :todo: do 'word wrapping' on the signature, by starting a new
657 row in the table, if necessary. How to indent the new
658 line? Maybe use align=right? I don't think dot has a
659 .
660 :todo: Optionally add return type info?
661 """
662
663 func_doc = var_doc.value
664 args = [self._operation_arg(n, d, func_doc) for (n, d)
665 in zip(func_doc.posargs, func_doc.posarg_defaults)]
666 args = [plaintext_to_html(arg) for arg in args]
667 if func_doc.vararg: args.append('*'+func_doc.vararg)
668 if func_doc.kwarg: args.append('**'+func_doc.kwarg)
669 label = '%s(%s)' % (var_doc.name, ', '.join(args))
670
671 url = self.linker.url_for(var_doc) or NOOP_URL
672
673 return self._OPERATION_CELL % (url, self._tooltip(var_doc), label)
674
690
693
694
695 _ATTRIBUTE_CELL = '''
696 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR>
697 '''
698
699
700 _OPERATION_CELL = '''
701 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR>
702 '''
703
704
705 _QUALIFIER_CELL = '''
706 <TR><TD VALIGN="BOTTOM" PORT="%s" BGCOLOR="%s" BORDER="1">%s</TD></TR>
707 '''
708
709 _QUALIFIER_DIV = '''
710 <TR><TD VALIGN="BOTTOM" HEIGHT="10" WIDTH="10" FIXEDSIZE="TRUE"></TD></TR>
711 '''
712
713
714 _LABEL = '''
715 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0">
716 <TR><TD ROWSPAN="%s">
717 <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"
718 CELLPADDING="0" PORT="body" BGCOLOR="%s">
719 <TR><TD>%s</TD></TR>
720 <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
721 %s</TABLE></TD></TR>
722 <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
723 %s</TABLE></TD></TR>
724 </TABLE>
725 </TD></TR>
726 %s
727 </TABLE>'''
728
729 _COLLAPSED_LABEL = '''
730 <TABLE CELLBORDER="0" BGCOLOR="%s" PORT="body">
731 <TR><TD>%s</TD></TR>
732 </TABLE>'''
733
735
736 classname = self.class_doc.canonical_name
737 classname = classname.contextualize(self.context.canonical_name)
738
739
740 if self.collapsed:
741 return self._COLLAPSED_LABEL % (self.bgcolor, classname)
742
743
744 attrib_cells = [self._attribute_cell(a) for a in self.attributes]
745 max_attributes = self.options.get('max_attributes', 15)
746 if len(attrib_cells) == 0:
747 attrib_cells = ['<TR><TD></TD></TR>']
748 elif len(attrib_cells) > max_attributes:
749 attrib_cells[max_attributes-2:-1] = ['<TR><TD>...</TD></TR>']
750 attributes = ''.join(attrib_cells)
751
752
753 oper_cells = [self._operation_cell(a) for a in self.operations]
754 max_operations = self.options.get('max_operations', 15)
755 if len(oper_cells) == 0:
756 oper_cells = ['<TR><TD></TD></TR>']
757 elif len(oper_cells) > max_operations:
758 oper_cells[max_operations-2:-1] = ['<TR><TD>...</TD></TR>']
759 operations = ''.join(oper_cells)
760
761
762 if self.qualifiers:
763 rowspan = len(self.qualifiers)*2+2
764 div = self._QUALIFIER_DIV
765 qualifiers = div+div.join([self._qualifier_cell(l,p) for
766 (l,p) in self.qualifiers])+div
767 else:
768 rowspan = 1
769 qualifiers = ''
770
771
772 return self._LABEL % (rowspan, self.bgcolor, classname,
773 attributes, operations, qualifiers)
774
776 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()]
777 attribs.append('label=<%s>' % self._get_html_label())
778 s = 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
779 if not self.collapsed:
780 for edge in self.edges:
781 s += '\n' + edge.to_dotfile()
782 return s
783
785 """
786 A specialized dot grah node used to display `ModuleDoc`\s using
787 UML notation. Simple module nodes look like::
788
789 .----.
790 +------------+
791 | modulename |
792 +------------+
793
794 Packages nodes are drawn with their modules & subpackages nested
795 inside::
796
797 .----.
798 +----------------------------------------+
799 | packagename |
800 | |
801 | .----. .----. .----. |
802 | +---------+ +---------+ +---------+ |
803 | | module1 | | module2 | | module3 | |
804 | +---------+ +---------+ +---------+ |
805 | |
806 +----------------------------------------+
807
808 """
809 - def __init__(self, module_doc, linker, context, collapsed=False,
810 excluded_submodules=(), **options):
811 self.module_doc = module_doc
812 self.linker = linker
813 self.context = context
814 self.collapsed = collapsed
815 self.options = options
816 self.excluded_submodules = excluded_submodules
817 DotGraphNode.__init__(self, shape='plaintext',
818 href=linker.url_for(module_doc) or NOOP_URL,
819 tooltip=module_doc.canonical_name)
820
821
822 _MODULE_LABEL = '''
823 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" ALIGN="LEFT">
824 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16"
825 FIXEDSIZE="true" BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR>
826 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" WIDTH="20"
827 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR>
828 </TABLE>'''
829
830
831 _NESTED_BODY = '''
832 <TABLE BORDER="0" CELLBORDER="0" CELLPADDING="0" CELLSPACING="0">
833 <TR><TD ALIGN="LEFT">%s</TD></TR>
834 %s
835 </TABLE>'''
836
837
838 _NESTED_BODY_ROW = '''
839 <TR><TD>
840 <TABLE BORDER="0" CELLBORDER="0"><TR>%s</TR></TABLE>
841 </TD></TR>'''
842
844 """
845 :Return: (label, depth, width) where:
846
847 - `label` is the HTML label
848 - `depth` is the depth of the package tree (for coloring)
849 - `width` is the max width of the HTML label, roughly in
850 units of characters.
851 """
852 MAX_ROW_WIDTH = 80
853 pkg_name = package.canonical_name
854 pkg_url = self.linker.url_for(package) or NOOP_URL
855
856 if (not package.is_package or len(package.submodules) == 0 or
857 self.collapsed):
858 pkg_color = self._color(package, 1)
859 label = self._MODULE_LABEL % (pkg_color, pkg_color,
860 pkg_url, pkg_name, pkg_name[-1])
861 return (label, 1, len(pkg_name[-1])+3)
862
863
864 row_list = ['']
865 row_width = 0
866 max_depth = 0
867 max_row_width = len(pkg_name[-1])+3
868 for submodule in package.submodules:
869 if submodule in self.excluded_submodules: continue
870
871 label, depth, width = self._get_html_label(submodule)
872
873 if row_width > 0 and width+row_width > MAX_ROW_WIDTH:
874 row_list.append('')
875 row_width = 0
876
877 row_width += width
878 row_list[-1] += '<TD ALIGN="LEFT">%s</TD>' % label
879
880 max_depth = max(depth, max_depth)
881 max_row_width = max(row_width, max_row_width)
882
883
884 pkg_color = self._color(package, depth+1)
885
886
887 rows = ''.join([self._NESTED_BODY_ROW % r for r in row_list])
888 body = self._NESTED_BODY % (pkg_name, rows)
889 label = self._MODULE_LABEL % (pkg_color, pkg_color,
890 pkg_url, pkg_name, body)
891 return label, max_depth+1, max_row_width
892
893 _COLOR_DIFF = 24
894 - def _color(self, package, depth):
895 if package == self.context: return SELECTED_BG
896 else:
897
898 if re.match(MODULE_BG, 'r#[0-9a-fA-F]{6}$'):
899 base = int(MODULE_BG[1:], 16)
900 else:
901 base = int('d8e8ff', 16)
902 red = (base & 0xff0000) >> 16
903 green = (base & 0x00ff00) >> 8
904 blue = (base & 0x0000ff)
905
906
907 red = max(64, red-(depth-1)*self._COLOR_DIFF)
908 green = max(64, green-(depth-1)*self._COLOR_DIFF)
909 blue = max(64, blue-(depth-1)*self._COLOR_DIFF)
910
911 return '#%06x' % ((red<<16)+(green<<8)+blue)
912
914 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()]
915 label, depth, width = self._get_html_label(self.module_doc)
916 attribs.append('label=<%s>' % label)
917 return 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
918
919
920
921
922
923
924
926 """
927 Return a `DotGraph` that graphically displays the package
928 hierarchies for the given packages.
929 """
930 if options.get('style', 'uml') == 'uml':
931 if get_dot_version() >= [2]:
932 return uml_package_tree_graph(packages, linker, context,
933 **options)
934 elif 'style' in options:
935 log.warning('UML style package trees require dot version 2.0+')
936
937 graph = DotGraph('Package Tree for %s' % name_list(packages, context),
938 body='ranksep=.3\n;nodesep=.1\n',
939 edge_defaults={'dir':'none'})
940
941
942 if options.get('dir', 'TB') != 'TB':
943 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB')
944
945
946 queue = list(packages)
947 modules = set(packages)
948 for module in queue:
949 queue.extend(module.submodules)
950 modules.update(module.submodules)
951
952
953 nodes = add_valdoc_nodes(graph, modules, linker, context)
954
955
956 for module in modules:
957 for submodule in module.submodules:
958 graph.edges.append(DotGraphEdge(nodes[module], nodes[submodule],
959 headport='tab'))
960
961 return graph
962
964 """
965 Return a `DotGraph` that graphically displays the package
966 hierarchies for the given packages as a nested set of UML
967 symbols.
968 """
969 graph = DotGraph('Package Tree for %s' % name_list(packages, context))
970
971 root_packages = []
972 for package1 in packages:
973 for package2 in packages:
974 if (package1 is not package2 and
975 package2.canonical_name.dominates(package1.canonical_name)):
976 break
977 else:
978 root_packages.append(package1)
979
980 if isinstance(context, VariableDoc) and context.value is not UNKNOWN:
981 context = context.value
982
983 for package in root_packages:
984 graph.nodes.append(DotGraphUmlModuleNode(package, linker, context))
985 return graph
986
987
989 """
990 Return a `DotGraph` that graphically displays the package
991 hierarchies for the given packages.
992 """
993 graph = DotGraph('Class Hierarchy for %s' % name_list(bases, context),
994 body='ranksep=0.3\n',
995 edge_defaults={'sametail':True, 'dir':'none'})
996
997
998 if options.get('dir', 'TB') != 'TB':
999 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB')
1000
1001
1002 classes = set(bases)
1003 queue = list(bases)
1004 for cls in queue:
1005 if cls.subclasses not in (None, UNKNOWN):
1006 queue.extend(cls.subclasses)
1007 classes.update(cls.subclasses)
1008 queue = list(bases)
1009 for cls in queue:
1010 if cls.bases not in (None, UNKNOWN):
1011 queue.extend(cls.bases)
1012 classes.update(cls.bases)
1013
1014
1015 classes = [d for d in classes if isinstance(d, ClassDoc)
1016 if d.pyval is not object]
1017 nodes = add_valdoc_nodes(graph, classes, linker, context)
1018
1019
1020 edges = set()
1021 for cls in classes:
1022 for subcls in cls.subclasses:
1023 if cls in nodes and subcls in nodes:
1024 edges.add((nodes[cls], nodes[subcls]))
1025 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
1026
1027 return graph
1028
1029
1031 """
1032 Return a `DotGraph` that graphically displays the class hierarchy
1033 for the given class, using UML notation. Options:
1034
1035 - max_attributes
1036 - max_operations
1037 - show_private_vars
1038 - show_magic_vars
1039 - link_attributes
1040 """
1041 nodes = {}
1042
1043
1044 for cls in class_doc.mro():
1045 if cls.pyval is object: continue
1046 if cls == class_doc: color = SELECTED_BG
1047 else: color = BASECLASS_BG
1048 nodes[cls] = DotGraphUmlClassNode(cls, linker, context,
1049 show_inherited_vars=False,
1050 collapsed=False, bgcolor=color)
1051
1052
1053 queue = [class_doc]
1054 for cls in queue:
1055 if cls.subclasses not in (None, UNKNOWN):
1056 queue.extend(cls.subclasses)
1057 for cls in cls.subclasses:
1058 if cls not in nodes:
1059 nodes[cls] = DotGraphUmlClassNode(cls, linker, context,
1060 collapsed=True,
1061 bgcolor=SUBCLASS_BG)
1062
1063
1064
1065 mro = class_doc.mro()
1066 for name, var in class_doc.variables.items():
1067 i = mro.index(var.container)
1068 for base in mro[i+1:]:
1069 if base.pyval is object: continue
1070 overridden_var = base.variables.get(name)
1071 if overridden_var and overridden_var.container == base:
1072 try:
1073 if isinstance(overridden_var.value, RoutineDoc):
1074 nodes[base].operations.remove(overridden_var)
1075 else:
1076 nodes[base].attributes.remove(overridden_var)
1077 except ValueError:
1078 pass
1079
1080
1081
1082 inheritance_nodes = set(nodes.values())
1083
1084
1085 if options.get('link_attributes', True):
1086 for node in nodes.values():
1087 node.link_attributes(nodes)
1088
1089
1090 for edge in node.edges:
1091 if edge.end in inheritance_nodes:
1092 edge['constraint'] = 'False'
1093
1094
1095 graph = DotGraph('UML class diagram for %s' % class_doc,
1096 body='ranksep=.2\n;nodesep=.3\n')
1097 graph.nodes = nodes.values()
1098
1099
1100 for node in inheritance_nodes:
1101 for base in node.class_doc.bases:
1102 if base in nodes:
1103 graph.edges.append(DotGraphEdge(nodes[base], node,
1104 dir='back', arrowtail='empty',
1105 headport='body', tailport='body',
1106 color=INH_LINK_COLOR, weight=100,
1107 style='bold'))
1108
1109
1110 return graph
1111
1112
1113 -def import_graph(modules, docindex, linker, context=None, **options):
1114 graph = DotGraph('Import Graph', body='ranksep=.3\n;nodesep=.3\n')
1115
1116
1117 if options.get('dir', 'RL') != 'TB':
1118 graph.body += 'rankdir=%s\n' % options.get('dir', 'RL')
1119
1120
1121 nodes = add_valdoc_nodes(graph, modules, linker, context)
1122
1123
1124 edges = set()
1125 for dst in modules:
1126 if dst.imports in (None, UNKNOWN): continue
1127 for var_name in dst.imports:
1128 for i in range(len(var_name), 0, -1):
1129 val_doc = docindex.get_valdoc(var_name[:i])
1130 if isinstance(val_doc, ModuleDoc):
1131 if val_doc in nodes and dst in nodes:
1132 edges.add((nodes[val_doc], nodes[dst]))
1133 break
1134 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
1135
1136 return graph
1137
1138
1139 -def call_graph(api_docs, docindex, linker, context=None, **options):
1140 """
1141 :param options:
1142 - `dir`: rankdir for the graph. (default=LR)
1143 - `add_callers`: also include callers for any of the
1144 routines in `api_docs`. (default=False)
1145 - `add_callees`: also include callees for any of the
1146 routines in `api_docs`. (default=False)
1147 :todo: Add an `exclude` option?
1148 """
1149 if docindex.callers is None:
1150 log.warning("No profiling information for call graph!")
1151 return DotGraph('Call Graph')
1152
1153 if isinstance(context, VariableDoc):
1154 context = context.value
1155
1156
1157 functions = []
1158 for api_doc in api_docs:
1159
1160 if isinstance(api_doc, VariableDoc):
1161 api_doc = api_doc.value
1162
1163 if isinstance(api_doc, RoutineDoc):
1164 functions.append(api_doc)
1165 elif isinstance(api_doc, NamespaceDoc):
1166 for vardoc in api_doc.variables.values():
1167 if isinstance(vardoc.value, RoutineDoc):
1168 functions.append(vardoc.value)
1169
1170
1171
1172
1173 functions = [f for f in functions if
1174 (f in docindex.callers) or (f in docindex.callees)]
1175
1176
1177 func_set = set(functions)
1178 if options.get('add_callers', False) or options.get('add_callees', False):
1179 for func_doc in functions:
1180 if options.get('add_callers', False):
1181 func_set.update(docindex.callers.get(func_doc, ()))
1182 if options.get('add_callees', False):
1183 func_set.update(docindex.callees.get(func_doc, ()))
1184
1185 graph = DotGraph('Call Graph for %s' % name_list(api_docs, context),
1186 node_defaults={'shape':'box', 'width': 0, 'height': 0})
1187
1188
1189 if options.get('dir', 'LR') != 'TB':
1190 graph.body += 'rankdir=%s\n' % options.get('dir', 'LR')
1191
1192 nodes = add_valdoc_nodes(graph, func_set, linker, context)
1193
1194
1195 edges = set()
1196 for func_doc in functions:
1197 for caller in docindex.callers.get(func_doc, ()):
1198 if caller in nodes:
1199 edges.add( (nodes[caller], nodes[func_doc]) )
1200 for callee in docindex.callees.get(func_doc, ()):
1201 if callee in nodes:
1202 edges.add( (nodes[func_doc], nodes[callee]) )
1203 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
1204
1205 return graph
1206
1207
1208
1209
1210
1211 _dot_version = None
1212 _DOT_VERSION_RE = re.compile(r'dot version ([\d\.]+)')
1228
1229
1230
1231
1232
1245
1246 NOOP_URL = 'javascript: void(0);'
1247 MODULE_NODE_HTML = '''
1248 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"
1249 CELLPADDING="0" PORT="table" ALIGN="LEFT">
1250 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" FIXEDSIZE="true"
1251 BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR>
1252 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1"
1253 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR>
1254 </TABLE>'''.strip()
1255
1257 """
1258 Update the style attributes of `node` to reflext its type
1259 and context.
1260 """
1261
1262 dot_version = get_dot_version()
1263
1264
1265 if isinstance(val_doc, VariableDoc) and val_doc.value is not UNKNOWN:
1266 val_doc = val_doc.value
1267 if isinstance(context, VariableDoc) and context.value is not UNKNOWN:
1268 context = context.value
1269
1270
1271
1272 node['href'] = url or NOOP_URL
1273
1274 if isinstance(val_doc, ModuleDoc) and dot_version >= [2]:
1275 node['shape'] = 'plaintext'
1276 if val_doc == context: color = SELECTED_BG
1277 else: color = MODULE_BG
1278 node['tooltip'] = node['label']
1279 node['html_label'] = MODULE_NODE_HTML % (color, color, url,
1280 val_doc.canonical_name,
1281 node['label'])
1282 node['width'] = node['height'] = 0
1283 node.port = 'body'
1284
1285 elif isinstance(val_doc, RoutineDoc):
1286 node['shape'] = 'box'
1287 node['style'] = 'rounded'
1288 node['width'] = 0
1289 node['height'] = 0
1290 node['label'] = '%s()' % node['label']
1291 node['tooltip'] = node['label']
1292 if val_doc == context:
1293 node['fillcolor'] = SELECTED_BG
1294 node['style'] = 'filled,rounded,bold'
1295
1296 else:
1297 node['shape'] = 'box'
1298 node['width'] = 0
1299 node['height'] = 0
1300 node['tooltip'] = node['label']
1301 if val_doc == context:
1302 node['fillcolor'] = SELECTED_BG
1303 node['style'] = 'filled,bold'
1304
1306 if context is not None:
1307 context = context.canonical_name
1308 names = [str(d.canonical_name.contextualize(context)) for d in api_docs]
1309 if len(names) == 0: return ''
1310 if len(names) == 1: return '%s' % names[0]
1311 elif len(names) == 2: return '%s and %s' % (names[0], names[1])
1312 else:
1313 return '%s, and %s' % (', '.join(names[:-1]), names[-1])
1314