1# epydoc -- Graph generation
2#
3# Copyright (C) 2005 Edward Loper
4# Author: Edward Loper <edloper@loper.org>
5# URL: <http://epydoc.sf.net>
6#
7# $Id: dotgraph.py 1663 2007-11-07 15:29:47Z dvarrazzo $
8
9"""
10Render 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
23import re
24import sys
25from epydoc import log
26from epydoc.apidoc import *
27from epydoc.util import *
28from epydoc.compat import * # Backwards compatibility
29
30# colors for graphs of APIDocs
31MODULE_BG = '#d8e8ff'
32CLASS_BG = '#d8ffe8'
33SELECTED_BG = '#ffd0d0'
34BASECLASS_BG = '#e0b0a0'
35SUBCLASS_BG = '#e0b0a0'
36ROUTINE_BG = '#e8d0b0' # maybe?
37INH_LINK_COLOR = '#800000'
38
39######################################################################
40#{ Dot Graphs
41######################################################################
42
43DOT_COMMAND = 'dot'
44"""The command that should be used to spawn dot"""
45
46class DotGraph:
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        # Encode the title, if necessary.
118        if isinstance(self.title, unicode):
119            self.title = self.title.encode('ascii', 'xmlcharrefreplace')
120
121        # Make sure the UID isn't too long.
122        self.uid = self.uid[:30]
123
124        # Make sure the UID is unique
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        # If dotversion >1.8.10, then we can generate the image and
140        # the cmapx with a single call to dot.  Otherwise, we need to
141        # run dot twice.
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 '' # failed to render
145        else:
146            if not self.write(image_file):
147                return '' # failed to render
148            cmapx = self.render('cmapx') or ''
149
150        # Decode the cmapx (dot uses utf-8)
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        # Link xrefs in nodes
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        # Link xrefs in edges
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        # Link xrefs in body
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
214    def _link_href(self, attribs, docstring_linker):
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        # Decode into unicode, if necessary.
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
245    def _run_dot(self, *options):
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            #log.debug(self.to_dotfile())
253            return None
254
255        return result
256
257    def to_dotfile(self):
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        # Default dot input encoding is UTF-8
278        return u'\n'.join(lines).encode('utf-8')
279
280class DotGraphNode:
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
292    def __getitem__(self, attr):
293        return self._attribs[attr]
294
295    def __setitem__(self, attr, val):
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
303    def to_dotfile(self):
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
314class DotGraphEdge:
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       #: :type: `DotGraphNode`
324        self.end = end           #: :type: `DotGraphNode`
325        self._attribs = attribs
326
327    def __getitem__(self, attr):
328        return self._attribs[attr]
329
330    def __setitem__(self, attr, val):
331        self._attribs[attr] = val
332
333    def to_dotfile(self):
334        """
335        Return the dot commands that should be used to render this edge.
336        """
337        # Set head & tail ports, if the nodes have preferred ports.
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        # Convert attribs to a string
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        # Return the dotfile edge.
348        return 'node%d -> node%d%s' % (self.start.id, self.end.id, attribs)
349
350######################################################################
351#{ Specialized Nodes for UML Graphs
352######################################################################
353
354class DotGraphUmlClassNode(DotGraphNode):
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        # Initialize operations & attributes lists.
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        # Initialize our dot node settings.
482        tooltip = self._summary(class_doc)
483        if tooltip:
484            # dot chokes on a \n in the attribute...
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    #{ Attribute Linking
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
518    def link_attributes(self, nodes):
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        # Try to convert each attribute var into a graph edge.  If
548        # _link_attribute returns true, then it succeeded, so remove
549        # that var from our attribute list; otherwise, leave that var
550        # in our attribute list.
551        self.attributes = [var for var in self.attributes
552                           if not self._link_attribute(var, nodes)]
553
554    def _link_attribute(self, var, nodes):
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        # Simple type.
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        # Collection type.
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        # Optional type.
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        # Mapping type.
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        # Mapping to collection type.
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        # We were unable to link this attribute.
600        return False
601
602    def _add_attribute_edge(self, var, nodes, type_str, **attribs):
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        # Use the type string to look up a corresponding ValueDoc.
609        type_doc = self.linker.docindex.find(type_str, var)
610        if not type_doc: return False
611
612        # Make sure the type is a class.
613        if not isinstance(type_doc, ClassDoc): return False
614
615        # Get the type ValueDoc's node.  If it doesn't have one (and
616        # add_nodes_for_linked_attributes=True), then create it.
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        # Add an edge from self to the target type node.
627        # [xx] should I set constraint=false here?
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    #{ Helper Methods
639    #/////////////////////////////////////////////////////////////////
640    def _summary(self, api_doc):
641        """Return a plaintext summary for `api_doc`"""
642        if not isinstance(api_doc, APIDoc): return ''
643        if api_doc.summary in (None, UNKNOWN): return ''
644        summary = api_doc.summary.to_plaintext(None).strip()
645        return plaintext_to_html(summary)
646
647    _summary = classmethod(_summary)
648
649    def _type_descr(self, api_doc):
650        """Return a plaintext type description for `api_doc`"""
651        if not hasattr(api_doc, 'type_descr'): return ''
652        if api_doc.type_descr in (None, UNKNOWN): return ''
653        type_descr = api_doc.type_descr.to_plaintext(self.linker).strip()
654        return plaintext_to_html(type_descr)
655
656    def _tooltip(self, var_doc):
657        """Return a tooltip for `var_doc`."""
658        return (self._summary(var_doc) or
659                self._summary(var_doc.value) or
660                var_doc.canonical_name)
661
662    #/////////////////////////////////////////////////////////////////
663    #{ Rendering
664    #/////////////////////////////////////////////////////////////////
665
666    def _attribute_cell(self, var_doc):
667        # Construct the label
668        label = var_doc.name
669        type_descr = (self._type_descr(var_doc) or
670                      self._type_descr(var_doc.value))
671        if type_descr: label += ': %s' % type_descr
672        # Get the URL
673        url = self.linker.url_for(var_doc) or NOOP_URL
674        # Construct & return the pseudo-html code
675        return self._ATTRIBUTE_CELL % (url, self._tooltip(var_doc), label)
676
677    def _operation_cell(self, var_doc):
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               &nbsp;.
683        :todo: Optionally add return type info?
684        """
685        # Construct the label (aka function signature)
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        # Get the URL
694        url = self.linker.url_for(var_doc) or NOOP_URL
695        # Construct & return the pseudo-html code
696        return self._OPERATION_CELL % (url, self._tooltip(var_doc), label)
697
698    def _operation_arg(self, name, default, func_doc):
699        """
700        :todo: Handle tuple args better
701        :todo: Optionally add type info?
702        """
703        if default is None:
704            return '%s' % name
705        else:
706            pyval_repr = default.summary_pyval_repr().to_plaintext(None)
707            return '%s=%s' % (name, pyval_repr)
708
709    def _qualifier_cell(self, key_label, port):
710        return self._QUALIFIER_CELL  % (port, self.bgcolor, key_label)
711
712    #: args: (url, tooltip, label)
713    _ATTRIBUTE_CELL = '''
714    <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR>
715    '''
716
717    #: args: (url, tooltip, label)
718    _OPERATION_CELL = '''
719    <TR><TD ALIGN="LEFT" HREF="%s" TOOLTIP="%s">%s</TD></TR>
720    '''
721
722    #: args: (port, bgcolor, label)
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    #: Args: (rowspan, bgcolor, classname, attributes, operations, qualifiers)
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
752    def _get_html_label(self):
753        # Get the class name & contextualize it.
754        classname = self.class_doc.canonical_name
755        classname = classname.contextualize(self.context.canonical_name)
756
757        # If we're collapsed, display the node as a single box.
758        if self.collapsed:
759            return self._COLLAPSED_LABEL % (self.bgcolor, classname)
760
761        # Construct the attribute list.  (If it's too long, truncate)
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        # Construct the operation list.  (If it's too long, truncate)
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        # Construct the qualifier list & determine the rowspan.
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        # Put it all together.
790        return self._LABEL % (rowspan, self.bgcolor, classname,
791                              attributes, operations, qualifiers)
792
793    def to_dotfile(self):
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
802class DotGraphUmlModuleNode(DotGraphNode):
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    #: Expects: (color, color, url, tooltip, body)
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    #: Expects: (name, body_rows)
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    #: Expects: (cells,)
856    _NESTED_BODY_ROW = '''
857    <TR><TD>
858      <TABLE BORDER="0" CELLBORDER="0"><TR>%s</TR></TABLE>
859    </TD></TR>'''
860
861    def _get_html_label(self, package):
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 # unit is roughly characters.
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        # Get the label for each submodule, and divide them into rows.
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            # Get the submodule's label.
889            label, depth, width = self._get_html_label(submodule)
890            # Check if we should start a new row.
891            if row_width > 0 and width+row_width > MAX_ROW_WIDTH:
892                row_list.append('')
893                row_width = 0
894            # Add the submodule's label to the row.
895            row_width += width
896            row_list[-1] += '<TD ALIGN="LEFT">%s</TD>' % label
897            # Update our max's.
898            max_depth = max(depth, max_depth)
899            max_row_width = max(row_width, max_row_width)
900
901        # Figure out which color to use.
902        pkg_color = self._color(package, depth+1)
903
904        # Assemble & return the label.
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            # Parse the base color.
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            # Make it darker with each level of depth. (but not *too*
924            # dark -- package name needs to be readable)
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            # Convert it back to a color string
929            return '#%06x' % ((red<<16)+(green<<8)+blue)
930
931    def to_dotfile(self):
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#{ Graph Generation Functions
941######################################################################
942
943def package_tree_graph(packages, linker, context=None, **options):
944    """
945    Return a `DotGraph` that graphically displays the package
946    hierarchies for the given packages.
947    """
948    if options.get('style', 'uml') == 'uml': # default to uml style?
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    # Options
960    if options.get('dir', 'TB') != 'TB': # default: top-to-bottom
961        graph.body += 'rankdir=%s\n' % options.get('dir', 'TB')
962
963    # Get a list of all modules in the package.
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    # Add a node for each module.
971    nodes = add_valdoc_nodes(graph, modules, linker, context)
972
973    # Add an edge for each package/submodule relationship.
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
981def uml_package_tree_graph(packages, linker, context=None, **options):
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    # Remove any packages whose containers are also in the list.
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    # If the context is a variable, then get its value.
998    if isinstance(context, VariableDoc) and context.value is not UNKNOWN:
999        context = context.value
1000    # Return a graph with one node for each root package.
1001    for package in root_packages:
1002        graph.nodes.append(DotGraphUmlModuleNode(package, linker, context))
1003    return graph
1004
1005######################################################################
1006def class_tree_graph(bases, linker, context=None, **options):
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    # Options
1022    if options.get('dir', 'TB') != 'TB': # default: top-down
1023        graph.body += 'rankdir=%s\n' % options.get('dir', 'TB')
1024    exclude = options.get('exclude', ())
1025
1026    # Find all superclasses & subclasses of the given classes.
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    # Add a node for each cls.
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    # Add an edge for each package/subclass relationship.
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######################################################################
1063def uml_class_tree_graph(class_doc, linker, context=None, **options):
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 = {} # ClassDoc -> DotGraphUmlClassNode
1075    exclude = options.get('exclude', ())
1076
1077    # Create nodes for class_doc and all its bases.
1078    for cls in class_doc.mro():
1079        if cls.pyval is object: continue # don't include `object`.
1080        if cls in exclude: break # stop if we get to an excluded class.
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    # Create nodes for all class_doc's subclasses.
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    # Only show variables in the class where they're defined for
1101    # *class_doc*.
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 # don't include `object`.
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 # var is filtered (eg private or magic)
1116
1117    # Keep track of which nodes are part of the inheritance graph
1118    # (since link_attributes might add new nodes)
1119    inheritance_nodes = set(nodes.values())
1120
1121    # Turn attributes into links.
1122    if options.get('link_attributes', True):
1123        for node in nodes.values():
1124            node.link_attributes(nodes)
1125            # Make sure that none of the new attribute edges break the
1126            # rank ordering assigned by inheritance.
1127            for edge in node.edges:
1128                if edge.end in inheritance_nodes:
1129                    edge['constraint'] = 'False'
1130
1131    # Construct the graph.
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    # Add inheritance edges.
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    # And we're done!
1147    return graph
1148
1149######################################################################
1150def import_graph(modules, docindex, linker, context=None, **options):
1151    graph = DotGraph('Import Graph', body='ranksep=.3\n;nodesep=.3\n')
1152
1153    # Options
1154    if options.get('dir', 'RL') != 'TB': # default: right-to-left.
1155        graph.body += 'rankdir=%s\n' % options.get('dir', 'RL')
1156
1157    # Add a node for each module.
1158    nodes = add_valdoc_nodes(graph, modules, linker, context)
1159
1160    # Edges.
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######################################################################
1176def 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') # return None instead?
1189
1190    if isinstance(context, VariableDoc):
1191        context = context.value
1192
1193    # Get the set of requested functions.
1194    functions = []
1195    for api_doc in api_docs:
1196        # If it's a variable, get its value.
1197        if isinstance(api_doc, VariableDoc):
1198            api_doc = api_doc.value
1199        # Add the value to the functions list.
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    # Filter out functions with no callers/callees?
1208    # [xx] this isnt' quite right, esp if add_callers or add_callees
1209    # options are fales.
1210    functions = [f for f in functions if
1211                 (f in docindex.callers) or (f in docindex.callees)]
1212
1213    # Add any callers/callees of the selected functions
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    # Options
1226    if options.get('dir', 'LR') != 'TB': # default: left-to-right
1227        graph.body += 'rankdir=%s\n' % options.get('dir', 'LR')
1228
1229    nodes = add_valdoc_nodes(graph, func_set, linker, context)
1230
1231    # Find the edges.
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#{ Dot Version
1246######################################################################
1247
1248_dot_version = None
1249_DOT_VERSION_RE = re.compile(r'dot version ([\d\.]+)')
1250def get_dot_version():
1251    global _dot_version
1252    if _dot_version is None:
1253        try:
1254            out, err = run_subprocess([DOT_COMMAND, '-V'])
1255            version_info = err or out
1256            m = _DOT_VERSION_RE.match(version_info)
1257            if m:
1258                _dot_version = [int(x) for x in m.group(1).split('.')]
1259            else:
1260                _dot_version = (0,)
1261        except OSError, e:
1262            _dot_version = (0,)
1263        log.info('Detected dot version %s' % _dot_version)
1264    return _dot_version
1265
1266######################################################################
1267#{ Helper Functions
1268######################################################################
1269
1270def add_valdoc_nodes(graph, val_docs, linker, context):
1271    """
1272    :todo: Use different node styles for different subclasses of APIDoc
1273    """
1274    nodes = {}
1275    val_docs = sorted(val_docs, key=lambda d:d.canonical_name)
1276    for i, val_doc in enumerate(val_docs):
1277        label = val_doc.canonical_name.contextualize(context.canonical_name)
1278        node = nodes[val_doc] = DotGraphNode(label)
1279        graph.nodes.append(node)
1280        specialize_valdoc_node(node, val_doc, context, linker.url_for(val_doc))
1281    return nodes
1282
1283NOOP_URL = 'javascript:void(0);'
1284MODULE_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
1293def specialize_valdoc_node(node, val_doc, context, url):
1294    """
1295    Update the style attributes of `node` to reflext its type
1296    and context.
1297    """
1298    # We can only use html-style nodes if dot_version>2.
1299    dot_version = get_dot_version()
1300
1301    # If val_doc or context is a variable, get its value.
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    # Set the URL.  (Do this even if it points to the page we're
1308    # currently on; otherwise, the tooltip is ignored.)
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
1342def name_list(api_docs, context=None):
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
1352