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,38 @@ 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+ "safe" ,
131+ })
132+
133+
134+ def read_threadsafety_data (threadsafety_filename : Path ) -> dict [str , ThreadSafetyEntry ]:
135+ threadsafety_data = {}
136+ for line in threadsafety_filename .read_text (encoding = "utf8" ).splitlines ():
137+ line = line .strip ()
138+ if not line or line .startswith ("#" ):
139+ continue
140+ # Each line is of the form: function_name : level : [comment]
141+ parts = line .split (":" , 2 )
142+ if len (parts ) < 2 :
143+ raise ValueError (f"Wrong field count in { line !r} " )
144+ name , level = parts [0 ].strip (), parts [1 ].strip ()
145+ if level not in _VALID_THREADSAFETY_LEVELS :
146+ raise ValueError (
147+ f"Unknown thread safety level { level !r} for { name !r} . "
148+ f"Valid levels: { sorted (_VALID_THREADSAFETY_LEVELS )} "
149+ )
150+ threadsafety_data [name ] = ThreadSafetyEntry (name = name , level = level )
151+ return threadsafety_data
152+
153+
116154def add_annotations (app : Sphinx , doctree : nodes .document ) -> None :
117155 state = app .env .domaindata ["c_annotations" ]
118156 refcount_data = state ["refcount_data" ]
119157 stable_abi_data = state ["stable_abi_data" ]
158+ threadsafety_data = state ["threadsafety_data" ]
120159 for node in doctree .findall (addnodes .desc_content ):
121160 par = node .parent
122161 if par ["domain" ] != "c" :
@@ -126,6 +165,12 @@ def add_annotations(app: Sphinx, doctree: nodes.document) -> None:
126165 name = par [0 ]["ids" ][0 ].removeprefix ("c." )
127166 objtype = par ["objtype" ]
128167
168+ # Thread safety annotation — inserted first so it appears last (bottom-most)
169+ # among all annotations.
170+ if entry := threadsafety_data .get (name ):
171+ annotation = _threadsafety_annotation (entry .level )
172+ node .insert (0 , annotation )
173+
129174 # Stable ABI annotation.
130175 if record := stable_abi_data .get (name ):
131176 if ROLE_TO_OBJECT_TYPE [record .role ] != objtype :
@@ -256,6 +301,35 @@ def _unstable_api_annotation() -> nodes.admonition:
256301 )
257302
258303
304+ _THREADSAFETY_DISPLAY = {
305+ "incompatible" : "Not safe to call from multiple threads." ,
306+ "compatible" : "Safe to call from multiple threads with external synchronization only." ,
307+ "safe" : "Safe for concurrent use." ,
308+ }
309+
310+ # Maps each thread safety level to the glossary term it should link to.
311+ _THREADSAFETY_TERM = {
312+ "incompatible" : "thread-incompatible" ,
313+ "compatible" : "thread-compatible" ,
314+ "safe" : "thread-safe" ,
315+ }
316+
317+
318+ def _threadsafety_annotation (level : str ) -> nodes .emphasis :
319+ display = sphinx_gettext (_THREADSAFETY_DISPLAY [level ])
320+ ref_node = addnodes .pending_xref (
321+ display ,
322+ nodes .Text (display ),
323+ refdomain = "std" ,
324+ reftarget = _THREADSAFETY_TERM [level ],
325+ reftype = "term" ,
326+ refexplicit = "True" ,
327+ )
328+ prefix = sphinx_gettext ("Thread safety:" ) + " "
329+ classes = [f"threadsafety-{ level } " ]
330+ return nodes .emphasis ("" , prefix , ref_node , classes = classes )
331+
332+
259333def _return_value_annotation (result_refs : int | None ) -> nodes .emphasis :
260334 classes = ["refcount" ]
261335 if result_refs is None :
@@ -342,11 +416,15 @@ def init_annotations(app: Sphinx) -> None:
342416 state ["stable_abi_data" ] = read_stable_abi_data (
343417 Path (app .srcdir , app .config .stable_abi_file )
344418 )
419+ state ["threadsafety_data" ] = read_threadsafety_data (
420+ Path (app .srcdir , app .config .threadsafety_file )
421+ )
345422
346423
347424def setup (app : Sphinx ) -> ExtensionMetadata :
348425 app .add_config_value ("refcount_file" , "" , "env" , types = {str })
349426 app .add_config_value ("stable_abi_file" , "" , "env" , types = {str })
427+ app .add_config_value ("threadsafety_file" , "" , "env" , types = {str })
350428 app .add_directive ("limited-api-list" , LimitedAPIList )
351429 app .add_directive ("corresponding-type-slot" , CorrespondingTypeSlot )
352430 app .connect ("builder-inited" , init_annotations )
0 commit comments