• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2"""
3    c_annotations.py
4    ~~~~~~~~~~~~~~~~
5
6    Supports annotations for C API elements:
7
8    * reference count annotations for C API functions.  Based on
9      refcount.py and anno-api.py in the old Python documentation tools.
10
11    * stable API annotations
12
13    Usage:
14    * Set the `refcount_file` config value to the path to the reference
15    count data file.
16    * Set the `stable_abi_file` config value to the path to stable ABI list.
17
18    :copyright: Copyright 2007-2014 by Georg Brandl.
19    :license: Python license.
20"""
21
22from os import path
23from docutils import nodes
24from docutils.parsers.rst import directives
25from docutils.parsers.rst import Directive
26from docutils.statemachine import StringList
27import csv
28
29from sphinx import addnodes
30from sphinx.domains.c import CObject
31
32
33REST_ROLE_MAP = {
34    'function': 'func',
35    'var': 'data',
36    'type': 'type',
37    'macro': 'macro',
38    'type': 'type',
39}
40
41
42class RCEntry:
43    def __init__(self, name):
44        self.name = name
45        self.args = []
46        self.result_type = ''
47        self.result_refs = None
48
49
50class Annotations:
51    def __init__(self, refcount_filename, stable_abi_file):
52        self.refcount_data = {}
53        with open(refcount_filename, 'r') as fp:
54            for line in fp:
55                line = line.strip()
56                if line[:1] in ("", "#"):
57                    # blank lines and comments
58                    continue
59                parts = line.split(":", 4)
60                if len(parts) != 5:
61                    raise ValueError("Wrong field count in %r" % line)
62                function, type, arg, refcount, comment = parts
63                # Get the entry, creating it if needed:
64                try:
65                    entry = self.refcount_data[function]
66                except KeyError:
67                    entry = self.refcount_data[function] = RCEntry(function)
68                if not refcount or refcount == "null":
69                    refcount = None
70                else:
71                    refcount = int(refcount)
72                # Update the entry with the new parameter or the result
73                # information.
74                if arg:
75                    entry.args.append((arg, type, refcount))
76                else:
77                    entry.result_type = type
78                    entry.result_refs = refcount
79
80        self.stable_abi_data = {}
81        with open(stable_abi_file, 'r') as fp:
82            for record in csv.DictReader(fp):
83                role = record['role']
84                name = record['name']
85                self.stable_abi_data[name] = record
86
87    def add_annotations(self, app, doctree):
88        for node in doctree.traverse(addnodes.desc_content):
89            par = node.parent
90            if par['domain'] != 'c':
91                continue
92            if not par[0].has_key('ids') or not par[0]['ids']:
93                continue
94            name = par[0]['ids'][0]
95            if name.startswith("c."):
96                name = name[2:]
97
98            objtype = par['objtype']
99
100            # Stable ABI annotation. These have two forms:
101            #   Part of the [Stable ABI](link).
102            #   Part of the [Stable ABI](link) since version X.Y.
103            record = self.stable_abi_data.get(name)
104            if record:
105                if record['role'] != objtype:
106                    raise ValueError(
107                        f"Object type mismatch in limited API annotation "
108                        f"for {name}: {record['role']!r} != {objtype!r}")
109                stable_added = record['added']
110                message = ' Part of the '
111                emph_node = nodes.emphasis(message, message,
112                                           classes=['stableabi'])
113                ref_node = addnodes.pending_xref(
114                    'Stable ABI', refdomain="std", reftarget='stable',
115                    reftype='ref', refexplicit="False")
116                ref_node += nodes.Text('Stable ABI')
117                emph_node += ref_node
118                if record['ifdef_note']:
119                    emph_node += nodes.Text(' ' + record['ifdef_note'])
120                if stable_added == '3.2':
121                    # Stable ABI was introduced in 3.2.
122                    emph_node += nodes.Text('.')
123                else:
124                    emph_node += nodes.Text(f' since version {stable_added}.')
125                node.insert(0, emph_node)
126
127            # Return value annotation
128            if objtype != 'function':
129                continue
130            entry = self.refcount_data.get(name)
131            if not entry:
132                continue
133            elif not entry.result_type.endswith("Object*"):
134                continue
135            if entry.result_refs is None:
136                rc = 'Return value: Always NULL.'
137            elif entry.result_refs:
138                rc = 'Return value: New reference.'
139            else:
140                rc = 'Return value: Borrowed reference.'
141            node.insert(0, nodes.emphasis(rc, rc, classes=['refcount']))
142
143
144def init_annotations(app):
145    annotations = Annotations(
146        path.join(app.srcdir, app.config.refcount_file),
147        path.join(app.srcdir, app.config.stable_abi_file),
148    )
149    app.connect('doctree-read', annotations.add_annotations)
150
151    class LimitedAPIList(Directive):
152
153        has_content = False
154        required_arguments = 0
155        optional_arguments = 0
156        final_argument_whitespace = True
157
158        def run(self):
159            content = []
160            for record in annotations.stable_abi_data.values():
161                role = REST_ROLE_MAP[record['role']]
162                name = record['name']
163                content.append(f'* :c:{role}:`{name}`')
164
165            pnode = nodes.paragraph()
166            self.state.nested_parse(StringList(content), 0, pnode)
167            return [pnode]
168
169    app.add_directive('limited-api-list', LimitedAPIList)
170
171
172def setup(app):
173    app.add_config_value('refcount_file', '', True)
174    app.add_config_value('stable_abi_file', '', True)
175    app.connect('builder-inited', init_annotations)
176
177    # monkey-patch C object...
178    CObject.option_spec = {
179        'noindex': directives.flag,
180        'stableabi': directives.flag,
181    }
182    old_handle_signature = CObject.handle_signature
183    def new_handle_signature(self, sig, signode):
184        signode.parent['stableabi'] = 'stableabi' in self.options
185        return old_handle_signature(self, sig, signode)
186    CObject.handle_signature = new_handle_signature
187    return {'version': '1.0', 'parallel_read_safe': True}
188