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