mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			305 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""Support annotations for C API elements.
 | 
						|
 | 
						|
* Reference count annotations for C API functions.
 | 
						|
* Stable ABI annotations
 | 
						|
* Limited API annotations
 | 
						|
 | 
						|
Configuration:
 | 
						|
* Set ``refcount_file`` to the path to the reference count data file.
 | 
						|
* Set ``stable_abi_file`` to the path to stable ABI list.
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import csv
 | 
						|
import dataclasses
 | 
						|
from pathlib import Path
 | 
						|
from typing import TYPE_CHECKING
 | 
						|
 | 
						|
import sphinx
 | 
						|
from docutils import nodes
 | 
						|
from docutils.statemachine import StringList
 | 
						|
from sphinx import addnodes
 | 
						|
from sphinx.locale import _ as sphinx_gettext
 | 
						|
from sphinx.util.docutils import SphinxDirective
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from sphinx.application import Sphinx
 | 
						|
    from sphinx.util.typing import ExtensionMetadata
 | 
						|
 | 
						|
ROLE_TO_OBJECT_TYPE = {
 | 
						|
    "func": "function",
 | 
						|
    "macro": "macro",
 | 
						|
    "member": "member",
 | 
						|
    "type": "type",
 | 
						|
    "data": "var",
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
@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"],
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
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
 | 
						|
 | 
						|
    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]
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
 | 
						|
    if sphinx.version_info[:2] < (7, 2):
 | 
						|
        from docutils.parsers.rst import directives
 | 
						|
        from sphinx.domains.c import CObject
 | 
						|
 | 
						|
        # 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,
 | 
						|
    }
 |