2024-07-19 09:21:56 -03:00
|
|
|
"""Support annotations for C API elements.
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
* Reference count annotations for C API functions.
|
|
|
|
* Stable ABI annotations
|
|
|
|
* Limited API annotations
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
Configuration:
|
|
|
|
* Set ``refcount_file`` to the path to the reference count data file.
|
|
|
|
* Set ``stable_abi_file`` to the path to stable ABI list.
|
|
|
|
"""
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
from __future__ import annotations
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
import csv
|
|
|
|
import dataclasses
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import TYPE_CHECKING
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
import sphinx
|
2013-10-12 14:54:30 -03:00
|
|
|
from docutils import nodes
|
2021-05-11 11:04:33 -03:00
|
|
|
from docutils.statemachine import StringList
|
2013-10-12 14:54:30 -03:00
|
|
|
from sphinx import addnodes
|
2024-07-19 09:21:56 -03:00
|
|
|
from sphinx.locale import _ as sphinx_gettext
|
|
|
|
from sphinx.util.docutils import SphinxDirective
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from sphinx.application import Sphinx
|
|
|
|
from sphinx.util.typing import ExtensionMetadata
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
ROLE_TO_OBJECT_TYPE = {
|
|
|
|
"func": "function",
|
|
|
|
"macro": "macro",
|
|
|
|
"member": "member",
|
|
|
|
"type": "type",
|
|
|
|
"data": "var",
|
2021-05-11 11:04:33 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
@dataclasses.dataclass(slots=True)
|
|
|
|
class RefCountEntry:
|
|
|
|
# Name of the function.
|
|
|
|
name: str
|
|
|
|
# List of (argument name, type, refcount effect) tuples.
|
|
|
|
# (Currently not used. If it was, a dataclass might work better.)
|
|
|
|
args: list = dataclasses.field(default_factory=list)
|
|
|
|
# Return type of the function.
|
|
|
|
result_type: str = ""
|
|
|
|
# Reference count effect for the return value.
|
|
|
|
result_refs: int | None = None
|
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
|
|
class StableABIEntry:
|
|
|
|
# Role of the object.
|
|
|
|
# Source: Each [item_kind] in stable_abi.toml is mapped to a C Domain role.
|
|
|
|
role: str
|
|
|
|
# Name of the object.
|
|
|
|
# Source: [<item_kind>.*] in stable_abi.toml.
|
|
|
|
name: str
|
|
|
|
# Version when the object was added to the stable ABI.
|
|
|
|
# (Source: [<item_kind>.*.added] in stable_abi.toml.
|
|
|
|
added: str
|
|
|
|
# An explananatory blurb for the ifdef.
|
|
|
|
# Source: ``feature_macro.*.doc`` in stable_abi.toml.
|
|
|
|
ifdef_note: str
|
|
|
|
# Defines how much of the struct is exposed. Only relevant for structs.
|
|
|
|
# Source: [<item_kind>.*.struct_abi_kind] in stable_abi.toml.
|
|
|
|
struct_abi_kind: str
|
|
|
|
|
|
|
|
|
|
|
|
def read_refcount_data(refcount_filename: Path) -> dict[str, RefCountEntry]:
|
|
|
|
refcount_data = {}
|
|
|
|
refcounts = refcount_filename.read_text(encoding="utf8")
|
|
|
|
for line in refcounts.splitlines():
|
|
|
|
line = line.strip()
|
|
|
|
if not line or line.startswith("#"):
|
|
|
|
# blank lines and comments
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Each line is of the form
|
|
|
|
# function ':' type ':' [param name] ':' [refcount effect] ':' [comment]
|
|
|
|
parts = line.split(":", 4)
|
|
|
|
if len(parts) != 5:
|
|
|
|
raise ValueError(f"Wrong field count in {line!r}")
|
|
|
|
function, type, arg, refcount, _comment = parts
|
|
|
|
|
|
|
|
# Get the entry, creating it if needed:
|
|
|
|
try:
|
|
|
|
entry = refcount_data[function]
|
|
|
|
except KeyError:
|
|
|
|
entry = refcount_data[function] = RefCountEntry(function)
|
|
|
|
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
|
|
|
|
|
|
|
|
return refcount_data
|
|
|
|
|
|
|
|
|
|
|
|
def read_stable_abi_data(stable_abi_file: Path) -> dict[str, StableABIEntry]:
|
|
|
|
stable_abi_data = {}
|
|
|
|
with open(stable_abi_file, encoding="utf8") as fp:
|
|
|
|
for record in csv.DictReader(fp):
|
|
|
|
name = record["name"]
|
|
|
|
stable_abi_data[name] = StableABIEntry(**record)
|
|
|
|
|
|
|
|
return stable_abi_data
|
|
|
|
|
|
|
|
|
|
|
|
def add_annotations(app: Sphinx, doctree: nodes.document) -> None:
|
|
|
|
state = app.env.domaindata["c_annotations"]
|
|
|
|
refcount_data = state["refcount_data"]
|
|
|
|
stable_abi_data = state["stable_abi_data"]
|
|
|
|
for node in doctree.findall(addnodes.desc_content):
|
|
|
|
par = node.parent
|
|
|
|
if par["domain"] != "c":
|
|
|
|
continue
|
|
|
|
if not par[0].get("ids", None):
|
|
|
|
continue
|
|
|
|
name = par[0]["ids"][0]
|
|
|
|
if name.startswith("c."):
|
|
|
|
name = name[2:]
|
|
|
|
|
|
|
|
objtype = par["objtype"]
|
|
|
|
|
|
|
|
# Stable ABI annotation.
|
|
|
|
if record := stable_abi_data.get(name):
|
|
|
|
if ROLE_TO_OBJECT_TYPE[record.role] != objtype:
|
|
|
|
msg = (
|
|
|
|
f"Object type mismatch in limited API annotation for {name}: "
|
|
|
|
f"{ROLE_TO_OBJECT_TYPE[record.role]!r} != {objtype!r}"
|
|
|
|
)
|
|
|
|
raise ValueError(msg)
|
|
|
|
annotation = _stable_abi_annotation(record)
|
|
|
|
node.insert(0, annotation)
|
|
|
|
|
|
|
|
# Unstable API annotation.
|
|
|
|
if name.startswith("PyUnstable"):
|
|
|
|
annotation = _unstable_api_annotation()
|
|
|
|
node.insert(0, annotation)
|
|
|
|
|
|
|
|
# Return value annotation
|
|
|
|
if objtype != "function":
|
|
|
|
continue
|
|
|
|
if name not in refcount_data:
|
|
|
|
continue
|
|
|
|
entry = refcount_data[name]
|
|
|
|
if not entry.result_type.endswith("Object*"):
|
|
|
|
continue
|
|
|
|
annotation = _return_value_annotation(entry.result_refs)
|
|
|
|
node.insert(0, annotation)
|
|
|
|
|
|
|
|
|
|
|
|
def _stable_abi_annotation(record: StableABIEntry) -> nodes.emphasis:
|
|
|
|
"""Create the Stable ABI annotation.
|
|
|
|
|
|
|
|
These have two forms:
|
|
|
|
Part of the `Stable ABI <link>`_.
|
|
|
|
Part of the `Stable ABI <link>`_ since version X.Y.
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
stable_added = record.added
|
|
|
|
message = sphinx_gettext("Part of the")
|
|
|
|
message = message.center(len(message) + 2)
|
|
|
|
emph_node = nodes.emphasis(message, message, classes=["stableabi"])
|
|
|
|
ref_node = addnodes.pending_xref(
|
|
|
|
"Stable ABI",
|
|
|
|
refdomain="std",
|
|
|
|
reftarget="stable",
|
|
|
|
reftype="ref",
|
|
|
|
refexplicit="False",
|
|
|
|
)
|
|
|
|
struct_abi_kind = record.struct_abi_kind
|
|
|
|
if struct_abi_kind in {"opaque", "members"}:
|
|
|
|
ref_node += nodes.Text(sphinx_gettext("Limited API"))
|
|
|
|
else:
|
|
|
|
ref_node += nodes.Text(sphinx_gettext("Stable ABI"))
|
|
|
|
emph_node += ref_node
|
|
|
|
if struct_abi_kind == "opaque":
|
|
|
|
emph_node += nodes.Text(" " + sphinx_gettext("(as an opaque struct)"))
|
|
|
|
elif struct_abi_kind == "full-abi":
|
|
|
|
emph_node += nodes.Text(
|
|
|
|
" " + sphinx_gettext("(including all members)")
|
|
|
|
)
|
|
|
|
if record.ifdef_note:
|
|
|
|
emph_node += nodes.Text(f" {record.ifdef_note}")
|
|
|
|
if stable_added == "3.2":
|
|
|
|
# Stable ABI was introduced in 3.2.
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
emph_node += nodes.Text(
|
|
|
|
" " + sphinx_gettext("since version %s") % stable_added
|
|
|
|
)
|
|
|
|
emph_node += nodes.Text(".")
|
|
|
|
if struct_abi_kind == "members":
|
|
|
|
msg = " " + sphinx_gettext(
|
|
|
|
"(Only some members are part of the stable ABI.)"
|
|
|
|
)
|
|
|
|
emph_node += nodes.Text(msg)
|
|
|
|
return emph_node
|
|
|
|
|
|
|
|
|
|
|
|
def _unstable_api_annotation() -> nodes.admonition:
|
|
|
|
ref_node = addnodes.pending_xref(
|
|
|
|
"Unstable API",
|
|
|
|
nodes.Text(sphinx_gettext("Unstable API")),
|
|
|
|
refdomain="std",
|
|
|
|
reftarget="unstable-c-api",
|
|
|
|
reftype="ref",
|
|
|
|
refexplicit="False",
|
|
|
|
)
|
|
|
|
emph_node = nodes.emphasis(
|
|
|
|
"This is ",
|
|
|
|
sphinx_gettext("This is") + " ",
|
|
|
|
ref_node,
|
|
|
|
nodes.Text(
|
|
|
|
sphinx_gettext(
|
|
|
|
". It may change without warning in minor releases."
|
|
|
|
)
|
|
|
|
),
|
|
|
|
)
|
|
|
|
return nodes.admonition(
|
|
|
|
"",
|
|
|
|
emph_node,
|
|
|
|
classes=["unstable-c-api", "warning"],
|
2021-05-11 11:04:33 -03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
def _return_value_annotation(result_refs: int | None) -> nodes.emphasis:
|
|
|
|
classes = ["refcount"]
|
|
|
|
if result_refs is None:
|
|
|
|
rc = sphinx_gettext("Return value: Always NULL.")
|
|
|
|
classes.append("return_null")
|
|
|
|
elif result_refs:
|
|
|
|
rc = sphinx_gettext("Return value: New reference.")
|
|
|
|
classes.append("return_new_ref")
|
|
|
|
else:
|
|
|
|
rc = sphinx_gettext("Return value: Borrowed reference.")
|
|
|
|
classes.append("return_borrowed_ref")
|
|
|
|
return nodes.emphasis(rc, rc, classes=classes)
|
|
|
|
|
|
|
|
|
|
|
|
class LimitedAPIList(SphinxDirective):
|
|
|
|
has_content = False
|
|
|
|
required_arguments = 0
|
|
|
|
optional_arguments = 0
|
|
|
|
final_argument_whitespace = True
|
2021-05-11 11:04:33 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
def run(self) -> list[nodes.Node]:
|
|
|
|
state = self.env.domaindata["c_annotations"]
|
|
|
|
content = [
|
|
|
|
f"* :c:{record.role}:`{record.name}`"
|
|
|
|
for record in state["stable_abi_data"].values()
|
|
|
|
]
|
|
|
|
node = nodes.paragraph()
|
|
|
|
self.state.nested_parse(StringList(content), 0, node)
|
|
|
|
return [node]
|
2021-05-11 11:04:33 -03:00
|
|
|
|
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
def init_annotations(app: Sphinx) -> None:
|
|
|
|
# Using domaindata is a bit hack-ish,
|
|
|
|
# but allows storing state without a global variable or closure.
|
|
|
|
app.env.domaindata["c_annotations"] = state = {}
|
|
|
|
state["refcount_data"] = read_refcount_data(
|
|
|
|
Path(app.srcdir, app.config.refcount_file)
|
|
|
|
)
|
|
|
|
state["stable_abi_data"] = read_stable_abi_data(
|
|
|
|
Path(app.srcdir, app.config.stable_abi_file)
|
|
|
|
)
|
2013-10-12 14:54:30 -03:00
|
|
|
|
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
|
|
app.add_config_value("refcount_file", "", "env", types={str})
|
|
|
|
app.add_config_value("stable_abi_file", "", "env", types={str})
|
|
|
|
app.add_directive("limited-api-list", LimitedAPIList)
|
|
|
|
app.connect("builder-inited", init_annotations)
|
|
|
|
app.connect("doctree-read", add_annotations)
|
2013-10-12 14:54:30 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
if sphinx.version_info[:2] < (7, 2):
|
|
|
|
from docutils.parsers.rst import directives
|
|
|
|
from sphinx.domains.c import CObject
|
2024-04-16 12:56:15 -03:00
|
|
|
|
2024-07-19 09:21:56 -03:00
|
|
|
# monkey-patch C object...
|
|
|
|
CObject.option_spec |= {
|
|
|
|
"no-index-entry": directives.flag,
|
|
|
|
"no-contents-entry": directives.flag,
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
"version": "1.0",
|
|
|
|
"parallel_read_safe": True,
|
|
|
|
"parallel_write_safe": True,
|
|
|
|
}
|