Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-librt for openSUSE:Factory 
checked in at 2026-08-24 12:01:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-librt (Old)
 and      /work/SRC/openSUSE:Factory/.python-librt.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-librt"

Mon Aug 24 12:01:32 2026 rev:7 rq:1373039 version:0.15.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-librt/python-librt.changes        
2026-07-02 20:11:20.073300322 +0200
+++ /work/SRC/openSUSE:Factory/.python-librt.new.1258/python-librt.changes      
2026-08-24 12:02:07.321712846 +0200
@@ -1,0 +2,7 @@
+Sat Aug 22 09:34:27 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.15.0:
+  * prepare for sentinel support
+  * Sync mypy and bump version
+
+-------------------------------------------------------------------

Old:
----
  librt-0.12.0.tar.gz

New:
----
  librt-0.15.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-librt.spec ++++++
--- /var/tmp/diff_new_pack.So19hT/_old  2026-08-24 12:02:08.231744952 +0200
+++ /var/tmp/diff_new_pack.So19hT/_new  2026-08-24 12:02:08.236745128 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-librt
-Version:        0.12.0
+Version:        0.15.0
 Release:        0
 Summary:        Mypyc runtime library
 License:        MIT

++++++ librt-0.12.0.tar.gz -> librt-0.15.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/CPy.h new/librt-0.15.0/CPy.h
--- old/librt-0.12.0/CPy.h      2026-06-30 17:48:02.000000000 +0200
+++ new/librt-0.15.0/CPy.h      2026-08-07 12:12:17.000000000 +0200
@@ -664,16 +664,68 @@
 // List operations
 
 
-PyObject *CPyList_Build(Py_ssize_t len, ...);
+#ifndef Py_GIL_DISABLED
+
 PyObject *CPyList_GetItem(PyObject *list, CPyTagged index);
 PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index);
+PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index);
+bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value);
+bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value);
+
+#else
+
+PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index);
+bool CPyList_SetItem_(PyObject *list, CPyTagged index, PyObject *value);
+
+static inline PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) {
+    if (likely(CPyTagged_CheckShort(index) && !CPyTagged_IsNegative(index))) {
+        // Inlined fast path
+        Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
+        return PyList_GetItemRef(list, n);
+    } else {
+        return CPyList_GetItem_(list, index);
+    }
+}
+
+static inline PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index) {
+    Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
+    if (n < 0) {
+        n += PyList_GET_SIZE(list);
+    }
+    return PyList_GetItemRef(list, n);
+}
+
+static inline PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) {
+    if (index < 0) {
+        index += PyList_GET_SIZE(list);
+    }
+    return PyList_GetItemRef(list, index);
+}
+
+static inline bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject 
*value) {
+    if (likely(CPyTagged_CheckShort(index) && !CPyTagged_IsNegative(index))) {
+        // Inlined fast path
+        Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
+        return PyList_SetItem(list, n, value) >= 0;
+    } else {
+        return CPyList_SetItem_(list, index, value);
+    }
+}
+
+static inline bool CPyList_SetItemInt64(PyObject *list, int64_t index, 
PyObject *value) {
+    if (index < 0) {
+        index += PyList_GET_SIZE(list);
+    }
+    return PyList_SetItem(list, index, value) >= 0;
+}
+
+#endif
+
 PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index);
 PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index);
-PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index);
 PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index);
-bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value);
 void CPyList_SetItemUnsafe(PyObject *list, Py_ssize_t index, PyObject *value);
-bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value);
+PyObject *CPyList_Build(Py_ssize_t len, ...);
 PyObject *CPyList_PopLast(PyObject *obj);
 PyObject *CPyList_Pop(PyObject *obj, CPyTagged index);
 CPyTagged CPyList_Count(PyObject *obj, PyObject *value);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/PKG-INFO new/librt-0.15.0/PKG-INFO
--- old/librt-0.12.0/PKG-INFO   2026-06-30 17:48:05.565397300 +0200
+++ new/librt-0.15.0/PKG-INFO   2026-08-07 12:12:21.037635300 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: librt
-Version: 0.12.0
+Version: 0.15.0
 Summary: Mypyc runtime library
 Author-email: Jukka Lehtosalo <[email protected]>, Ivan Levkivskyi 
<[email protected]>
 License-Expression: MIT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/function_wrapper.c 
new/librt-0.15.0/function_wrapper.c
--- old/librt-0.12.0/function_wrapper.c 2026-06-30 17:48:02.000000000 +0200
+++ new/librt-0.15.0/function_wrapper.c 2026-08-07 12:12:17.000000000 +0200
@@ -145,9 +145,19 @@
     {0, 0, 0, 0, 0}
 };
 
-static PyObject* CPy_PyMethod_New(PyObject *func, PyObject *self, PyObject 
*typ) {
-    (void)typ;
-    if (!self) {
+static PyObject* CPyFunction_descr_get(PyObject *func, PyObject *self, 
PyObject *typ) {
+    int flags = ((PyCFunctionObject *)func)->m_ml->ml_flags;
+    if (flags & METH_CLASS) {
+        if (typ == NULL) {
+            if (self == NULL) {
+                PyErr_SetString(PyExc_TypeError, "__get__(None, None) is 
invalid");
+                return NULL;
+            }
+            typ = (PyObject *)Py_TYPE(self);
+        }
+        return PyMethod_New(func, typ);
+    }
+    if (!self || (flags & METH_STATIC)) {
         Py_INCREF(func);
         return func;
     }
@@ -162,7 +172,7 @@
     {Py_tp_clear, (void *)CPyFunction_clear},
     {Py_tp_members, (void *)CPyFunction_members},
     {Py_tp_getset, (void *)CPyFunction_getsets},
-    {Py_tp_descr_get, (void *)CPy_PyMethod_New},
+    {Py_tp_descr_get, (void *)CPyFunction_descr_get},
     {0, 0},
 };
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/internal/librt_internal.c 
new/librt-0.15.0/internal/librt_internal.c
--- old/librt-0.12.0/internal/librt_internal.c  2026-06-30 17:48:02.000000000 
+0200
+++ new/librt-0.15.0/internal/librt_internal.c  2026-08-07 12:12:17.000000000 
+0200
@@ -930,6 +930,7 @@
 #define LITERAL_BYTES 5
 #define LITERAL_FLOAT 6
 #define LITERAL_COMPLEX 7
+#define LITERAL_SENTINEL 8
 
 // Supported builtin collections.
 #define LIST_GEN 20
@@ -1161,6 +1162,11 @@
         return _skip(data, 8);
     if (tag == LITERAL_COMPLEX)
         return _skip(data, 16);
+    if (tag == LITERAL_SENTINEL) {
+        if (unlikely(_skip_str_bytes(data) == CPY_NONE_ERROR))
+            return CPY_NONE_ERROR;
+        return _skip_str_bytes(data);
+    }
     PyErr_Format(PyExc_ValueError, "Unsupported tag: %d", tag);
     return CPY_NONE_ERROR;
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/librt/threading.pyi 
new/librt-0.15.0/librt/threading.pyi
--- old/librt-0.12.0/librt/threading.pyi        1970-01-01 01:00:00.000000000 
+0100
+++ new/librt-0.15.0/librt/threading.pyi        2026-08-07 12:12:09.000000000 
+0200
@@ -0,0 +1,16 @@
+from types import TracebackType
+from typing import final
+
+@final
+class Lock:
+    def acquire(self, blocking: bool = True) -> bool: ...
+    def release(self) -> None: ...
+    def locked(self) -> bool: ...
+    def __enter__(self) -> bool: ...
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_val: BaseException | None,
+        exc_tb: TracebackType | None,
+        /,
+    ) -> None: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/librt.egg-info/PKG-INFO 
new/librt-0.15.0/librt.egg-info/PKG-INFO
--- old/librt-0.12.0/librt.egg-info/PKG-INFO    2026-06-30 17:48:05.000000000 
+0200
+++ new/librt-0.15.0/librt.egg-info/PKG-INFO    2026-08-07 12:12:20.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: librt
-Version: 0.12.0
+Version: 0.15.0
 Summary: Mypyc runtime library
 Author-email: Jukka Lehtosalo <[email protected]>, Ivan Levkivskyi 
<[email protected]>
 License-Expression: MIT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/librt.egg-info/SOURCES.txt 
new/librt-0.15.0/librt.egg-info/SOURCES.txt
--- old/librt-0.12.0/librt.egg-info/SOURCES.txt 2026-06-30 17:48:05.000000000 
+0200
+++ new/librt-0.15.0/librt.egg-info/SOURCES.txt 2026-08-07 12:12:20.000000000 
+0200
@@ -140,6 +140,10 @@
 ./strings/librt_strings_api.c
 ./strings/librt_strings_api.h
 ./strings/librt_strings_common.h
+./threading/librt_threading.c
+./threading/librt_threading.h
+./threading/librt_threading_api.c
+./threading/librt_threading_api.h
 ./time/librt_time.c
 ./time/librt_time.h
 ./time/librt_time_api.c
@@ -222,6 +226,7 @@
 librt/py.typed
 librt/random.pyi
 librt/strings.pyi
+librt/threading.pyi
 librt/time.pyi
 librt/vecs.pyi
 librt.egg-info/PKG-INFO
@@ -237,6 +242,10 @@
 strings/librt_strings_api.c
 strings/librt_strings_api.h
 strings/librt_strings_common.h
+threading/librt_threading.c
+threading/librt_threading.h
+threading/librt_threading_api.c
+threading/librt_threading_api.h
 time/librt_time.c
 time/librt_time.h
 time/librt_time_api.c
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/list_ops.c new/librt-0.15.0/list_ops.c
--- old/librt-0.12.0/list_ops.c 2026-06-30 17:48:02.000000000 +0200
+++ new/librt-0.15.0/list_ops.c 2026-08-07 12:12:17.000000000 +0200
@@ -49,6 +49,8 @@
     return PyObject_CallMethodNoArgs(list, mypyc_interned_str.copy);
 }
 
+#ifndef Py_GIL_DISABLED
+
 PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index) {
     Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
     Py_ssize_t size = PyList_GET_SIZE(list);
@@ -69,24 +71,6 @@
     return result;
 }
 
-PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index) {
-    Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
-    Py_ssize_t size = PyList_GET_SIZE(list);
-    if (n >= 0) {
-        if (n >= size) {
-            PyErr_SetString(PyExc_IndexError, "list index out of range");
-            return NULL;
-        }
-    } else {
-        n += size;
-        if (n < 0) {
-            PyErr_SetString(PyExc_IndexError, "list index out of range");
-            return NULL;
-        }
-    }
-    return PyList_GET_ITEM(list, n);
-}
-
 PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) {
     if (CPyTagged_CheckShort(index)) {
         Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
@@ -112,29 +96,6 @@
     }
 }
 
-PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index) {
-    if (CPyTagged_CheckShort(index)) {
-        Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
-        Py_ssize_t size = PyList_GET_SIZE(list);
-        if (n >= 0) {
-            if (n >= size) {
-                PyErr_SetString(PyExc_IndexError, "list index out of range");
-                return NULL;
-            }
-        } else {
-            n += size;
-            if (n < 0) {
-                PyErr_SetString(PyExc_IndexError, "list index out of range");
-                return NULL;
-            }
-        }
-        return PyList_GET_ITEM(list, n);
-    } else {
-        PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG);
-        return NULL;
-    }
-}
-
 PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) {
     size_t size = PyList_GET_SIZE(list);
     if (likely((uint64_t)index < size)) {
@@ -156,23 +117,6 @@
     return result;
 }
 
-PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index) {
-    size_t size = PyList_GET_SIZE(list);
-    if (likely((uint64_t)index < size)) {
-        return PyList_GET_ITEM(list, index);
-    }
-    if (index >= 0) {
-        PyErr_SetString(PyExc_IndexError, "list index out of range");
-        return NULL;
-    }
-    index += size;
-    if (index < 0) {
-        PyErr_SetString(PyExc_IndexError, "list index out of range");
-        return NULL;
-    }
-    return PyList_GET_ITEM(list, index);
-}
-
 bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value) {
     if (CPyTagged_CheckShort(index)) {
         Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
@@ -220,6 +164,95 @@
     return true;
 }
 
+#else /* Py_GIL_DISABLED */
+
+PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index) {
+    if (CPyTagged_CheckShort(index)) {
+        Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
+        if (n < 0) {
+            n += PyList_GET_SIZE(list);
+        }
+        return PyList_GetItemRef(list, n);
+    } else {
+        PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG);
+        return NULL;
+    }
+}
+
+bool CPyList_SetItem_(PyObject *list, CPyTagged index, PyObject *value) {
+    if (CPyTagged_CheckShort(index)) {
+        Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
+        Py_ssize_t size = PyList_GET_SIZE(list);
+        if (n < 0) {
+            n += size;
+        }
+        return PyList_SetItem(list, n, value) >= 0;
+    } else {
+        PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG);
+        return false;
+    }
+}
+
+#endif
+
+PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index) {
+    Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
+    Py_ssize_t size = PyList_GET_SIZE(list);
+    if (n >= 0) {
+        if (n >= size) {
+            PyErr_SetString(PyExc_IndexError, "list index out of range");
+            return NULL;
+        }
+    } else {
+        n += size;
+        if (n < 0) {
+            PyErr_SetString(PyExc_IndexError, "list index out of range");
+            return NULL;
+        }
+    }
+    return PyList_GET_ITEM(list, n);
+}
+
+PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index) {
+    if (CPyTagged_CheckShort(index)) {
+        Py_ssize_t n = CPyTagged_ShortAsSsize_t(index);
+        Py_ssize_t size = PyList_GET_SIZE(list);
+        if (n >= 0) {
+            if (n >= size) {
+                PyErr_SetString(PyExc_IndexError, "list index out of range");
+                return NULL;
+            }
+        } else {
+            n += size;
+            if (n < 0) {
+                PyErr_SetString(PyExc_IndexError, "list index out of range");
+                return NULL;
+            }
+        }
+        return PyList_GET_ITEM(list, n);
+    } else {
+        PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG);
+        return NULL;
+    }
+}
+
+PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index) {
+    size_t size = PyList_GET_SIZE(list);
+    if (likely((uint64_t)index < size)) {
+        return PyList_GET_ITEM(list, index);
+    }
+    if (index >= 0) {
+        PyErr_SetString(PyExc_IndexError, "list index out of range");
+        return NULL;
+    }
+    index += size;
+    if (index < 0) {
+        PyErr_SetString(PyExc_IndexError, "list index out of range");
+        return NULL;
+    }
+    return PyList_GET_ITEM(list, index);
+}
+
 // This function should only be used to fill in brand new lists.
 void CPyList_SetItemUnsafe(PyObject *list, Py_ssize_t index, PyObject *value) {
     PyList_SET_ITEM(list, index, value);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/mypyc_util.h 
new/librt-0.15.0/mypyc_util.h
--- old/librt-0.12.0/mypyc_util.h       2026-06-30 17:48:02.000000000 +0200
+++ new/librt-0.15.0/mypyc_util.h       2026-08-07 12:12:17.000000000 +0200
@@ -156,6 +156,10 @@
     return x << 1;
 }
 
+static inline int CPyTagged_IsNegative(CPyTagged x) {
+    return ((Py_ssize_t)x) < 0;
+}
+
 // Are we targeting Python 3.X or newer?
 #define CPY_3_11_FEATURES (PY_VERSION_HEX >= 0x030b0000)
 #define CPY_3_12_FEATURES (PY_VERSION_HEX >= 0x030c0000)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/pyproject.toml 
new/librt-0.15.0/pyproject.toml
--- old/librt-0.12.0/pyproject.toml     2026-06-30 17:47:54.000000000 +0200
+++ new/librt-0.15.0/pyproject.toml     2026-08-07 12:12:09.000000000 +0200
@@ -19,7 +19,7 @@
     {name = "Jukka Lehtosalo", email = "[email protected]"},
     {name = "Ivan Levkivskyi", email = "[email protected]"},
 ]
