33* Reference count annotations for C API functions.
44* Stable ABI annotations
55* Limited API annotations
6+ * Thread safety annotations for C API functions.
67
78Configuration:
89* Set ``refcount_file`` to the path to the reference count data file.
910* Set ``stable_abi_file`` to the path to stable ABI list.
11+ * Set ``threadsafety_file`` to the path to the thread safety data file.
1012"""
1113
1214from __future__ import annotations
@@ -48,6 +50,15 @@ class RefCountEntry:
4850 result_refs : int | None = None
4951
5052
53+ @dataclasses .dataclass (frozen = True , slots = True )
54+ class ThreadSafetyEntry :
55+ # Name of the function.
56+ name : str
57+ # Thread safety level.
58+ # One of: 'incompatible', 'compatible', 'safe'.
59+ level : str
60+
61+
5162@dataclasses .dataclass (frozen = True , slots = True )
5263class StableABIEntry :
5364 # Role of the object.
@@ -113,10 +124,42 @@ def read_stable_abi_data(stable_abi_file: Path) -> dict[str, StableABIEntry]:
113124 return stable_abi_data
114125
115126
127+ _VALID_THREADSAFETY_LEVELS = frozenset ({
128+ "incompatible" ,
129+ "compatible" ,
130+ "distinct" ,
131+ "shared" ,
132+ "atomic" ,
133+ })
134+
135+
136+ def read_threadsafety_data (
137+ threadsafety_filename : Path ,
138+ ) -> dict [str , ThreadSafetyEntry ]:
139+ threadsafety_data = {}
140+ for line in threadsafety_filename .read_text (encoding = "utf8" ).splitlines ():
141+ line = line .strip ()
142+ if not line or line .startswith ("#" ):
143+ continue
144+ # Each line is of the form: function_name : level : [comment]
145+ parts = line .split (":" , 2 )
146+ if len (parts ) < 2 :
147+ raise ValueError (f"Wrong field count in { line !r} " )
148+ name , level = parts [0 ].strip (), parts [1 ].strip ()
149+ if level not in _VALID_THREADSAFETY_LEVELS :
150+ raise ValueError (
151+ f"Unknown thread safety level { level !r} for { name !r} . "
152+ f"Valid levels: { sorted (_VALID_THREADSAFETY_LEVELS )} "
153+ )
154+ threadsafety_data [name ] = ThreadSafetyEntry (name = name , level = level )
155+ return threadsafety_data
156+
157+
116158def add_annotations (app : Sphinx , doctree : nodes .document ) -> None :
117159 state = app .env .domaindata ["c_annotations" ]
118160 refcount_data = state ["refcount_data" ]
119161 stable_abi_data = state ["stable_abi_data" ]
162+ threadsafety_data = state ["threadsafety_data" ]
120163 for node in doctree .findall (addnodes .desc_content ):
121164 par = node .parent
122165 if par ["domain" ] != "c" :
@@ -126,6 +169,12 @@ def add_annotations(app: Sphinx, doctree: nodes.document) -> None:
126169 name = par [0 ]["ids" ][0 ].removeprefix ("c." )
127170 objtype = par ["objtype" ]
128171
172+ # Thread safety annotation — inserted first so it appears last (bottom-most)
173+ # among all annotations.
174+ if entry := threadsafety_data .get (name ):
175+ annotation = _threadsafety_annotation (entry .level )
176+ node .insert (0 , annotation )
177+
129178 # Stable ABI annotation.
130179 if record := stable_abi_data .get (name ):
131180 if ROLE_TO_OBJECT_TYPE [record .role ] != objtype :
@@ -256,6 +305,46 @@ def _unstable_api_annotation() -> nodes.admonition:
256305 )
257306
258307
308+ def _threadsafety_annotation (level : str ) -> nodes .emphasis :
309+ match level :
310+ case "incompatible" :
311+ display = sphinx_gettext ("Not safe to call from multiple threads." )
312+ reftarget = "threadsafety-level-incompatible"
313+ case "compatible" :
314+ display = sphinx_gettext (
315+ "Safe to call from multiple threads"
316+ " with external synchronization only."
317+ )
318+ reftarget = "threadsafety-level-compatible"
319+ case "distinct" :
320+ display = sphinx_gettext (
321+ "Safe to call without external synchronization"
322+ " on distinct objects."
323+ )
324+ reftarget = "threadsafety-level-distinct"
325+ case "shared" :
326+ display = sphinx_gettext (
327+ "Safe for concurrent use on the same object."
328+ )
329+ reftarget = "threadsafety-level-shared"
330+ case "atomic" :
331+ display = sphinx_gettext ("Atomic." )
332+ reftarget = "threadsafety-level-atomic"
333+ case _:
334+ raise AssertionError (f"Unknown thread safety level { level !r} " )
335+ ref_node = addnodes .pending_xref (
336+ display ,
337+ nodes .Text (display ),
338+ refdomain = "std" ,
339+ reftarget = reftarget ,
340+ reftype = "ref" ,
341+ refexplicit = "True" ,
342+ )
343+ prefix = sphinx_gettext ("Thread safety:" ) + " "
344+ classes = ["threadsafety" , f"threadsafety-{ level } " ]
345+ return nodes .emphasis ("" , prefix , ref_node , classes = classes )
346+
347+
259348def _return_value_annotation (result_refs : int | None ) -> nodes .emphasis :
260349 classes = ["refcount" ]
261350 if result_refs is None :
@@ -342,11 +431,15 @@ def init_annotations(app: Sphinx) -> None:
342431 state ["stable_abi_data" ] = read_stable_abi_data (
343432 Path (app .srcdir , app .config .stable_abi_file )
344433 )
434+ state ["threadsafety_data" ] = read_threadsafety_data (
435+ Path (app .srcdir , app .config .threadsafety_file )
436+ )
345437
346438
347439def setup (app : Sphinx ) -> ExtensionMetadata :
348440 app .add_config_value ("refcount_file" , "" , "env" , types = {str })
349441 app .add_config_value ("stable_abi_file" , "" , "env" , types = {str })
442+ app .add_config_value ("threadsafety_file" , "" , "env" , types = {str })
350443 app .add_directive ("limited-api-list" , LimitedAPIList )
351444 app .add_directive ("corresponding-type-slot" , CorrespondingTypeSlot )
352445 app .connect ("builder-inited" , init_annotations )
0 commit comments