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