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
22 from os import path
23 import docutils
24 from docutils import nodes
25 from docutils.parsers.rst import directives
26 from docutils.parsers.rst import Directive
27 from docutils.statemachine import StringList
28 from sphinx.locale import _ as sphinx_gettext
29 import csv
30
31 from sphinx import addnodes
32 from sphinx.domains.c import CObject
33
34
35 REST_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.
48 if 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
55 class ESC[4;38;5;81mRCEntry:
56 def __init__(self, name):
57 self.name = name
58 self.args = []
59 self.result_type = ''
60 self.result_refs = None
61
62
63 class ESC[4;38;5;81mAnnotations:
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
175 def 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 ESC[4;38;5;81mLimitedAPIList(ESC[4;38;5;149mDirective):
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
203 def 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}