2013-10-12 14:54:30 -03:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
c_annotations.py
|
|
|
|
~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
Supports annotations for C API elements:
|
|
|
|
|
|
|
|
* reference count annotations for C API functions. Based on
|
|
|
|
refcount.py and anno-api.py in the old Python documentation tools.
|
|
|
|
|
|
|
|
* stable API annotations
|
|
|
|
|
2021-05-11 11:04:33 -03:00
|
|
|
Usage:
|
|
|
|
* Set the `refcount_file` config value to the path to the reference
|
2013-10-12 14:54:30 -03:00
|
|
|
count data file.
|
2021-05-11 11:04:33 -03:00
|
|
|
* Set the `stable_abi_file` config value to the path to stable ABI list.
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2014-09-30 17:17:41 -03:00
|
|
|
:copyright: Copyright 2007-2014 by Georg Brandl.
|
2013-10-12 14:54:30 -03:00
|
|
|
:license: Python license.
|
|
|
|
"""
|
|
|
|
|
|
|
|
from os import path
|
2023-04-08 04:56:20 -03:00
|
|
|
import docutils
|
2013-10-12 14:54:30 -03:00
|
|
|
from docutils import nodes
|
|
|
|
from docutils.parsers.rst import directives
|
2021-05-11 11:04:33 -03:00
|
|
|
from docutils.parsers.rst import Directive
|
|
|
|
from docutils.statemachine import StringList
|
2023-03-06 17:20:52 -04:00
|
|
|
from sphinx.locale import _ as sphinx_gettext
|
2021-05-11 11:04:33 -03:00
|
|
|
import csv
|
2013-10-12 14:54:30 -03:00
|
|
|
|
|
|
|
from sphinx import addnodes
|
|
|
|
from sphinx.domains.c import CObject
|
|
|
|
|
|
|
|
|
2021-05-11 11:04:33 -03:00
|
|
|
REST_ROLE_MAP = {
|
|
|
|
'function': 'func',
|
|
|
|
'var': 'data',
|
|
|
|
'type': 'type',
|
|
|
|
'macro': 'macro',
|
|
|
|
'type': 'type',
|
2022-04-06 11:50:45 -03:00
|
|
|
'member': 'member',
|
2021-05-11 11:04:33 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-04-08 04:56:20 -03:00
|
|
|
# Monkeypatch nodes.Node.findall for forwards compatability
|
|
|
|
# This patch can be dropped when the minimum Sphinx version is 4.4.0
|
|
|
|
# or the minimum Docutils version is 0.18.1.
|
|
|
|
if docutils.__version_info__ < (0, 18, 1):
|
|
|
|
def findall(self, *args, **kwargs):
|
|
|
|
return iter(self.traverse(*args, **kwargs))
|
|
|
|
|
|
|
|
nodes.Node.findall = findall
|
|
|
|
|
|
|
|
|
2013-10-12 14:54:30 -03:00
|
|
|
class RCEntry:
|
|
|
|
def __init__(self, name):
|
|
|
|
self.name = name
|
|
|
|
self.args = []
|
|
|
|
self.result_type = ''
|
|
|
|
self.result_refs = None
|
|
|
|
|
|
|
|
|
2021-05-11 11:04:33 -03:00
|
|
|
class Annotations:
|
|
|
|
def __init__(self, refcount_filename, stable_abi_file):
|
|
|
|
self.refcount_data = {}
|
|
|
|
with open(refcount_filename, 'r') as fp:
|
2013-10-12 14:54:30 -03:00
|
|
|
for line in fp:
|
|
|
|
line = line.strip()
|
|
|
|
if line[:1] in ("", "#"):
|
|
|
|
# blank lines and comments
|
|
|
|
continue
|
|
|
|
parts = line.split(":", 4)
|
|
|
|
if len(parts) != 5:
|
|
|
|
raise ValueError("Wrong field count in %r" % line)
|
|
|
|
function, type, arg, refcount, comment = parts
|
|
|
|
# Get the entry, creating it if needed:
|
|
|
|
try:
|
2021-05-11 11:04:33 -03:00
|
|
|
entry = self.refcount_data[function]
|
2013-10-12 14:54:30 -03:00
|
|
|
except KeyError:
|
2021-05-11 11:04:33 -03:00
|
|
|
entry = self.refcount_data[function] = RCEntry(function)
|
2013-10-12 14:54:30 -03:00
|
|
|
if not refcount or refcount == "null":
|
|
|
|
refcount = None
|
|
|
|
else:
|
|
|
|
refcount = int(refcount)
|
|
|
|
# Update the entry with the new parameter or the result
|
|
|
|
# information.
|
|
|
|
if arg:
|
|
|
|
entry.args.append((arg, type, refcount))
|
|
|
|
else:
|
|
|
|
entry.result_type = type
|
|
|
|
entry.result_refs = refcount
|
2021-05-11 11:04:33 -03:00
|
|
|
|
|
|
|
self.stable_abi_data = {}
|
|
|
|
with open(stable_abi_file, 'r') as fp:
|
|
|
|
for record in csv.DictReader(fp):
|
|
|
|
role = record['role']
|
|
|
|
name = record['name']
|
|
|
|
self.stable_abi_data[name] = record
|
2013-10-12 14:54:30 -03:00
|
|
|
|
|
|
|
def add_annotations(self, app, doctree):
|
2023-04-08 04:56:20 -03:00
|
|
|
for node in doctree.findall(addnodes.desc_content):
|
2013-10-12 14:54:30 -03:00
|
|
|
par = node.parent
|
|
|
|
if par['domain'] != 'c':
|
|
|
|
continue
|
2020-11-13 11:15:17 -04:00
|
|
|
if not par[0].has_key('ids') or not par[0]['ids']:
|
2013-10-12 14:54:30 -03:00
|
|
|
continue
|
2020-11-13 11:15:17 -04:00
|
|
|
name = par[0]['ids'][0]
|
2014-04-17 19:29:01 -03:00
|
|
|
if name.startswith("c."):
|
|
|
|
name = name[2:]
|
2021-05-11 11:04:33 -03:00
|
|
|
|
|
|
|
objtype = par['objtype']
|
|
|
|
|
|
|
|
# Stable ABI annotation. These have two forms:
|
|
|
|
# Part of the [Stable ABI](link).
|
|
|
|
# Part of the [Stable ABI](link) since version X.Y.
|
2022-04-06 11:50:45 -03:00
|
|
|
# For structs, there's some more info in the message:
|
|
|
|
# Part of the [Limited API](link) (as an opaque struct).
|
|
|
|
# Part of the [Stable ABI](link) (including all members).
|
|
|
|
# Part of the [Limited API](link) (Only some members are part
|
|
|
|
# of the stable ABI.).
|
|
|
|
# ... all of which can have "since version X.Y" appended.
|
2021-05-11 11:04:33 -03:00
|
|
|
record = self.stable_abi_data.get(name)
|
|
|
|
if record:
|
|
|
|
if record['role'] != objtype:
|
|
|
|
raise ValueError(
|
|
|
|
f"Object type mismatch in limited API annotation "
|
|
|
|
f"for {name}: {record['role']!r} != {objtype!r}")
|
|
|
|
stable_added = record['added']
|
|
|
|
message = ' Part of the '
|
|
|
|
emph_node = nodes.emphasis(message, message,
|
|
|
|
classes=['stableabi'])
|
|
|
|
ref_node = addnodes.pending_xref(
|
|
|
|
'Stable ABI', refdomain="std", reftarget='stable',
|
|
|
|
reftype='ref', refexplicit="False")
|
2022-04-06 11:50:45 -03:00
|
|
|
struct_abi_kind = record['struct_abi_kind']
|
|
|
|
if struct_abi_kind in {'opaque', 'members'}:
|
|
|
|
ref_node += nodes.Text('Limited API')
|
|
|
|
else:
|
|
|
|
ref_node += nodes.Text('Stable ABI')
|
2021-05-11 11:04:33 -03:00
|
|
|
emph_node += ref_node
|
2022-04-06 11:50:45 -03:00
|
|
|
if struct_abi_kind == 'opaque':
|
|
|
|
emph_node += nodes.Text(' (as an opaque struct)')
|
|
|
|
elif struct_abi_kind == 'full-abi':
|
|
|
|
emph_node += nodes.Text(' (including all members)')
|
2021-05-11 11:04:33 -03:00
|
|
|
if record['ifdef_note']:
|
|
|
|
emph_node += nodes.Text(' ' + record['ifdef_note'])
|
|
|
|
if stable_added == '3.2':
|
|
|
|
# Stable ABI was introduced in 3.2.
|
2022-04-06 11:50:45 -03:00
|
|
|
pass
|
2021-05-11 11:04:33 -03:00
|
|
|
else:
|
2022-04-06 11:50:45 -03:00
|
|
|
emph_node += nodes.Text(f' since version {stable_added}')
|
|
|
|
emph_node += nodes.Text('.')
|
|
|
|
if struct_abi_kind == 'members':
|
|
|
|
emph_node += nodes.Text(
|
|
|
|
' (Only some members are part of the stable ABI.)')
|
2021-05-11 11:04:33 -03:00
|
|
|
node.insert(0, emph_node)
|
|
|
|
|
2023-02-28 04:31:01 -04:00
|
|
|
# Unstable API annotation.
|
|
|
|
if name.startswith('PyUnstable'):
|
|
|
|
warn_node = nodes.admonition(
|
|
|
|
classes=['unstable-c-api', 'warning'])
|
|
|
|
message = 'This is '
|
|
|
|
emph_node = nodes.emphasis(message, message)
|
|
|
|
ref_node = addnodes.pending_xref(
|
|
|
|
'Unstable API', refdomain="std",
|
|
|
|
reftarget='unstable-c-api',
|
|
|
|
reftype='ref', refexplicit="False")
|
|
|
|
ref_node += nodes.Text('Unstable API')
|
|
|
|
emph_node += ref_node
|
|
|
|
emph_node += nodes.Text('. It may change without warning in minor releases.')
|
|
|
|
warn_node += emph_node
|
|
|
|
node.insert(0, warn_node)
|
|
|
|
|
2021-05-11 11:04:33 -03:00
|
|
|
# Return value annotation
|
|
|
|
if objtype != 'function':
|
|
|
|
continue
|
|
|
|
entry = self.refcount_data.get(name)
|
2013-10-12 14:54:30 -03:00
|
|
|
if not entry:
|
|
|
|
continue
|
2018-12-20 03:33:58 -04:00
|
|
|
elif not entry.result_type.endswith("Object*"):
|
2013-10-12 14:54:30 -03:00
|
|
|
continue
|
|
|
|
if entry.result_refs is None:
|
2023-03-06 17:20:52 -04:00
|
|
|
rc = sphinx_gettext('Return value: Always NULL.')
|
2013-10-12 14:54:30 -03:00
|
|
|
elif entry.result_refs:
|
2023-03-06 17:20:52 -04:00
|
|
|
rc = sphinx_gettext('Return value: New reference.')
|
2013-10-12 14:54:30 -03:00
|
|
|
else:
|
2023-03-06 17:20:52 -04:00
|
|
|
rc = sphinx_gettext('Return value: Borrowed reference.')
|
2013-10-12 14:54:30 -03:00
|
|
|
node.insert(0, nodes.emphasis(rc, rc, classes=['refcount']))
|
|
|
|
|
|
|
|
|
|
|
|
def init_annotations(app):
|
2021-05-11 11:04:33 -03:00
|
|
|
annotations = Annotations(
|
|
|
|
path.join(app.srcdir, app.config.refcount_file),
|
|
|
|
path.join(app.srcdir, app.config.stable_abi_file),
|
|
|
|
)
|
|
|
|
app.connect('doctree-read', annotations.add_annotations)
|
|
|
|
|
|
|
|
class LimitedAPIList(Directive):
|
|
|
|
|
|
|
|
has_content = False
|
|
|
|
required_arguments = 0
|
|
|
|
optional_arguments = 0
|
|
|
|
final_argument_whitespace = True
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
content = []
|
|
|
|
for record in annotations.stable_abi_data.values():
|
|
|
|
role = REST_ROLE_MAP[record['role']]
|
|
|
|
name = record['name']
|
|
|
|
content.append(f'* :c:{role}:`{name}`')
|
|
|
|
|
|
|
|
pnode = nodes.paragraph()
|
|
|
|
self.state.nested_parse(StringList(content), 0, pnode)
|
|
|
|
return [pnode]
|
|
|
|
|
|
|
|
app.add_directive('limited-api-list', LimitedAPIList)
|
2013-10-12 14:54:30 -03:00
|
|
|
|
|
|
|
|
|
|
|
def setup(app):
|
|
|
|
app.add_config_value('refcount_file', '', True)
|
2021-05-11 11:04:33 -03:00
|
|
|
app.add_config_value('stable_abi_file', '', True)
|
2013-10-12 14:54:30 -03:00
|
|
|
app.connect('builder-inited', init_annotations)
|
|
|
|
|
|
|
|
# monkey-patch C object...
|
|
|
|
CObject.option_spec = {
|
|
|
|
'noindex': directives.flag,
|
2013-10-12 17:55:34 -03:00
|
|
|
'stableabi': directives.flag,
|
2013-10-12 14:54:30 -03:00
|
|
|
}
|
|
|
|
old_handle_signature = CObject.handle_signature
|
|
|
|
def new_handle_signature(self, sig, signode):
|
2013-10-12 17:55:34 -03:00
|
|
|
signode.parent['stableabi'] = 'stableabi' in self.options
|
2013-10-12 14:54:30 -03:00
|
|
|
return old_handle_signature(self, sig, signode)
|
|
|
|
CObject.handle_signature = new_handle_signature
|
2014-09-30 17:17:41 -03:00
|
|
|
return {'version': '1.0', 'parallel_read_safe': True}
|