Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@
# Relative filename of the data files
refcount_file = 'data/refcounts.dat'
stable_abi_file = 'data/stable_abi.dat'
threadsafety_file = 'data/threadsafety.dat'

# Options for sphinxext-opengraph
# -------------------------------
Expand Down
17 changes: 17 additions & 0 deletions Doc/data/threadsafety.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Thread safety annotations for C API functions.
#
# Each line has the form:
# function_name : level
#
# Where level is one of:
# incompatible -- not safe even with external locking
# compatible -- safe if the caller serializes all access with external locks
# safe -- safe for concurrent use
#
# Lines beginning with '#' are ignored.
# The function name must match the C domain identifier used in the documentation.

# Synchronization primitives (Doc/c-api/synchronization.rst)
PyMutex_Lock:safe:
PyMutex_Unlock:safe:
PyMutex_IsLocked:safe:
13 changes: 13 additions & 0 deletions Doc/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,19 @@ Glossary
See :ref:`Thread State and the Global Interpreter Lock <threads>` for more
information.

thread-compatible
A function or operation that is safe to call from multiple threads
provided the caller supplies appropriate external synchronization, for
example by holding a :term:`lock` for the duration of each call. Without
such synchronization, concurrent calls may produce :term:`race conditions
<race condition>` or :term:`data races <data race>`.

thread-incompatible
A function or operation that cannot be made safe for concurrent use even
with external synchronization. Thread-incompatible code typically
accesses global state in an unsynchronized way and must be called from
only one thread at a time throughout the program's lifetime.

thread-safe
A module, function, or class that behaves correctly when used by multiple
threads concurrently. Thread-safe code uses appropriate
Expand Down
78 changes: 78 additions & 0 deletions Doc/tools/extensions/c_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
* Reference count annotations for C API functions.
* Stable ABI annotations
* Limited API annotations
* Thread safety annotations for C API functions.

Configuration:
* Set ``refcount_file`` to the path to the reference count data file.
* Set ``stable_abi_file`` to the path to stable ABI list.
* Set ``threadsafety_file`` to the path to the thread safety data file.
"""

from __future__ import annotations
Expand Down Expand Up @@ -48,6 +50,15 @@ class RefCountEntry:
result_refs: int | None = None


@dataclasses.dataclass(frozen=True, slots=True)
class ThreadSafetyEntry:
# Name of the function.
name: str
# Thread safety level.
# One of: 'incompatible', 'compatible', 'safe'.
level: str


@dataclasses.dataclass(frozen=True, slots=True)
class StableABIEntry:
# Role of the object.
Expand Down Expand Up @@ -113,10 +124,38 @@ def read_stable_abi_data(stable_abi_file: Path) -> dict[str, StableABIEntry]:
return stable_abi_data


_VALID_THREADSAFETY_LEVELS = frozenset({
"incompatible",
"compatible",
"safe",
})


def read_threadsafety_data(threadsafety_filename: Path) -> dict[str, ThreadSafetyEntry]:
threadsafety_data = {}
for line in threadsafety_filename.read_text(encoding="utf8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
# Each line is of the form: function_name : level : [comment]
parts = line.split(":", 2)
if len(parts) < 2:
raise ValueError(f"Wrong field count in {line!r}")
name, level = parts[0].strip(), parts[1].strip()
if level not in _VALID_THREADSAFETY_LEVELS:
raise ValueError(
f"Unknown thread safety level {level!r} for {name!r}. "
f"Valid levels: {sorted(_VALID_THREADSAFETY_LEVELS)}"
)
threadsafety_data[name] = ThreadSafetyEntry(name=name, level=level)
return threadsafety_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"]
threadsafety_data = state["threadsafety_data"]
for node in doctree.findall(addnodes.desc_content):
par = node.parent
if par["domain"] != "c":
Expand All @@ -126,6 +165,12 @@ def add_annotations(app: Sphinx, doctree: nodes.document) -> None:
name = par[0]["ids"][0].removeprefix("c.")
objtype = par["objtype"]

# Thread safety annotation — inserted first so it appears last (bottom-most)
# among all annotations.
if entry := threadsafety_data.get(name):
annotation = _threadsafety_annotation(entry.level)
node.insert(0, annotation)

# Stable ABI annotation.
if record := stable_abi_data.get(name):
if ROLE_TO_OBJECT_TYPE[record.role] != objtype:
Expand Down Expand Up @@ -256,6 +301,35 @@ def _unstable_api_annotation() -> nodes.admonition:
)


_THREADSAFETY_DISPLAY = {
"incompatible": "Not safe to call from multiple threads.",
"compatible": "Safe to call from multiple threads with external synchronization only.",
"safe": "Safe for concurrent use.",
}

# Maps each thread safety level to the glossary term it should link to.
_THREADSAFETY_TERM = {
"incompatible": "thread-incompatible",
"compatible": "thread-compatible",
"safe": "thread-safe",
}


def _threadsafety_annotation(level: str) -> nodes.emphasis:
display = sphinx_gettext(_THREADSAFETY_DISPLAY[level])
ref_node = addnodes.pending_xref(
display,
nodes.Text(display),
refdomain="std",
reftarget=_THREADSAFETY_TERM[level],
reftype="term",
refexplicit="True",
)
prefix = sphinx_gettext("Thread safety:") + " "
classes = [f"threadsafety-{level}"]
return nodes.emphasis("", prefix, ref_node, classes=classes)


def _return_value_annotation(result_refs: int | None) -> nodes.emphasis:
classes = ["refcount"]
if result_refs is None:
Expand Down Expand Up @@ -342,11 +416,15 @@ def init_annotations(app: Sphinx) -> None:
state["stable_abi_data"] = read_stable_abi_data(
Path(app.srcdir, app.config.stable_abi_file)
)
state["threadsafety_data"] = read_threadsafety_data(
Path(app.srcdir, app.config.threadsafety_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_config_value("threadsafety_file", "", "env", types={str})
app.add_directive("limited-api-list", LimitedAPIList)
app.add_directive("corresponding-type-slot", CorrespondingTypeSlot)
app.connect("builder-inited", init_annotations)
Expand Down
Loading