-version = "0.12.0"
+version = "0.15.0"
 license = "MIT"
 classifiers = [
   "Development Status :: 3 - Alpha",
@@ -50,8 +50,10 @@
 build-frontend = "build"
 linux.manylinux-x86_64-image = "manylinux_2_28"
 linux.manylinux-aarch64-image = "manylinux_2_28"
+linux.manylinux-ppc64le-image = "manylinux_2_28"
 linux.musllinux-x86_64-image = "musllinux_1_2"
 linux.musllinux-aarch64-image = "musllinux_1_2"
+linux.musllinux-ppc64le-image = "musllinux_1_2"
 
 build-verbosity = 1
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/pythonsupport.c 
new/librt-0.15.0/pythonsupport.c
--- old/librt-0.12.0/pythonsupport.c    2026-06-30 17:48:02.000000000 +0200
+++ new/librt-0.15.0/pythonsupport.c    2026-08-07 12:12:17.000000000 +0200
@@ -5,6 +5,27 @@
 
 #include "pythonsupport.h"
 
+#ifdef Py_GIL_DISABLED
+// Cold slow path of CPy_GetAttrRef (declared in pythonsupport.h). Reached only
+// when the inline fast-path try-incref fails: the value is owned by another
+// thread, so taking a reference requires an atomic shared-refcount operation.
+// Kept out-of-line so the inline fast path stays small.
+//
+// First try the lock-free shared-refcount CAS. If the value has not had
+// maybe-weakref set yet (for example, it was published by CPy_InitAttrRef), 
force
+// a cross-thread reference via _Py_NewRefWithLock, which cannot fail and sets
+// maybe-weakref so subsequent reads take the fast path. The value was already
+// observed in the field by CPy_GetAttrRef; CPy_SetAttrRef's QSBR-delayed 
decref
+// keeps any replaced value alive long enough for this reader.
+CPy_NOINLINE
+PyObject *CPy_GetAttrRefSlow(PyObject *v) {
+    if (_Py_TryIncRefShared(v)) {
+        return v;
+    }
+    return _Py_NewRefWithLock(v);  // sets maybe-weakref; cannot fail
+}
+#endif
+
 /////////////////////////////////////////
 // Adapted from bltinmodule.c in Python 3.7.0
 PyObject*
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/pythonsupport.h 
new/librt-0.15.0/pythonsupport.h
--- old/librt-0.12.0/pythonsupport.h    2026-06-30 17:48:02.000000000 +0200
+++ new/librt-0.15.0/pythonsupport.h    2026-08-07 12:12:17.000000000 +0200
@@ -23,6 +23,10 @@
 #include "internal/pycore_setobject.h"  // _PySet_Update
 #endif
 
+#ifdef Py_GIL_DISABLED
+#include "internal/pycore_object.h"  // _Py_TryIncrefFast, _Py_TryIncRefShared
+#endif
+
 #if CPY_3_12_FEATURES
 #include "internal/pycore_frame.h"
 #endif
@@ -34,6 +38,136 @@
 } // why isn't emacs smart enough to not indent this
 #endif
 
+#ifdef Py_GIL_DISABLED
+// Read a native attribute that is a single reference-counted 'PyObject *' 
field,
+// returning a new reference (or NULL if the field is NULL/undefined).
+//
+// On free-threaded builds a plain load followed by an incref races with a
+// concurrent setter that may decref the old value to zero and free it before 
the
+// incref runs (use-after-free). CPy_SetAttrRef avoids that by reclaiming old
+// values through QSBR-delayed decref, so a value observed in the field remains
+// safe to touch while this reader is running.
+//
+// Only the hot case is inlined here: an incref of a value owned by this 
thread or
+// immortal, via '_Py_TryIncrefFast' (no CAS, no loop). Everything colder -- 
the
+// cross-thread shared-refcount CAS and the _Py_NewRefWithLock fallback -- 
lives
+// out-of-line in 'CPy_GetAttrRefSlow'. Splitting it this way keeps each call
+// site's fast path small enough to inline, which is measurably faster than
+// letting the compiler auto-out-line the whole helper (that merges every read
+// site's branch history into one shared copy and mispredicts). It is only 
used in
+// free-threaded builds; the default (GIL) build keeps the plain load + incref
+// generated inline by mypyc.
+PyObject *CPy_GetAttrRefSlow(PyObject *v);
+
+static inline PyObject *CPy_GetAttrRef(PyObject **field) {
+    PyObject *v = (PyObject *)_Py_atomic_load_ptr_acquire(field);
+    if (v == NULL) {
+        return NULL;
+    }
+    if (_Py_TryIncrefFast(v)) {
+        return v;
+    }
+    return CPy_GetAttrRefSlow(v);
+}
+
+// Read a native attribute that is a single reference-counted 'PyObject *' 
field
+// AND is Final (assigned once during construction, never rebound -- mypyc 
emits no
+// setter for it), returning a new reference (or NULL if undefined).
+//
+// A Final attribute has no concurrent writer after 'self' is published, so the
+// use-after-free race that CPy_GetAttrRef guards against cannot happen: the 
field
+// holds a strong reference for the object's whole lifetime, and any thread 
reading
+// it necessarily holds 'self', which keeps the value alive. So the try-incref 
+
+// _Py_NewRefWithLock fallback are unnecessary here -- a plain load + 
Py_INCREF is
+// safe. A cross-thread Py_INCREF is an unconditional
+// atomic add on ob_ref_shared, so (unlike CPy_GetAttrRef's try-incref) it 
needs no
+// maybe-weakref and has no slow path. The load is relaxed rather than 
acquire: the
+// reader reached 'self' through a synchronization edge (self's own 
publication)
+// that already ordered the construction stores before it, exactly as with
+// CPy_InitAttrRef's relaxed store. Relaxed keeps it TSan-clean at zero cost 
(plain
+// mov/ldr).
+static inline PyObject *CPy_GetAttrRefFinal(PyObject **field) {
+    PyObject *v = (PyObject *)_Py_atomic_load_ptr_relaxed(field);
+    if (v != NULL) {
+        Py_INCREF(v);
+    }
+    return v;
+}
+
+// Reclaim the previous value of a native attribute after it has been replaced.
+//
+// CPy_GetAttrRef reads the field optimistically without holding any lock the
+// writer also takes, so a reader can load the old pointer and then try to 
take a
+// reference after this store. The old value must therefore stay alive until 
every
+// thread has passed a quiescent point, which is exactly what a QSBR-deferred
+// decref guarantees. So all mortal old values are
+// reclaimed via _PyObject_XDecRefDelayed, matching CPython's own 
replace-a-slot
+// paths (e.g. _PyObject_SetDict / _PyObject_SetManagedDict).
+//
+// We deliberately do NOT take a "local refcount > 1, owned by this thread" 
fast
+// path: dropping the field's reference is not the only decref of the object, 
so a
+// non-freeing local decrement here does not prevent an unrelated reference 
holder
+// from driving the object to zero (a plain, non-deferred Py_DECREF -> 
_Py_Dealloc)
+// while an in-flight reader still holds the stale pointer -- a use-after-free 
that
+// only QSBR deferral closes. Immortal objects are never freed, so skipping 
their
+// decref entirely is safe and avoids queuing a no-op onto the delayed-free 
list.
+static inline void CPy_DecRefAttrOld(PyObject *op) {
+    if (op == NULL) {
+        return;
+    }
+    if (_Py_IsImmortal(op)) {
+        return;
+    }
+    _PyObject_XDecRefDelayed(op);
+}
+
+// Set a native attribute that is a single reference-counted 'PyObject *' 
field,
+// stealing the reference to 'value' (which may be NULL to delete the 
attribute)
+// and safely reclaiming the previous value.
+//
+// Memory safety does NOT depend on SetMaybeWeakref here, so (unlike an earlier
+// version) we do not call it. Two things keep this safe:
+//   - The old value is reclaimed via CPy_DecRefAttrOld, a QSBR-deferred 
decref, so
+//     it cannot be freed while an in-flight CPy_GetAttrRef still holds the 
stale
+//     pointer.
+//   - A concurrent cross-thread reader whose inline fast-path try-incref 
fails on
+//     an unflagged 'value' still cannot fail and never blocks on this writer:
+//     CPy_GetAttrRef falls into CPy_GetAttrRefSlow, which is fully lock-free. 
It
+//     retries with a lock-free shared-refcount CAS (_Py_TryIncRefShared) and, 
only
+//     if that also fails, forces a reference via _Py_NewRefWithLock, which 
cannot
+//     fail and lazily sets maybe-weakref so later cross-thread reads take the 
CAS.
+// This mirrors CPy_InitAttrRef, which already omits SetMaybeWeakref for the 
same
+// reason. Setting the flag here would only be a possible performance tuning 
knob
+// (it would let that first cross-thread reader succeed on the cheaper
+// _Py_TryIncRefShared CAS instead of falling through to _Py_NewRefWithLock); 
it is
+// not needed for correctness. The atomic exchange publishes the new pointer 
and
+// hands back the old one without a writer/writer race.
+static inline void CPy_SetAttrRef(PyObject **field, PyObject *value) {
+    PyObject *old = (PyObject *)_Py_atomic_exchange_ptr(field, value);
+    CPy_DecRefAttrOld(old);
+}
+
+// Initialize a native attribute that is known to be previously undefined 
(NULL),
+// stealing the reference to 'value'.
+//
+// Initializer stores only happen while 'self' is still thread-local (the
+// attribute-definedness analysis marks a SetAttr as an initializer only before
+// 'self' can leak -- see mypyc/analysis/attrdefined.py). So there is no old 
value
+// to reclaim, no competing writer, and the field store is not itself the
+// publication point: 'self' is published later (when it escapes __init__ or is
+// returned), and that publication carries the release barrier making all the
+// construction stores visible. A relaxed store therefore suffices.
+//
+// Unlike CPy_SetAttrRef, this deliberately does NOT call SetMaybeWeakref (its 
CAS
+// is pure overhead here, ~+2.6ns per fresh store, and construction-heavy code
+// pays it on every attribute of every new object). The cost is moved off this 
hot
+// path onto CPy_GetAttrRef's cold slow path, which sets maybe-weakref lazily 
on
+// the first cross-thread read that needs it.
+static inline void CPy_InitAttrRef(PyObject **field, PyObject *value) {
+    _Py_atomic_store_ptr_relaxed(field, value);
+}
+#endif
+
 PyObject* update_bases(PyObject *bases);
 int init_subclass(PyTypeObject *type, PyObject *kwds);
 
