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 name, var in class_doc.variables.iteritems():
471 if ((not show_private and var.is_public == False) or
472 (not show_magic and re.match('__\w+__$', name)) or
473 (not show_inherited and var.container != class_doc)):
474 pass
475 elif isinstance(var.value, RoutineDoc):
476 self.operations.append(var)
477 else:
478 self.attributes.append(var)
479
480
481 tooltip = self._summary(class_doc)
482 if tooltip:
483
484 tooltip = " ".join(tooltip.split())
485 else:
486 tooltip = class_doc.canonical_name
487 DotGraphNode.__init__(self, tooltip=tooltip,
488 width=0, height=0, shape='plaintext',
489 href=linker.url_for(class_doc) or NOOP_URL)
490
491
492
493
494
495 SIMPLE_TYPE_RE = re.compile(
496 r'^([\w\.]+)$')
497 """A regular expression that matches descriptions of simple types."""
498
499 COLLECTION_TYPE_RE = re.compile(
500 r'^(list|set|sequence|tuple|collection) of ([\w\.]+)$')
501 """A regular expression that matches descriptions of collection types."""
502
503 MAPPING_TYPE_RE = re.compile(
504 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to ([\w\.]+)$')
505 """A regular expression that matches descriptions of mapping types."""
506
507 MAPPING_TO_COLLECTION_TYPE_RE = re.compile(
508 r'^(dict|dictionary|map|mapping) from ([\w\.]+) to '
509 r'(list|set|sequence|tuple|collection) of ([\w\.]+)$')
510 """A regular expression that matches descriptions of mapping types
511 whose value type is a collection."""
512
513 OPTIONAL_TYPE_RE = re.compile(
514 r'^(None or|optional) ([\w\.]+)$|^([\w\.]+) or None$')
515 """A regular expression that matches descriptions of optional types."""
516
518 """
519 Convert any attributes with type descriptions corresponding to
520 documented classes to edges. The following type descriptions
521 are currently handled:
522
523 - Dotted names: Create an attribute edge to the named type,
524 labelled with the variable name.
525 - Collections: Create an attribute edge to the named type,
526 labelled with the variable name, and marked with '*' at the
527 type end of the edge.
528 - Mappings: Create an attribute edge to the named type,
529 labelled with the variable name, connected to the class by
530 a qualifier box that contains the key type description.
531 - Optional: Create an attribute edge to the named type,
532 labelled with the variable name, and marked with '0..1' at
533 the type end of the edge.
534
535 The edges created by `link_attributes()` are handled internally
536 by `DotGraphUmlClassNode`; they should *not* be added directly
537 to the `DotGraph`.
538
539 :param nodes: A dictionary mapping from `ClassDoc`\s to
540 `DotGraphUmlClassNode`\s, used to look up the nodes for
541 attribute types. If the ``add_nodes_for_linked_attributes``
542 option is used, then new nodes will be added to this
543 dictionary for any types that are not already listed.
544 These added nodes must be added to the `DotGraph`.
545 """
546
547
548
549
550 self.attributes = [var for var in self.attributes
551 if not self._link_attribute(var, nodes)]
552
554 """
555 Helper for `link_attributes()`: try to convert the attribute
556 variable `var` into an edge, and add that edge to
557 `self.edges`. Return ``True`` iff the variable was
558 successfully converted to an edge (in which case, it should be
559 removed from the attributes list).
560 """
561 type_descr = self._type_descr(var) or self._type_descr(var.value)
562
563
564 m = self.SIMPLE_TYPE_RE.match(type_descr)
565 if m and self._add_attribute_edge(var, nodes, m.group(1)):
566 return True
567
568
569 m = self.COLLECTION_TYPE_RE.match(type_descr)
570 if m and self._add_attribute_edge(var, nodes, m.group(2),
571 headlabel='*'):
572 return True
573
574
575 m = self.OPTIONAL_TYPE_RE.match(type_descr)
576 if m and self._add_attribute_edge(var, nodes, m.group(2) or m.group(3),
577 headlabel='0..1'):
578 return True
579
580
581 m = self.MAPPING_TYPE_RE.match(type_descr)
582 if m:
583 port = 'qualifier_%s' % var.name
584 if self._add_attribute_edge(var, nodes, m.group(3),
585 tailport='%s:e' % port):
586 self.qualifiers.append( (m.group(2), port) )
587 return True
588
589
590 m = self.MAPPING_TO_COLLECTION_TYPE_RE.match(type_descr)
591 if m:
592 port = 'qualifier_%s' % var.name
593 if self._add_attribute_edge(var, nodes, m.group(4), headlabel='*',
594 tailport='%s:e' % port):
595 self.qualifiers.append( (m.group(2), port) )
596 return True
597
598
599 return False
600
602 """
603 Helper for `link_attributes()`: try to add an edge for the
604 given attribute variable `var`. Return ``True`` if
605 successful.
606 """
607
608 type_doc = self.linker.docindex.find(type_str, var)
609 if not type_doc: return False
610
611
612 if not isinstance(type_doc, ClassDoc): return False
613
614
615
616 type_node = nodes.get(type_doc)
617 if not type_node:
618 if self.options.get('add_nodes_for_linked_attributes', True):
619 type_node = DotGraphUmlClassNode(type_doc, self.linker,
620 self.context, collapsed=True)
621 nodes[type_doc] = type_node
622 else:
623 return False
624
625
626
627 attribs.setdefault('headport', 'body')
628 attribs.setdefault('tailport', 'body')
629 url = self.linker.url_for(var) or NOOP_URL
630 self.edges.append(DotGraphEdge(self, type_node, label=var.name,
631 arrowhead='open', href=url,
632 tooltip=var.canonical_name, labeldistance=1.5,
633 **attribs))
634 return True
635
636
637
638
645
646 _summary = classmethod(_summary)
647
654
660
661
662
663
664
675
677 """
678 :todo: do 'word wrapping' on the signature, by starting a new
679 row in the table, if necessary. How to indent the new
680 line? Maybe use align=right? I don't think dot has a
681 .
682 :todo: Optionally add return type info?
683 """
684
685 func_doc = var_doc.value
686 args = [self._operation_arg(n, d, func_doc) for (n, d)
687 in zip(func_doc.posargs, func_doc.posarg_defaults)]
688 args = [plaintext_to_html(arg) for arg in args]
689 if func_doc.vararg: args.append('*'+func_doc.vararg)
690 if func_doc.kwarg: args.append('**'+func_doc.kwarg)
691 label = '%s(%s)' % (var_doc.name, ', '.join(args))
692
693 url = self.linker.url_for(var_doc) or NOOP_URL
694
695 return self._OPERATION_CELL % (url, self._tooltip(var_doc), label)
696
707
710
711
712 _ATTRIBUTE_CELL = '''
713 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR>
714 '''
715
716
717 _OPERATION_CELL = '''
718 <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR>
719 '''
720
721
722 _QUALIFIER_CELL = '''
723 <TR><TD VALIGN="BOTTOM" PORT="%s" BGCOLOR="%s" BORDER="1">%s</TD></TR>
724 '''
725
726 _QUALIFIER_DIV = '''
727 <TR><TD VALIGN="BOTTOM" HEIGHT="10" WIDTH="10" FIXEDSIZE="TRUE"></TD></TR>
728 '''
729
730
731 _LABEL = '''
732 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0">
733 <TR><TD ROWSPAN="%s">
734 <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"
735 CELLPADDING="0" PORT="body" BGCOLOR="%s">
736 <TR><TD>%s</TD></TR>
737 <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
738 %s</TABLE></TD></TR>
739 <TR><TD><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
740 %s</TABLE></TD></TR>
741 </TABLE>
742 </TD></TR>
743 %s
744 </TABLE>'''
745
746 _COLLAPSED_LABEL = '''
747 <TABLE CELLBORDER="0" BGCOLOR="%s" PORT="body">
748 <TR><TD>%s</TD></TR>
749 </TABLE>'''
750
752
753 classname = self.class_doc.canonical_name
754 classname = classname.contextualize(self.context.canonical_name)
755
756
757 if self.collapsed:
758 return self._COLLAPSED_LABEL % (self.bgcolor, classname)
759
760
761 attrib_cells = [self._attribute_cell(a) for a in self.attributes]
762 max_attributes = self.options.get('max_attributes', 15)
763 if len(attrib_cells) == 0:
764 attrib_cells = ['<TR><TD></TD></TR>']
765 elif len(attrib_cells) > max_attributes:
766 attrib_cells[max_attributes-2:-1] = ['<TR><TD>...</TD></TR>']
767 attributes = ''.join(attrib_cells)
768
769
770 oper_cells = [self._operation_cell(a) for a in self.operations]
771 max_operations = self.options.get('max_operations', 15)
772 if len(oper_cells) == 0:
773 oper_cells = ['<TR><TD></TD></TR>']
774 elif len(oper_cells) > max_operations:
775 oper_cells[max_operations-2:-1] = ['<TR><TD>...</TD></TR>']
776 operations = ''.join(oper_cells)
777
778
779 if self.qualifiers:
780 rowspan = len(self.qualifiers)*2+2
781 div = self._QUALIFIER_DIV
782 qualifiers = div+div.join([self._qualifier_cell(l,p) for
783 (l,p) in self.qualifiers])+div
784 else:
785 rowspan = 1
786 qualifiers = ''
787
788
789 return self._LABEL % (rowspan, self.bgcolor, classname,
790 attributes, operations, qualifiers)
791
793 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()]
794 attribs.append('label=<%s>' % self._get_html_label())
795 s = 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
796 if not self.collapsed:
797 for edge in self.edges:
798 s += '\n' + edge.to_dotfile()
799 return s
800
802 """
803 A specialized dot grah node used to display `ModuleDoc`\s using
804 UML notation. Simple module nodes look like::
805
806 .----.
807 +------------+
808 | modulename |
809 +------------+
810
811 Packages nodes are drawn with their modules & subpackages nested
812 inside::
813
814 .----.
815 +----------------------------------------+
816 | packagename |
817 | |
818 | .----. .----. .----. |
819 | +---------+ +---------+ +---------+ |
820 | | module1 | | module2 | | module3 | |
821 | +---------+ +---------+ +---------+ |
822 | |
823 +----------------------------------------+
824
825 """
826 - def __init__(self, module_doc, linker, context, collapsed=False,
827 excluded_submodules=(), **options):
828 self.module_doc = module_doc
829 self.linker = linker
830 self.context = context
831 self.collapsed = collapsed
832 self.options = options
833 self.excluded_submodules = excluded_submodules
834 DotGraphNode.__init__(self, shape='plaintext',
835 href=linker.url_for(module_doc) or NOOP_URL,
836 tooltip=module_doc.canonical_name)
837
838
839 _MODULE_LABEL = '''
840 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" ALIGN="LEFT">
841 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16"
842 FIXEDSIZE="true" BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR>
843 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" WIDTH="20"
844 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR>
845 </TABLE>'''
846
847
848 _NESTED_BODY = '''
849 <TABLE BORDER="0" CELLBORDER="0" CELLPADDING="0" CELLSPACING="0">
850 <TR><TD ALIGN="LEFT">%s</TD></TR>
851 %s
852 </TABLE>'''
853
854
855 _NESTED_BODY_ROW = '''
856 <TR><TD>
857 <TABLE BORDER="0" CELLBORDER="0"><TR>%s</TR></TABLE>
858 </TD></TR>'''
859
861 """
862 :Return: (label, depth, width) where:
863
864 - ``label`` is the HTML label
865 - ``depth`` is the depth of the package tree (for coloring)
866 - ``width`` is the max width of the HTML label, roughly in
867 units of characters.
868 """
869 MAX_ROW_WIDTH = 80
870 pkg_name = package.canonical_name
871 pkg_url = self.linker.url_for(package) or NOOP_URL
872
873 if (not package.is_package or len(package.submodules) == 0 or
874 self.collapsed):
875 pkg_color = self._color(package, 1)
876 label = self._MODULE_LABEL % (pkg_color, pkg_color,
877 pkg_url, pkg_name, pkg_name[-1])
878 return (label, 1, len(pkg_name[-1])+3)
879
880
881 row_list = ['']
882 row_width = 0
883 max_depth = 0
884 max_row_width = len(pkg_name[-1])+3
885 for submodule in package.submodules:
886 if submodule in self.excluded_submodules: continue
887
888 label, depth, width = self._get_html_label(submodule)
889
890 if row_width > 0 and width+row_width > MAX_ROW_WIDTH:
891 row_list.append('')
892 row_width = 0
893
894 row_width += width
895 row_list[-1] += '<TD ALIGN="LEFT">%s</TD>' % label
896
897 max_depth = max(depth, max_depth)
898 max_row_width = max(row_width, max_row_width)
899
900
901 pkg_color = self._color(package, depth+1)
902
903
904 rows = ''.join([self._NESTED_BODY_ROW % r for r in row_list])
905 body = self._NESTED_BODY % (pkg_name, rows)
906 label = self._MODULE_LABEL % (pkg_color, pkg_color,
907 pkg_url, pkg_name, body)
908 return label, max_depth+1, max_row_width
909
910 _COLOR_DIFF = 24
911 - def _color(self, package, depth):
912 if package == self.context: return SELECTED_BG
913 else:
914
915 if re.match(MODULE_BG, 'r#[0-9a-fA-F]{6}$'):
916 base = int(MODULE_BG[1:], 16)
917 else:
918 base = int('d8e8ff', 16)
919 red = (base & 0xff0000) >> 16
920 green = (base & 0x00ff00) >> 8
921 blue = (base & 0x0000ff)
922
923
924 red = max(64, red-(depth-1)*self._COLOR_DIFF)
925 green = max(64, green-(depth-1)*self._COLOR_DIFF)
926 blue = max(64, blue-(depth-1)*self._COLOR_DIFF)
927
928 return '#%06x' % ((red<<16)+(green<<8)+blue)
929
931 attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()]
932 label, depth, width = self._get_html_label(self.module_doc)
933 attribs.append('label=<%s>' % label)
934 return 'node%d%s' % (self.id, ' [%s]' % (','.join(attribs)))
935
936
937
938
939
940
941
943 """
944 Return a `DotGraph` that graphically displays the package
945 hierarchies for the given packages.
946 """
947 if options.get('style', 'uml') == 'uml':
948 if get_dot_version() >= [2]:
949 return uml_package_tree_graph(packages, linker, context,
950 **options)
951 elif 'style' in options:
952 log.warning('UML style package trees require dot version 2.0+')
953
954 graph = DotGraph('Package Tree for %s' % name_list(packages, context),
955 body='ranksep=.3\n;nodesep=.1\n',
956 edge_defaults={'dir':'none'})
957
958
959 if options.get('dir', 'TB') != 'TB':
960 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB')
961
962
963 queue = list(packages)
964 modules = set(packages)
965 for module in queue:
966 queue.extend(module.submodules)
967 modules.update(module.submodules)
968
969
970 nodes = add_valdoc_nodes(graph, modules, linker, context)
971
972
973 for module in modules:
974 for submodule in module.submodules:
975 graph.edges.append(DotGraphEdge(nodes[module], nodes[submodule],
976 headport='tab'))
977
978 return graph
979
981 """
982 Return a `DotGraph` that graphically displays the package
983 hierarchies for the given packages as a nested set of UML
984 symbols.
985 """
986 graph = DotGraph('Package Tree for %s' % name_list(packages, context))
987
988 root_packages = []
989 for package1 in packages:
990 for package2 in packages:
991 if (package1 is not package2 and
992 package2.canonical_name.dominates(package1.canonical_name)):
993 break
994 else:
995 root_packages.append(package1)
996
997 if isinstance(context, VariableDoc) and context.value is not UNKNOWN:
998 context = context.value
999
1000 for package in root_packages:
1001 graph.nodes.append(DotGraphUmlModuleNode(package, linker, context))
1002 return graph
1003
1004
1006 """
1007 Return a `DotGraph` that graphically displays the package
1008 hierarchies for the given packages.
1009 """
1010 graph = DotGraph('Class Hierarchy for %s' % name_list(bases, context),
1011 body='ranksep=0.3\n',
1012 edge_defaults={'sametail':True, 'dir':'none'})
1013
1014
1015 if options.get('dir', 'TB') != 'TB':
1016 graph.body += 'rankdir=%s\n' % options.get('dir', 'TB')
1017
1018
1019 classes = set(bases)
1020 queue = list(bases)
1021 for cls in queue:
1022 if isinstance(cls, ClassDoc):
1023 if cls.subclasses not in (None, UNKNOWN):
1024 queue.extend(cls.subclasses)
1025 classes.update(cls.subclasses)
1026 queue = list(bases)
1027 for cls in queue:
1028 if isinstance(cls, ClassDoc):
1029 if cls.bases not in (None, UNKNOWN):
1030 queue.extend(cls.bases)
1031 classes.update(cls.bases)
1032
1033
1034 classes = [d for d in classes if isinstance(d, ClassDoc)
1035 if d.pyval is not object]
1036 nodes = add_valdoc_nodes(graph, classes, linker, context)
1037
1038
1039 edges = set()
1040 for cls in classes:
1041 for subcls in cls.subclasses:
1042 if cls in nodes and subcls in nodes:
1043 edges.add((nodes[cls], nodes[subcls]))
1044 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
1045
1046 return graph
1047
1048
1050 """
1051 Return a `DotGraph` that graphically displays the class hierarchy
1052 for the given class, using UML notation. Options:
1053
1054 - max_attributes
1055 - max_operations
1056 - show_private_vars
1057 - show_magic_vars
1058 - link_attributes
1059 """
1060 nodes = {}
1061
1062
1063 for cls in class_doc.mro():
1064 if cls.pyval is object: continue
1065 if cls == class_doc: color = SELECTED_BG
1066 else: color = BASECLASS_BG
1067 nodes[cls] = DotGraphUmlClassNode(cls, linker, context,
1068 show_inherited_vars=False,
1069 collapsed=False, bgcolor=color)
1070
1071
1072 queue = [class_doc]
1073 for cls in queue:
1074 if (isinstance(cls, ClassDoc) and
1075 cls.subclasses not in (None, UNKNOWN)):
1076 queue.extend(cls.subclasses)
1077 for cls in cls.subclasses:
1078 if cls not in nodes:
1079 nodes[cls] = DotGraphUmlClassNode(cls, linker, context,
1080 collapsed=True,
1081 bgcolor=SUBCLASS_BG)
1082
1083
1084
1085 mro = class_doc.mro()
1086 for name, var in class_doc.variables.items():
1087 i = mro.index(var.container)
1088 for base in mro[i+1:]:
1089 if base.pyval is object: continue
1090 overridden_var = base.variables.get(name)
1091 if overridden_var and overridden_var.container == base:
1092 try:
1093 if isinstance(overridden_var.value, RoutineDoc):
1094 nodes[base].operations.remove(overridden_var)
1095 else:
1096 nodes[base].attributes.remove(overridden_var)
1097 except ValueError:
1098 pass
1099
1100
1101
1102 inheritance_nodes = set(nodes.values())
1103
1104
1105 if options.get('link_attributes', True):
1106 for node in nodes.values():
1107 node.link_attributes(nodes)
1108
1109
1110 for edge in node.edges:
1111 if edge.end in inheritance_nodes:
1112 edge['constraint'] = 'False'
1113
1114
1115 graph = DotGraph('UML class diagram for %s' % class_doc.canonical_name,
1116 body='ranksep=.2\n;nodesep=.3\n')
1117 graph.nodes = nodes.values()
1118
1119
1120 for node in inheritance_nodes:
1121 for base in node.class_doc.bases:
1122 if base in nodes:
1123 graph.edges.append(DotGraphEdge(nodes[base], node,
1124 dir='back', arrowtail='empty',
1125 headport='body', tailport='body',
1126 color=INH_LINK_COLOR, weight=100,
1127 style='bold'))
1128
1129
1130 return graph
1131
1132
1133 -def import_graph(modules, docindex, linker, context=None, **options):
1134 graph = DotGraph('Import Graph', body='ranksep=.3\n;nodesep=.3\n')
1135
1136
1137 if options.get('dir', 'RL') != 'TB':
1138 graph.body += 'rankdir=%s\n' % options.get('dir', 'RL')
1139
1140
1141 nodes = add_valdoc_nodes(graph, modules, linker, context)
1142
1143
1144 edges = set()
1145 for dst in modules:
1146 if dst.imports in (None, UNKNOWN): continue
1147 for var_name in dst.imports:
1148 for i in range(len(var_name), 0, -1):
1149 val_doc = docindex.get_valdoc(var_name[:i])
1150 if isinstance(val_doc, ModuleDoc):
1151 if val_doc in nodes and dst in nodes:
1152 edges.add((nodes[val_doc], nodes[dst]))
1153 break
1154 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
1155
1156 return graph
1157
1158
1159 -def call_graph(api_docs, docindex, linker, context=None, **options):
1160 """
1161 :param options:
1162 - ``dir``: rankdir for the graph. (default=LR)
1163 - ``add_callers``: also include callers for any of the
1164 routines in ``api_docs``. (default=False)
1165 - ``add_callees``: also include callees for any of the
1166 routines in ``api_docs``. (default=False)
1167 :todo: Add an ``exclude`` option?
1168 """
1169 if docindex.callers is None:
1170 log.warning("No profiling information for call graph!")
1171 return DotGraph('Call Graph')
1172
1173 if isinstance(context, VariableDoc):
1174 context = context.value
1175
1176
1177 functions = []
1178 for api_doc in api_docs:
1179
1180 if isinstance(api_doc, VariableDoc):
1181 api_doc = api_doc.value
1182
1183 if isinstance(api_doc, RoutineDoc):
1184 functions.append(api_doc)
1185 elif isinstance(api_doc, NamespaceDoc):
1186 for vardoc in api_doc.variables.values():
1187 if isinstance(vardoc.value, RoutineDoc):
1188 functions.append(vardoc.value)
1189
1190
1191
1192
1193 functions = [f for f in functions if
1194 (f in docindex.callers) or (f in docindex.callees)]
1195
1196
1197 func_set = set(functions)
1198 if options.get('add_callers', False) or options.get('add_callees', False):
1199 for func_doc in functions:
1200 if options.get('add_callers', False):
1201 func_set.update(docindex.callers.get(func_doc, ()))
1202 if options.get('add_callees', False):
1203 func_set.update(docindex.callees.get(func_doc, ()))
1204
1205 graph = DotGraph('Call Graph for %s' % name_list(api_docs, context),
1206 node_defaults={'shape':'box', 'width': 0, 'height': 0})
1207
1208
1209 if options.get('dir', 'LR') != 'TB':
1210 graph.body += 'rankdir=%s\n' % options.get('dir', 'LR')
1211
1212 nodes = add_valdoc_nodes(graph, func_set, linker, context)
1213
1214
1215 edges = set()
1216 for func_doc in functions:
1217 for caller in docindex.callers.get(func_doc, ()):
1218 if caller in nodes:
1219 edges.add( (nodes[caller], nodes[func_doc]) )
1220 for callee in docindex.callees.get(func_doc, ()):
1221 if callee in nodes:
1222 edges.add( (nodes[func_doc], nodes[callee]) )
1223 graph.edges = [DotGraphEdge(src,dst) for (src,dst) in edges]
1224
1225 return graph
1226
1227
1228
1229
1230
1231 _dot_version = None
1232 _DOT_VERSION_RE = re.compile(r'dot version ([\d\.]+)')
1248
1249
1250
1251
1252
1265
1266 NOOP_URL = 'javascript: void(0);'
1267 MODULE_NODE_HTML = '''
1268 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"
1269 CELLPADDING="0" PORT="table" ALIGN="LEFT">
1270 <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" FIXEDSIZE="true"
1271 BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR>
1272 <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1"
1273 PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR>
1274 </TABLE>'''.strip()
1275
1277 """
1278 Update the style attributes of `node` to reflext its type
1279 and context.
1280 """
1281
1282 dot_version = get_dot_version()
1283
1284
1285 if isinstance(val_doc, VariableDoc) and val_doc.value is not UNKNOWN:
1286 val_doc = val_doc.value
1287 if isinstance(context, VariableDoc) and context.value is not UNKNOWN:
1288 context = context.value
1289
1290
1291
1292 node['href'] = url or NOOP_URL
1293
1294 if isinstance(val_doc, ModuleDoc) and dot_version >= [2]:
1295 node['shape'] = 'plaintext'
1296 if val_doc == context: color = SELECTED_BG
1297 else: color = MODULE_BG
1298 node['tooltip'] = node['label']
1299 node['html_label'] = MODULE_NODE_HTML % (color, color, url,
1300 val_doc.canonical_name,
1301 node['label'])
1302 node['width'] = node['height'] = 0
1303 node.port = 'body'
1304
1305 elif isinstance(val_doc, RoutineDoc):
1306 node['shape'] = 'box'
1307 node['style'] = 'rounded'
1308 node['width'] = 0
1309 node['height'] = 0
1310 node['label'] = '%s()' % node['label']
1311 node['tooltip'] = node['label']
1312 if val_doc == context:
1313 node['fillcolor'] = SELECTED_BG
1314 node['style'] = 'filled,rounded,bold'
1315
1316 else:
1317 node['shape'] = 'box'
1318 node['width'] = 0
1319 node['height'] = 0
1320 node['tooltip'] = node['label']
1321 if val_doc == context:
1322 node['fillcolor'] = SELECTED_BG
1323 node['style'] = 'filled,bold'
1324
1326 if context is not None:
1327 context = context.canonical_name
1328 names = [str(d.canonical_name.contextualize(context)) for d in api_docs]
1329 if len(names) == 0: return ''
1330 if len(names) == 1: return '%s' % names[0]
1331 elif len(names) == 2: return '%s and %s' % (names[0], names[1])
1332 else:
1333 return '%s, and %s' % (', '.join(names[:-1]), names[-1])
1334