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