@@ -205,6 +339,25 @@
     Py_ssize_t count = 0;
     Py_ssize_t i;
 
+#ifdef Py_GIL_DISABLED
+    for (i = 0; i < PyList_GET_SIZE(self); i++) {
+        PyObject *item = PyList_GetItemRef((PyObject *)self, i);
+        if (unlikely(item == NULL)) {
+            // Race condition: list shrank between size read and get item
+            if (PyErr_ExceptionMatches(PyExc_IndexError)) {
+                PyErr_Clear();
+                break;
+            }
+            return CPY_INT_TAG;
+        }
+        int cmp = PyObject_RichCompareBool(item, value, Py_EQ);
+        Py_DECREF(item);
+        if (cmp > 0)
+            count++;
+        else if (cmp < 0)
+            return CPY_INT_TAG;
+    }
+#else
     for (i = 0; i < Py_SIZE(self); i++) {
         int cmp = PyObject_RichCompareBool(self->ob_item[i], value, Py_EQ);
         if (cmp > 0)
@@ -212,6 +365,7 @@
         else if (cmp < 0)
             return CPY_INT_TAG;
     }
+#endif
     return CPyTagged_ShortFromSsize_t(count);
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/setup.py new/librt-0.15.0/setup.py
--- old/librt-0.12.0/setup.py   2026-06-30 17:48:02.000000000 +0200
+++ new/librt-0.15.0/setup.py   2026-08-07 12:12:17.000000000 +0200
@@ -164,5 +164,11 @@
                 include_dirs=["."],
                 extra_compile_args=cflags,
             ),
+            Extension(
+                "librt.threading",
+                ["threading/librt_threading.c"],
+                include_dirs=[".", "threading"],
+                extra_compile_args=cflags,
+            ),
         ]
     )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/smoke_tests.py 
new/librt-0.15.0/smoke_tests.py
--- old/librt-0.12.0/smoke_tests.py     2026-06-30 17:47:54.000000000 +0200
+++ new/librt-0.15.0/smoke_tests.py     2026-08-07 12:12:09.000000000 +0200
@@ -23,6 +23,7 @@
 )
 from librt.random import Random, random, randint
 from librt.strings import BytesWriter, StringWriter
+from librt.threading import Lock
 from librt.time import time
 
 Tag = u8
@@ -331,3 +332,26 @@
     for i in range(100):
         val = randint(0, 10)
         assert 0 <= val <= 10
