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