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
23import docutils
24from docutils import nodes
25from docutils.parsers.rst import directives
26from docutils.parsers.rst import Directive
27from docutils.statemachine import StringList
28from sphinx.locale import _ as sphinx_gettext
29import csv
30
31from sphinx import addnodes
32from sphinx.domains.c import CObject
33
34
35REST_ROLE_MAP = {
36    'function': 'func',
37    'var': 'data',
38    'type': 'type',
39    'macro': 'macro',
40    'type': 'type',
41    'member': 'member',
42}
43
44
45# Monkeypatch nodes.Node.findall for forwards compatability
46# This patch can be dropped when the minimum Sphinx version is 4.4.0
47# or the minimum Docutils version is 0.18.1.
48if docutils.__version_info__ < (0, 18, 1):
49    def findall(self, *args, **kwargs):
50        return iter(self.traverse(*args, **kwargs))
51
52    nodes.Node.findall = findall
53
54
55class RCEntry:
56    def __init__(self, name):
57        self.name = name
58        self.args = []
59        self.result_type = ''
60        self.result_refs = None
61
62
63class Annotations:
64    def __init__(self, refcount_filename, stable_abi_file):
65        self.refcount_data = {}
66        with open(refcount_filename, 'r') as fp:
67            for line in fp:
68                line = line.strip()
69                if line[:1] in ("", "#"):
70                    # blank lines and comments
71                    continue
72                parts = line.split(":", 4)
73                if len(parts) != 5:
74                    raise ValueError("Wrong field count in %r" % line)
75                function, type, arg, refcount, comment = parts
76                # Get the entry, creating it if needed:
77                try:
78                    entry = self.refcount_data[function]
79                except KeyError:
80                    entry = self.refcount_data[function] = RCEntry(function)
81                if not refcount or refcount == "null":
82                    refcount = None
83                else:
84                    refcount = int(refcount)
85                # Update the entry with the new parameter or the result
86                # information.
87                if arg:
88                    entry.args.append((arg, type, refcount))
89                else:
90                    entry.result_type = type
91                    entry.result_refs = refcount
92
93        self.stable_abi_data = {}
94        with open(stable_abi_file, 'r') as fp:
95            for record in csv.DictReader(fp):
96                role = record['role']
97                name = record['name']
98                self.stable_abi_data[name] = record
99
100    def add_annotations(self, app, doctree):
101        for node in doctree.findall(addnodes.desc_content):
102            par = node.parent
103            if par['domain'] != 'c':
104                continue
105            if not par[0].has_key('ids') or not par[0]['ids']:
106                continue
107            name = par[0]['ids'][0]
108            if name.startswith("c."):
109                name = name[2:]
110
111            objtype = par['objtype']
112
113            # Stable ABI annotation. These have two forms:
114            #   Part of the [Stable ABI](link).
115            #   Part of the [Stable ABI](link) since version X.Y.
116            # For structs, there's some more info in the message:
117            #   Part of the [Limited API](link) (as an opaque struct).
118            #   Part of the [Stable ABI](link) (including all members).
119            #   Part of the [Limited API](link) (Only some members are part
120            #       of the stable ABI.).
121            # ... all of which can have "since version X.Y" appended.
122            record = self.stable_abi_data.get(name)
123            if record:
124                if record['role'] != objtype:
125                    raise ValueError(
126                        f"Object type mismatch in limited API annotation "
127                        f"for {name}: {record['role']!r} != {objtype!r}")
128                stable_added = record['added']
129                message = ' Part of the '
130                emph_node = nodes.emphasis(message, message,
131                                           classes=['stableabi'])
132                ref_node = addnodes.pending_xref(
133                    'Stable ABI', refdomain="std", reftarget='stable',
134                    reftype='ref', refexplicit="False")
135                struct_abi_kind = record['struct_abi_kind']
136                if struct_abi_kind in {'opaque', 'members'}:
137                    ref_node += nodes.Text('Limited API')
138                else:
139                    ref_node += nodes.Text('Stable ABI')
140                emph_node += ref_node
141                if struct_abi_kind == 'opaque':
142                    emph_node += nodes.Text(' (as an opaque struct)')
143                elif struct_abi_kind == 'full-abi':
144                    emph_node += nodes.Text(' (including all members)')
145                if record['ifdef_note']:
146                    emph_node += nodes.Text(' ' + record['ifdef_note'])
147                if stable_added == '3.2':
148                    # Stable ABI was introduced in 3.2.
149                    pass
150                else:
151                    emph_node += nodes.Text(f' since version {stable_added}')
152                emph_node += nodes.Text('.')
153                if struct_abi_kind == 'members':
154                    emph_node += nodes.Text(
155                        ' (Only some members are part of the stable ABI.)')
156                node.insert(0, emph_node)
157
158            # Return value annotation
159            if objtype != 'function':
160                continue
161            entry = self.refcount_data.get(name)
162            if not entry:
163                continue
164            elif not entry.result_type.endswith("Object*"):
165                continue
166            if entry.result_refs is None:
167                rc = sphinx_gettext('Return value: Always NULL.')
168            elif entry.result_refs:
169                rc = sphinx_gettext('Return value: New reference.')
170            else:
171                rc = sphinx_gettext('Return value: Borrowed reference.')
172            node.insert(0, nodes.emphasis(rc, rc, classes=['refcount']))
173
174
175def init_annotations(app):
176    annotations = Annotations(
177        path.join(app.srcdir, app.config.refcount_file),
178        path.join(app.srcdir, app.config.stable_abi_file),
179    )
180    app.connect('doctree-read', annotations.add_annotations)
181
182    class LimitedAPIList(Directive):
183
184        has_content = False
185        required_arguments = 0
186        optional_arguments = 0
187        final_argument_whitespace = True
188
189        def run(self):
190            content = []
191            for record in annotations.stable_abi_data.values():
192                role = REST_ROLE_MAP[record['role']]
193                name = record['name']
194                content.append(f'* :c:{role}:`{name}`')
195
196            pnode = nodes.paragraph()
197            self.state.nested_parse(StringList(content), 0, pnode)
198            return [pnode]
199
200    app.add_directive('limited-api-list', LimitedAPIList)
201
202
203def setup(app):
204    app.add_config_value('refcount_file', '', True)
205    app.add_config_value('stable_abi_file', '', True)
206    app.connect('builder-inited', init_annotations)
207
208    # monkey-patch C object...
209    CObject.option_spec = {
210        'noindex': directives.flag,
211        'stableabi': directives.flag,
212    }
213    old_handle_signature = CObject.handle_signature
214    def new_handle_signature(self, sig, signode):
215        signode.parent['stableabi'] = 'stableabi' in self.options
216        return old_handle_signature(self, sig, signode)
217    CObject.handle_signature = new_handle_signature
218    return {'version': '1.0', 'parallel_read_safe': True}
219