+
+
+def test_lock_acquire_release() -> None:
+    lock = Lock()
+    assert lock.locked() is False
+    assert lock.acquire() is True
+    assert lock.locked() is True
+    lock.release()
+    assert lock.locked() is False
+
+
+def test_lock_acquire_non_blocking() -> None:
+    lock = Lock()
+    assert lock.acquire(blocking=False) is True
+    assert lock.locked() is True
+    lock.release()
+
+
+def test_lock_with_statement() -> None:
+    lock = Lock()
+    with lock:
+        assert lock.locked() is True
+    assert lock.locked() is False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/threading/librt_threading.c 
new/librt-0.15.0/threading/librt_threading.c
--- old/librt-0.12.0/threading/librt_threading.c        1970-01-01 
01:00:00.000000000 +0100
+++ new/librt-0.15.0/threading/librt_threading.c        2026-08-07 
12:12:17.000000000 +0200
@@ -0,0 +1,714 @@
+#include "pythoncapi_compat.h"
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include "librt_threading.h"
+#include "mypyc_util.h"
+
+#if !defined(_WIN32)
+#include <unistd.h>
+#include <errno.h>
+#endif
+
+#if CPY_3_14_FEATURES
+
+// Python 3.14+ (all platforms, with or without the GIL): Use PyMutex (1-byte
+// atomic lock with parking lot). PyMutex gives better interruptibility than 
the
+// pthread fallback, and its fast release path is competitive with sem_t.
+// PyMutex_LockFast, _PyMutex_LockTimed, and _PY_LOCK_DETACH are internal
+// CPython APIs that might change across minor releases.
+#define LOCK_BACKEND_PYMUTEX
+#ifndef Py_BUILD_CORE
+#define Py_BUILD_CORE
+#endif
+#include "internal/pycore_lock.h"
+
+#elif defined(_WIN32)
+
+// Python <3.14 on Windows: Use Slim Reader/Writer Lock
+#define LOCK_BACKEND_SRWLOCK
+#include <windows.h>
+
+#else
+
+// Python <3.14 on POSIX.
+//
+// Prefer a POSIX unnamed semaphore when the platform supports it well and the
+// GIL is enabled, and fall back to a pthread mutex + condition variable
+// otherwise. We use the same test CPython uses to pick its semaphore-based 
lock
+// (see Python/thread_pthread.h): an unnamed semaphore is only usable when
+// sem_init() actually works AND a timed wait is available. Notably this is 
true
+// on Linux but false on macOS (whose sem_init() is a non-functional stub), so
+// macOS uses the mutex+condvar fallback. Free-threaded builds also use the
+// mutex+condvar fallback because the semaphore backend's `locked` bookkeeping
+// relies on GIL serialization.
+#if !defined(Py_GIL_DISABLED) && \
+    defined(_POSIX_SEMAPHORES) && (_POSIX_SEMAPHORES + 0) != -1 && \
+    (defined(HAVE_SEM_TIMEDWAIT) || defined(HAVE_SEM_CLOCKWAIT))
+#define LOCK_BACKEND_SEM
+#include <semaphore.h>
+#else
+#define LOCK_BACKEND_PTHREAD
+#include <pthread.h>
+#endif
+
+#endif
+
+//
+// Lock
+//
+// A fast mutex lock for use from mypyc-compiled code.
+//
+// On Python 3.14+ (all platforms), this uses CPython's PyMutex, a 1-byte 
atomic
+// lock backed by a parking lot for contended waits. PyMutex automatically
+// releases the GIL when blocking.
+//
+// On Python 3.13 and earlier with Windows, this uses an SRWLOCK (Slim
+// Reader/Writer Lock) plus a CONDITION_VARIABLE guarding a `locked` flag. The 
SRWLOCK only
+// protects the flag and is never held across the user's critical section, so
+// release() may be called from a thread other than the acquirer (matching
+// threading.Lock semantics). This mirrors CPython's Windows lock (NRMUTEX in
+// Python/thread_nt.h) and is the Windows twin of the POSIX pthread+condvar
+// backend below.
+//
+// On Python 3.13 and earlier with POSIX systems, there are two backends, both
+// of which allow release() from a thread other than the one that acquired the
+// lock (matching threading.Lock semantics):
+//
+//  - Where unnamed POSIX semaphores work well (e.g. Linux) and the GIL is
+//    enabled, this uses a sem_t initialized to 1: acquire is sem_wait, release
+//    is sem_post. Semaphores have no ownership concept, so cross-thread 
release
+//    is directly well-defined.
+//
+//  - Otherwise (e.g. macOS, whose sem_init() is a non-functional stub, and
+//    free-threaded builds), this uses a pthread mutex + condition variable
+//    guarding a `locked` flag. The mutex only protects the flag and is never
+//    held across the user's critical section, so the OS mutex is always
+//    unlocked on the same thread that locked it.
+//
+
+// ---------- Platform-specific lock state ----------
+
+#if defined(LOCK_BACKEND_PYMUTEX)
+
+typedef struct {
+    PyObject_HEAD
+    PyMutex mutex;
+} LockObject;
+
+#elif defined(LOCK_BACKEND_SRWLOCK)
+
+typedef struct {
+    PyObject_HEAD
+    // The SRWLOCK below does NOT represent the Python lock; like the pthread
+    // fallback's `mut`, it only guards `locked` and the condition variable,
+    // and is held just long enough to inspect/flip the flag -- never across
+    // the user's critical section. That is what allows release() from a
+    // thread other than the acquirer: SRWLOCK's same-thread-release rule is
+    // never violated, and clearing `locked` is just a guarded store. This
+    // mirrors CPython's Windows lock (NRMUTEX in Python/thread_nt.h).
+    SRWLOCK srw;
+    CONDITION_VARIABLE lock_released;
+    int locked;  // 0=unlocked, 1=locked; protected by `srw`
+} LockObject;
+
+#elif defined(LOCK_BACKEND_SEM)
+
+typedef struct {
+    PyObject_HEAD
+    sem_t sem;          // counting semaphore, initialized to 1
+    // Tracks the locked state for locked() and release-unlocked detection.
+    // The semaphore itself is the source of truth for mutual exclusion; this
+    // flag is advisory bookkeeping. It is set after a successful acquire and
+    // cleared before sem_post in release.
+    //
+    // This backend is only selected when the GIL is enabled, so this flag is a
+    // plain int relying on the GIL for serialization: it is only ever touched
+    // with the GIL held (the blocking sem_wait drops the GIL, but the flag
+    // store happens after the GIL is reacquired). This mirrors the old
+    // PyThread_type_lock wrapper bookkeeping used by CPython 3.12 and earlier,
+    // where _thread lock kept a plain `char locked` for sanity checks and
+    // locked().
+    int locked;
+} LockObject;
+
+#else  // pthread mutex + condvar fallback
+
+typedef struct {
+    PyObject_HEAD
+    // The pthread mutex below does NOT represent the Python lock; it only
+    // guards the `locked` flag and the condition variable. The Python lock
+    // state is `locked` itself. This indirection (matching CPython's POSIX
+    // lock) is what allows release() from a thread other than the acquirer:
+    // `mut` is always locked and unlocked within a single call on a single
+    // thread, so pthread's same-thread-unlock rule is never violated.
+    pthread_mutex_t mut;
+    pthread_cond_t lock_released;
+    // Always accessed while holding `mut` (including the locked() reader),
+    // so a plain int is sufficient -- the mutex provides the ordering.
+    // Matches CPython's pthread_lock.locked (a plain char).
+    int locked;  // 0=unlocked, 1=locked; protected by `mut`
+} LockObject;
+
+#endif
+
+// ---------- Platform-specific init/acquire/release ----------
+
+static inline int
+Lock_init_internal(LockObject *self)
+{
+#if defined(LOCK_BACKEND_PYMUTEX)
+    self->mutex = (PyMutex){0};
+#elif defined(LOCK_BACKEND_SRWLOCK)
+    InitializeSRWLock(&self->srw);
+    InitializeConditionVariable(&self->lock_released);
+    self->locked = 0;
+#elif defined(LOCK_BACKEND_SEM)
+    if (sem_init(&self->sem, 0, 1) != 0) {
+        PyErr_SetFromErrno(PyExc_OSError);
+        return -1;
+    }
+    self->locked = 0;
+#else
+    int status = pthread_mutex_init(&self->mut, NULL);
+    if (status != 0) {
+        errno = status;
+        PyErr_SetFromErrno(PyExc_OSError);
+        return -1;
+    }
+
+    status = pthread_cond_init(&self->lock_released, NULL);
+    if (status != 0) {
+        pthread_mutex_destroy(&self->mut);
+        errno = status;
+        PyErr_SetFromErrno(PyExc_OSError);
+        return -1;
+    }
+
+    self->locked = 0;
+#endif
+    return 0;
+}
+
+// Try to acquire the lock. Returns 1 (true) on success, 0 (false) if
+// non-blocking and the lock is held, or -1 if interrupted by an error-raising
+// signal handler.
+static int
+Lock_acquire_impl(LockObject *self, int blocking)
+{
+#if defined(LOCK_BACKEND_PYMUTEX)
+    if (!blocking) {
+        PyLockStatus r = _PyMutex_LockTimed(&self->mutex, 0, 
_Py_LOCK_DONT_DETACH);
+        return r == PY_LOCK_ACQUIRED;
+    }
+    if (PyMutex_LockFast(&self->mutex)) {
+        return 1;
+    }
+    PyLockStatus r = _PyMutex_LockTimed(&self->mutex, -1,
+                                        _PY_LOCK_DETACH | 
_PY_LOCK_HANDLE_SIGNALS);
+    if (r == PY_LOCK_INTR) {
+        return -1;
+    }
+    return 1;
+
+#elif defined(LOCK_BACKEND_SRWLOCK)
+    // `srw` only guards `locked` and the condition variable; it is held just
+    // long enough to inspect/flip the flag, never across the user's critical
+    // section. This is what lets a different thread call release().
+    //
+    // Fast path: grab the lock without releasing the GIL if it is free. This 
is
+    // also the whole story in the non-blocking case.
+    AcquireSRWLockExclusive(&self->srw);
+    if (!self->locked) {
+        self->locked = 1;
+        ReleaseSRWLockExclusive(&self->srw);
+        return 1;
+    }
+    ReleaseSRWLockExclusive(&self->srw);
+    if (!blocking) {
+        return 0;
+    }
+
+    // Slow path: wait for the lock to be released, with the GIL dropped so
+    // other Python threads can run (and release the lock).
+    // SleepConditionVariableSRW atomically releases the SRWLOCK while sleeping
+    // and reacquires it on wake, exactly like pthread_cond_wait.
+    Py_BEGIN_ALLOW_THREADS
+    AcquireSRWLockExclusive(&self->srw);
+    while (self->locked) {
+        SleepConditionVariableSRW(&self->lock_released, &self->srw, INFINITE, 
0);
+    }
+    self->locked = 1;
+    ReleaseSRWLockExclusive(&self->srw);
+    Py_END_ALLOW_THREADS
+    return 1;
+
+#elif defined(LOCK_BACKEND_SEM)
+    // A semaphore has no ownership: any thread may sem_post a token that
+    // another thread consumed via sem_wait, so cross-thread release is
+    // directly well-defined. `locked` is advisory bookkeeping for locked()
+    // and for guarding against releasing an unheld lock.
+    //
+    // Fast path: try a non-blocking acquire first to avoid GIL 
release/reacquire
+    // overhead in the common uncontended case. This is also the whole story in
+    // the non-blocking case.
+    {
+        int status;
+        do {
+            status = sem_trywait(&self->sem);
+        } while (status == -1 && errno == EINTR);
+        if (status == 0) {
+            self->locked = 1;
+            return 1;
+        }
+    }
+    if (!blocking) {
+        return 0;  // EAGAIN: already held
+    }
+
+    // Slow path: block with the GIL dropped so other Python threads can run
+    // (and release the lock). If a signal interrupts sem_wait(), run pending
+    // Python signal handlers with the GIL held; retry unless a handler raises.
+    for (;;) {
+        int status;
+        int err = 0;
+
+        Py_BEGIN_ALLOW_THREADS
+        status = sem_wait(&self->sem);
+        if (status == -1) {
+            err = errno;
+        }
+        Py_END_ALLOW_THREADS
+
+        if (status == 0) {
+            self->locked = 1;
+            return 1;
+        }
+
+        if (err != EINTR) {
+            PyErr_SetFromErrno(PyExc_OSError);
+            return -1;
+        }
+
+        if (Py_MakePendingCalls() < 0) {
+            return -1;
+        }
+    }
+
+#else  // pthread mutex + condvar fallback
+    // `mut` only guards `locked` and the condition variable; it is held just
+    // long enough to inspect/flip the flag, never across the user's critical
+    // section. This is what lets a different thread call release().
+    //
+    // Fast path: grab the lock without releasing the GIL if it is free. This 
is
+    // also the whole story in the non-blocking case.
+    pthread_mutex_lock(&self->mut);
+    if (!self->locked) {
+        self->locked = 1;
+        pthread_mutex_unlock(&self->mut);
+        return 1;
+    }
+    pthread_mutex_unlock(&self->mut);
+    if (!blocking) {
+        return 0;
+    }
+
+    // Slow path: wait for the lock to be released, with the GIL dropped so
+    // other Python threads can run (and release the lock). If we wake but do
+    // not get the lock, give pending Python signal handlers a chance to run,
+    // matching CPython's pthread fallback.
+    for (;;) {
+        int acquired = 0;
+        int interrupted = 0;
+        int status;
+
+        Py_BEGIN_ALLOW_THREADS
+        status = pthread_mutex_lock(&self->mut);
+        if (status == 0) {
+            while (self->locked) {
+                status = pthread_cond_wait(&self->lock_released, &self->mut);
+                if (status != 0) {
+                    break;
+                }
+                if (self->locked) {
+                    interrupted = 1;
+                    break;
+                }
+            }
+            if (status == 0 && !interrupted) {
+                self->locked = 1;
+                acquired = 1;
+            }
+            int unlock_status = pthread_mutex_unlock(&self->mut);
+            if (status == 0) {
+                status = unlock_status;
+            }
+        }
+        Py_END_ALLOW_THREADS
+
+        if (status != 0) {
+            errno = status;
+            PyErr_SetFromErrno(PyExc_OSError);
+            return -1;
+        }
+        if (acquired) {
+            return 1;
+        }
+        if (interrupted && Py_MakePendingCalls() < 0) {
+            return -1;
+        }
+    }
+#endif
+}
+
+// Release the lock. Returns 0 on success, -1 if the lock was not held.
+static int
+Lock_release_impl(LockObject *self)
+{
+#if defined(LOCK_BACKEND_PYMUTEX)
+    // threading.Lock is unowned, so release() may be called from a different
+    // thread than acquire(). CPython's atomic _PyMutex_TryUnlock() is not part
+    // of the public API and is not exported by all CPython builds. This fast
+    // partial-replacement path deliberately avoids a second guard mutex:
+    // ordinary release() of an unlocked lock raises RuntimeError, but racy
+    // erroneous release() calls are undefined behavior and may make
+    // PyMutex_Unlock() abort.
+    if (!PyMutex_IsLocked(&self->mutex)) {
+        return -1;
+    }
+    PyMutex_Unlock(&self->mutex);
+    return 0;
+
+#elif defined(LOCK_BACKEND_SRWLOCK)
+    AcquireSRWLockExclusive(&self->srw);
+    if (!self->locked) {
+        ReleaseSRWLockExclusive(&self->srw);
+        return -1;
+    }
+    self->locked = 0;
+    // Wake one waiter (if any). Signalling under `srw` is fine and avoids a
+    // lost-wakeup race.
+    WakeConditionVariable(&self->lock_released);
+    ReleaseSRWLockExclusive(&self->srw);
+    return 0;
+
+#elif defined(LOCK_BACKEND_SEM)
+    // Check-then-clear the flag, then post. This backend is only selected when
+    // the GIL is enabled, so the check and clear are serialized without an
+    // atomic.
+    if (!self->locked) {
+        return -1;
+    }
+    self->locked = 0;
+    sem_post(&self->sem);
+    return 0;
+
+#else  // pthread mutex + condvar fallback
+    pthread_mutex_lock(&self->mut);
+    if (!self->locked) {
+        pthread_mutex_unlock(&self->mut);
+        return -1;
+    }
+    self->locked = 0;
+    // Wake one waiter (if any). Signalling under `mut` is fine and avoids a
+    // lost-wakeup race.
+    pthread_cond_signal(&self->lock_released);
+    pthread_mutex_unlock(&self->mut);
+    return 0;
+#endif
+}
+
+static inline int
+Lock_is_locked(LockObject *self)
+{
+#if defined(LOCK_BACKEND_PYMUTEX)
+    return PyMutex_IsLocked(&self->mutex);
+#elif defined(LOCK_BACKEND_SRWLOCK)
+    // locked() is not expected to be on a perf-critical path, so take the
+    // SRWLOCK (shared) for a clean read of the guarded flag rather than
+    // relying on an atomic/volatile field.
+    AcquireSRWLockShared(&self->srw);
+    int result = self->locked;
+    ReleaseSRWLockShared(&self->srw);
+    return result;
+#elif defined(LOCK_BACKEND_SEM)
+    // The flag is GIL-serialized (see the struct comment); locked() is a
+    // plain read, matching the old CPython _thread lock bookkeeping described
+    // above.
+    return self->locked != 0;
+#else  // pthread mutex + condvar fallback
+    // `locked` is only ever accessed under `mut`; take it for a clean read.
+    // locked() is not expected to be on a perf-critical path.
+    pthread_mutex_lock(&self->mut);
+    int result = self->locked;
+    pthread_mutex_unlock(&self->mut);
+    return result;
+#endif
+}
+
+// ---------- Python type methods (shared across platforms) ----------
+
+static PyTypeObject LockType;
+
+static PyObject *
+Lock_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+    if (type != &LockType) {
+        PyErr_SetString(PyExc_TypeError, "Lock cannot be subclassed");
+        return NULL;
+    }
+
+    LockObject *self = (LockObject *)type->tp_alloc(type, 0);
+    if (self != NULL && Lock_init_internal(self) < 0) {
+        type->tp_free((PyObject *)self);
+        return NULL;
+    }
+    return (PyObject *)self;
+}
+
+static int
+Lock_init(LockObject *self, PyObject *args, PyObject *kwds)
+{
+    if (!PyArg_ParseTuple(args, "")) {
+        return -1;
+    }
+
+    if (kwds != NULL && PyDict_Size(kwds) > 0) {
+        PyErr_SetString(PyExc_TypeError,
+                        "Lock() takes no keyword arguments");
+        return -1;
+    }
+
+    return 0;
+}
+
+static void
+Lock_dealloc(LockObject *self)
+{
+#if defined(LOCK_BACKEND_SEM)
+    sem_destroy(&self->sem);
+#elif defined(LOCK_BACKEND_PTHREAD)
+    // `mut` is only ever held transiently within a single call, so it is
+    // always unlocked here even if the Python lock is still "locked".
+    // Some pthread implementations require the cond to be destroyed first.
+    pthread_cond_destroy(&self->lock_released);
+    pthread_mutex_destroy(&self->mut);
+#endif
+    Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static PyObject *
+Lock_acquire(LockObject *self, PyObject *const *args, Py_ssize_t nargs,
+             PyObject *kwnames)
+{
+    int blocking = 1;
+
+    Py_ssize_t nkw = kwnames ? PyTuple_GET_SIZE(kwnames) : 0;
+    if (nargs + nkw > 1) {
+        PyErr_SetString(PyExc_TypeError, "acquire() takes at most 1 argument");
+        return NULL;
+    }
+
+    if (nargs == 1) {
+        blocking = PyObject_IsTrue(args[0]);
+        if (blocking < 0)
+            return NULL;
+    } else if (nkw == 1) {
+        PyObject *key = PyTuple_GET_ITEM(kwnames, 0);
+        if (PyUnicode_CompareWithASCIIString(key, "blocking") != 0) {
+            PyErr_Format(PyExc_TypeError,
+                         "acquire() got an unexpected keyword argument '%U'",
+                         key);
+            return NULL;
+        }
+        blocking = PyObject_IsTrue(args[0]);
+        if (blocking < 0)
+            return NULL;
+    }
+
+    int result = Lock_acquire_impl(self, blocking);
+    if (result < 0) {
+        return NULL;
+    }
+    return PyBool_FromLong(result);
+}
+
+static PyObject *
+Lock_release(LockObject *self, PyObject *Py_UNUSED(ignored))
+{
+    if (Lock_release_impl(self) < 0) {
+        PyErr_SetString(PyExc_RuntimeError, "cannot release an unlocked lock");
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+static PyObject *
+Lock_locked(LockObject *self, PyObject *Py_UNUSED(ignored))
+{
+    return PyBool_FromLong(Lock_is_locked(self));
+}
+
+static PyObject *
+Lock_enter(LockObject *self, PyObject *Py_UNUSED(ignored))
+{
+    int result = Lock_acquire_impl(self, 1);
+    if (result < 0)
+        return NULL;
+    return PyBool_FromLong(result);
+}
+
+static PyObject *
+Lock_exit(LockObject *self, PyObject *const *args, Py_ssize_t nargs)
+{
+    return Lock_release(self, NULL);
+}
+
+static PyMethodDef Lock_methods[] = {
+    {"acquire", (PyCFunction)(void(*)(void))Lock_acquire, METH_FASTCALL | 
METH_KEYWORDS,
+     PyDoc_STR("Acquire the lock, blocking or non-blocking.\n"
+               "Returns True if the lock was acquired, False otherwise.")},
+    {"release", (PyCFunction)Lock_release, METH_NOARGS,
+     PyDoc_STR("Release the lock.")},
+    {"locked", (PyCFunction)Lock_locked, METH_NOARGS,
+     PyDoc_STR("Return True if the lock is currently held.")},
+    {"__enter__", (PyCFunction)Lock_enter, METH_NOARGS,
+     PyDoc_STR("Acquire the lock.")},
+    {"__exit__", (PyCFunction)Lock_exit, METH_FASTCALL,
+     PyDoc_STR("Release the lock.")},
+    {NULL}
+};
+
+static PyTypeObject LockType = {
+    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
+    .tp_name = "Lock",
+    .tp_doc = PyDoc_STR("A fast mutual exclusion lock"),
+    .tp_basicsize = sizeof(LockObject),
+    .tp_itemsize = 0,
+    .tp_flags = Py_TPFLAGS_DEFAULT,
+    .tp_new = Lock_new,
+    .tp_init = (initproc)Lock_init,
+    .tp_dealloc = (destructor)Lock_dealloc,
+    .tp_methods = Lock_methods,
+};
+
+static PyTypeObject *
+Lock_type_internal(void) {
+    return &LockType;
+}
+
+// Create a new Lock object (for use from compiled code)
+static PyObject *
+Lock_new_internal(void) {
+    LockObject *self = (LockObject *)LockType.tp_alloc(&LockType, 0);
+    if (self != NULL && Lock_init_internal(self) < 0) {
+        LockType.tp_free((PyObject *)self);
+        return NULL;
+    }
+    return (PyObject *)self;
+}
+
+// Acquire the lock (blocking), for use from compiled code.
+// Returns true on success, sets error and returns 2 (ERR_MAGIC) on failure.
+static char
+Lock_acquire_internal(PyObject *self) {
+    int result = Lock_acquire_impl((LockObject *)self, 1);
+    if (result < 0) {
+        return 2;
+    }
+    return (char)result;
+}
+
+// Acquire the lock with explicit blocking arg, for use from compiled code.
+// Returns true if acquired, false otherwise. Sets error and returns 2
+// (ERR_MAGIC) on failure.
+static char
+Lock_acquire_blocking_internal(PyObject *self, char blocking) {
+    int result = Lock_acquire_impl((LockObject *)self, blocking);
+    if (result < 0) {
+        return 2;
+    }
+    return (char)result;
+}
+
+// Release the lock, for use from compiled code.
+// Returns 0 (None) on success, sets error and returns 2 (ERR_MAGIC) on 
failure.
+static char
+Lock_release_internal(PyObject *self) {
+    if (Lock_release_impl((LockObject *)self) < 0) {
+        PyErr_SetString(PyExc_RuntimeError, "cannot release an unlocked lock");
+        return 2;
+    }
+    return 0;
+}
+
+// Check if the lock is held, for use from compiled code.
+static char
+Lock_locked_internal(PyObject *self) {
+    return (char)Lock_is_locked((LockObject *)self);
+}
+
+static PyMethodDef librt_threading_module_methods[] = {
+    {NULL, NULL, 0, NULL}
+};
+
+static int
+threading_abi_version(void) {
+    return LIBRT_THREADING_ABI_VERSION;
+}
+
+static int
+threading_api_version(void) {
+    return LIBRT_THREADING_API_VERSION;
+}
+
+static int
+librt_threading_module_exec(PyObject *m)
+{
+    if (PyType_Ready(&LockType) < 0) {
+        return -1;
+    }
+    if (PyModule_AddObjectRef(m, "Lock", (PyObject *)&LockType) < 0) {
+        return -1;
+    }
+
+    // Export mypyc internal C API via capsule
+    static void *threading_api[LIBRT_THREADING_API_LEN] = {
+        (void *)threading_abi_version,
+        (void *)threading_api_version,
+        (void *)Lock_type_internal,
+        (void *)Lock_new_internal,
+        (void *)Lock_acquire_internal,
+        (void *)Lock_release_internal,
+        (void *)Lock_locked_internal,
+        (void *)Lock_acquire_blocking_internal,
+    };
+    PyObject *c_api_object = PyCapsule_New((void *)threading_api, 
"librt.threading._C_API", NULL);
+    if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
+        return -1;
+    }
+    return 0;
+}
+
+static PyModuleDef_Slot librt_threading_module_slots[] = {
+    {Py_mod_exec, librt_threading_module_exec},
+#ifdef Py_MOD_GIL_NOT_USED
+    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
+#endif
+    {0, NULL}
+};
+
+static PyModuleDef librt_threading_module = {
+    .m_base = PyModuleDef_HEAD_INIT,
+    .m_name = "threading",
+    .m_doc = "Fast threading primitives optimized for mypyc",
+    .m_size = 0,
+    .m_methods = librt_threading_module_methods,
+    .m_slots = librt_threading_module_slots,
+};
+
+PyMODINIT_FUNC
+PyInit_threading(void)
+{
+    return PyModuleDef_Init(&librt_threading_module);
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/threading/librt_threading.h 
new/librt-0.15.0/threading/librt_threading.h
--- old/librt-0.12.0/threading/librt_threading.h        1970-01-01 
01:00:00.000000000 +0100
+++ new/librt-0.15.0/threading/librt_threading.h        2026-08-07 
12:12:17.000000000 +0200
@@ -0,0 +1,10 @@
+#ifndef LIBRT_THREADING_H
+#define LIBRT_THREADING_H
+
+#include <Python.h>
+
+#define LIBRT_THREADING_ABI_VERSION 1
+#define LIBRT_THREADING_API_VERSION 1
+#define LIBRT_THREADING_API_LEN 8
+
+#endif  // LIBRT_THREADING_H
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/threading/librt_threading_api.c 
new/librt-0.15.0/threading/librt_threading_api.c
--- old/librt-0.12.0/threading/librt_threading_api.c    1970-01-01 
01:00:00.000000000 +0100
+++ new/librt-0.15.0/threading/librt_threading_api.c    2026-08-07 
12:12:17.000000000 +0200
@@ -0,0 +1,43 @@
+#include "librt_threading_api.h"
+
+void *LibRTThreading_API[LIBRT_THREADING_API_LEN] = {0};
+
+int
+import_librt_threading(void)
+{
+    PyObject *mod = PyImport_ImportModule("librt.threading");
+    if (mod == NULL)
+        return -1;
+    Py_DECREF(mod);  // we import just for the side effect of making the below 
work.
+    void **capsule = (void **)PyCapsule_Import("librt.threading._C_API", 0);
+    if (capsule == NULL)
+        return -1;
+
+    // Only after version validation succeeds can we safely copy the full 
table.
+    int (*abi_version)(void) = (int (*)(void))capsule[0];
+    int (*api_version)(void) = (int (*)(void))capsule[1];
+    if (abi_version() != LIBRT_THREADING_ABI_VERSION) {
+        char err[128];
+        snprintf(err, sizeof(err), "ABI version conflict for librt.threading, 
expected %d, found %d",
+            LIBRT_THREADING_ABI_VERSION,
+            abi_version()
+        );
+        PyErr_SetString(PyExc_ValueError, err);
+        return -1;
+    }
+    if (api_version() < LIBRT_THREADING_API_VERSION) {
+        char err[128];
+        snprintf(err, sizeof(err),
+                 "API version conflict for librt.threading, expected %d or 
newer, found %d (hint: upgrade librt)",
+            LIBRT_THREADING_API_VERSION,
+            api_version()
+        );
+        PyErr_SetString(PyExc_ValueError, err);
+        return -1;
+    }
+    // Provider API version is >= our expected version, which (by the API
+    // compatibility contract) means it has at least LIBRT_THREADING_API_LEN
+    // entries, so this copy is safe.
+    memcpy(LibRTThreading_API, capsule, sizeof(LibRTThreading_API));
+    return 0;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/threading/librt_threading_api.h 
new/librt-0.15.0/threading/librt_threading_api.h
--- old/librt-0.12.0/threading/librt_threading_api.h    1970-01-01 
01:00:00.000000000 +0100
+++ new/librt-0.15.0/threading/librt_threading_api.h    2026-08-07 
12:12:17.000000000 +0200
@@ -0,0 +1,20 @@
+#ifndef LIBRT_THREADING_API_H
+#define LIBRT_THREADING_API_H
+
+#include "librt_threading.h"
+
+int
+import_librt_threading(void);
+
+extern void *LibRTThreading_API[LIBRT_THREADING_API_LEN];
+
+#define LibRTThreading_ABIVersion (*(int (*)(void)) LibRTThreading_API[0])
+#define LibRTThreading_APIVersion (*(int (*)(void)) LibRTThreading_API[1])
+#define LibRTThreading_Lock_type_internal (*(PyTypeObject* (*)(void)) 
LibRTThreading_API[2])
+#define LibRTThreading_Lock_new_internal (*(PyObject* (*)(void)) 
LibRTThreading_API[3])
+#define LibRTThreading_Lock_acquire_internal (*(char (*)(PyObject *self)) 
LibRTThreading_API[4])
+#define LibRTThreading_Lock_release_internal (*(char (*)(PyObject *self)) 
LibRTThreading_API[5])
+#define LibRTThreading_Lock_locked_internal (*(char (*)(PyObject *self)) 
LibRTThreading_API[6])
+#define LibRTThreading_Lock_acquire_blocking_internal (*(char (*)(PyObject 
*self, char blocking)) LibRTThreading_API[7])
+
+#endif  // LIBRT_THREADING_API_H
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/vecs/vec_t.c 
new/librt-0.15.0/vecs/vec_t.c
--- old/librt-0.12.0/vecs/vec_t.c       2026-06-30 17:48:02.000000000 +0200
+++ new/librt-0.15.0/vecs/vec_t.c       2026-08-07 12:12:17.000000000 +0200
@@ -756,6 +756,36 @@
     VecT v = vec_alloc(alloc_size, item_type);
     if (VEC_IS_ERROR(v))
         return vec_error();
+#ifdef Py_GIL_DISABLED
+    if (is_list) {
+        // Other threads could be mutating the list concurrently, so use 
strong references
+        Py_ssize_t actual = 0;
+        for (Py_ssize_t i = 0; i < PyList_GET_SIZE(seq) && i < n; i++) {
+            PyObject *item = PyList_GetItemRef(seq, i);
+            if (unlikely(item == NULL)) {
+                // Race condition: list shrank between size read and get item
+                for (Py_ssize_t j = actual; j < alloc_size; j++)
+                    v.items[j] = NULL;
+                VEC_DECREF(v);
+                return vec_error();
+            }
+            if (!VecT_ItemCheck(v, item, item_type)) {
+                Py_DECREF(item);
+                for (Py_ssize_t j = actual; j < alloc_size; j++)
+                    v.items[j] = NULL;
+                VEC_DECREF(v);
+                return vec_error();
+            }
+            v.items[actual++] = item;
+        }
+        for (Py_ssize_t j = actual; j < alloc_size; j++)
+            v.items[j] = NULL;
+        vec_track_buffer(&v);
+        v.len = actual;
+        return v;
+    }
+    // Tuples are immutable, so fall through to the shared loop below
+#endif
     for (Py_ssize_t i = 0; i < n; i++) {
         PyObject *item = is_list ? PyList_GET_ITEM(seq, i) : 
PyTuple_GET_ITEM(seq, i);
         if (!VecT_ItemCheck(v, item, item_type)) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.12.0/vecs/vec_template.c 
new/librt-0.15.0/vecs/vec_template.c
--- old/librt-0.12.0/vecs/vec_template.c        2026-06-30 17:48:02.000000000 
+0200
+++ new/librt-0.15.0/vecs/vec_template.c        2026-08-07 12:12:17.000000000 
+0200
@@ -148,6 +148,30 @@
     VEC v = vec_alloc(alloc_size);
     if (VEC_IS_ERROR(v))
         return vec_error();
+#ifdef Py_GIL_DISABLED
+    if (is_list) {
+        // Other threads could be mutating the list concurrently, so use 
strong references
+        Py_ssize_t actual = 0;
+        for (Py_ssize_t i = 0; i < PyList_GET_SIZE(seq) && i < n; i++) {
+            PyObject *item = PyList_GetItemRef(seq, i);
+            if (unlikely(item == NULL)) {
+                // Race condition: list shrank between size read and get item
+                VEC_DECREF(v);
+                return vec_error();
+            }
+            ITEM_C_TYPE x = UNBOX_ITEM(item);
+            Py_DECREF(item);
+            if (IS_UNBOX_ERROR(x)) {
+                VEC_DECREF(v);
+                return vec_error();
+            }
+            v.items[actual++] = x;
+        }
+        v.len = actual;
+        return v;
+    }
+    // Tuples are immutable, so fall through to the shared loop below
+#endif
     for (Py_ssize_t i = 0; i < n; i++) {
         PyObject *item = is_list ? PyList_GET_ITEM(seq, i) : 
PyTuple_GET_ITEM(seq, i);
         ITEM_C_TYPE x = UNBOX_ITEM(item);

Reply via email to