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-07-02 20:08:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-librt (Old)
 and      /work/SRC/openSUSE:Factory/.python-librt.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-librt"

Thu Jul  2 20:08:45 2026 rev:6 rq:1363035 version:0.12.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-librt/python-librt.changes        
2026-05-20 16:47:54.852667826 +0200
+++ /work/SRC/openSUSE:Factory/.python-librt.new.1982/python-librt.changes      
2026-07-02 20:11:20.073300322 +0200
@@ -1,0 +2,6 @@
+Wed Jul  1 16:25:21 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.12.0:
+  * Sync mypy and bump version
+
+-------------------------------------------------------------------

Old:
----
  librt-0.11.0.tar.gz

New:
----
  librt-0.12.0.tar.gz

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

Other differences:
------------------
++++++ python-librt.spec ++++++
--- /var/tmp/diff_new_pack.Rilrfm/_old  2026-07-02 20:11:20.729323021 +0200
+++ /var/tmp/diff_new_pack.Rilrfm/_new  2026-07-02 20:11:20.733323160 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-librt
-Version:        0.11.0
+Version:        0.12.0
 Release:        0
 Summary:        Mypyc runtime library
 License:        MIT

++++++ librt-0.11.0.tar.gz -> librt-0.12.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/PKG-INFO new/librt-0.12.0/PKG-INFO
--- old/librt-0.11.0/PKG-INFO   2026-05-10 19:48:12.862697600 +0200
+++ new/librt-0.12.0/PKG-INFO   2026-06-30 17:48:05.565397300 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: librt
-Version: 0.11.0
+Version: 0.12.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.11.0/README.md new/librt-0.12.0/README.md
--- old/librt-0.11.0/README.md  2026-05-10 19:48:00.000000000 +0200
+++ new/librt-0.12.0/README.md  2026-06-30 17:47:54.000000000 +0200
@@ -6,17 +6,26 @@
 [![Checked with 
mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
 
 This library contains basic functionality that is useful in code compiled
-using mypyc, and efficient C implementations of various Python standard library
-classes and functions. Mypyc can produce faster extensions when you use 
`librt` in
-the code you compile. `librt` also contains some internal library features 
used by mypy.
+using the [mypyc](https://mypyc.readthedocs.io/en/latest/) compiler, and 
efficient
+C alternatives to various Python standard library classes and functions. Mypyc 
can
+produce faster extensions when you use `librt` in the code you compile. 
`librt` also
+contains some internal library features used by mypy.
+
+Here are some feature highlights:
+
+ * [librt.vecs](https://mypyc.readthedocs.io/en/latest/librt_vecs.html) 
defines the growable packed array-like type `vec`
+ * [librt.strings](https://mypyc.readthedocs.io/en/latest/librt_strings.html) 
defines fast string and bytes builders and binary data read/write helpers
+ * [librt.base64](https://mypyc.readthedocs.io/en/latest/librt_base64.html) 
provides fast SIMD Base64 encoding/decoding
+
+For more details, refer to [librt 
documentation](https://mypyc.readthedocs.io/en/latest/librt.html).
+
+Report any issues in the [mypyc issue 
tracker](https://github.com/mypyc/mypyc/issues).
 
 This repository is only used to build and publish the mypyc runtime library. 
Development
 happens in the [mypy repository](https://github.com/python/mypy). Code is then 
perodically
 synced from the `mypyc/lib-rt`
 [subdirectory in the mypy 
repository](https://github.com/python/mypy/tree/master/mypyc/lib-rt).
 
-Report any issues in the [mypyc issue 
tracker](https://github.com/mypyc/mypyc/issues).
-
 ## Developer notes
 
 Since this repo should be kept in sync with `mypy`, it has an unusual 
directory structure.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/dict_ops.c new/librt-0.12.0/dict_ops.c
--- old/librt-0.11.0/dict_ops.c 2026-05-10 19:48:09.000000000 +0200
+++ new/librt-0.12.0/dict_ops.c 2026-06-30 17:48:02.000000000 +0200
@@ -15,13 +15,10 @@
 // some indirections.
 PyObject *CPyDict_GetItem(PyObject *dict, PyObject *key) {
     if (PyDict_CheckExact(dict)) {
-        PyObject *res = PyDict_GetItemWithError(dict, key);
-        if (!res) {
-            if (!PyErr_Occurred()) {
-                PyErr_SetObject(PyExc_KeyError, key);
-            }
-        } else {
-            Py_INCREF(res);
+        PyObject *res;
+        int found = PyDict_GetItemRef(dict, key, &res);
+        if (found == 0) {
+            PyErr_SetObject(PyExc_KeyError, key);
         }
         return res;
     } else {
@@ -56,14 +53,15 @@
 PyObject *CPyDict_Get(PyObject *dict, PyObject *key, PyObject *fallback) {
     // We are dodgily assuming that get on a subclass doesn't have
     // different behavior.
-    PyObject *res = PyDict_GetItemWithError(dict, key);
-    if (!res) {
-        if (PyErr_Occurred()) {
-            return NULL;
-        }
-        res = fallback;
+    PyObject *res;
+    int found = PyDict_GetItemRef(dict, key, &res);
+    if (found < 0) {
+        return NULL;
+    }
+    if (found == 0) {
+        Py_INCREF(fallback);
+        return fallback;
     }
-    Py_INCREF(res);
     return res;
 }
 
@@ -100,17 +98,19 @@
         } else if (data_type == 3) {
             new_obj = PySet_New(NULL);
         } else {
-            return NULL;
+            new_obj = NULL;
         }
 
-        if (CPyDict_SetItem(dict, key, new_obj) == -1) {
-            return NULL;
+        if (new_obj == NULL) {
+            res = NULL;
+        } else if (CPyDict_SetItem(dict, key, new_obj) == -1) {
+            Py_DECREF(new_obj);
+            res = NULL;
         } else {
-            return new_obj;
+            res = new_obj;
         }
-    } else {
-        return res;
     }
+    return res;
 }
 
 int CPyDict_SetItem(PyObject *dict, PyObject *key, PyObject *value) {
@@ -346,16 +346,24 @@
     PyObject *dummy;
 
     if (PyDict_CheckExact(dict_or_iter)) {
-        ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &ret.f2, &dummy);
+        PyObject *key;
+        // PyDict_Next() returns a borrowed reference. On free-threaded builds,
+        // hold the dict lock until we have converted it to a strong reference.
+        Py_BEGIN_CRITICAL_SECTION(dict_or_iter);
+        ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &key, &dummy);
+        if (ret.f0) {
+            ret.f2 = Py_NewRef(key);
+        }
+        Py_END_CRITICAL_SECTION();
+
         if (ret.f0) {
             ret.f1 = CPyTagged_FromSsize_t(py_offset);
         } else {
             // Set key to None, so mypyc can manage refcounts.
             ret.f1 = 0;
             ret.f2 = Py_None;
+            Py_INCREF(ret.f2);
         }
-        // PyDict_Next() returns borrowed references.
-        Py_INCREF(ret.f2);
     } else {
         // offset is dummy in this case, just use the old value.
         ret.f1 = offset;
@@ -370,16 +378,24 @@
     PyObject *dummy;
 
     if (PyDict_CheckExact(dict_or_iter)) {
-        ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &dummy, &ret.f2);
+        PyObject *value;
+        // PyDict_Next() returns a borrowed reference. On free-threaded builds,
+        // hold the dict lock until we have converted it to a strong reference.
+        Py_BEGIN_CRITICAL_SECTION(dict_or_iter);
+        ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &dummy, &value);
+        if (ret.f0) {
+            ret.f2 = Py_NewRef(value);
+        }
+        Py_END_CRITICAL_SECTION();
+
         if (ret.f0) {
             ret.f1 = CPyTagged_FromSsize_t(py_offset);
         } else {
             // Set value to None, so mypyc can manage refcounts.
             ret.f1 = 0;
             ret.f2 = Py_None;
+            Py_INCREF(ret.f2);
         }
-        // PyDict_Next() returns borrowed references.
-        Py_INCREF(ret.f2);
     } else {
         // offset is dummy in this case, just use the old value.
         ret.f1 = offset;
@@ -393,7 +409,18 @@
     Py_ssize_t py_offset = CPyTagged_AsSsize_t(offset);
 
     if (PyDict_CheckExact(dict_or_iter)) {
-        ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &ret.f2, &ret.f3);
+        PyObject *key;
+        PyObject *value;
+        // PyDict_Next() returns borrowed references. On free-threaded builds,
+        // hold the dict lock until we have converted them to strong 
references.
+        Py_BEGIN_CRITICAL_SECTION(dict_or_iter);
+        ret.f0 = PyDict_Next(dict_or_iter, &py_offset, &key, &value);
+        if (ret.f0) {
+            ret.f2 = Py_NewRef(key);
+            ret.f3 = Py_NewRef(value);
+        }
+        Py_END_CRITICAL_SECTION();
+
         if (ret.f0) {
             ret.f1 = CPyTagged_FromSsize_t(py_offset);
         } else {
@@ -401,6 +428,8 @@
             ret.f1 = 0;
             ret.f2 = Py_None;
             ret.f3 = Py_None;
+            Py_INCREF(ret.f2);
+            Py_INCREF(ret.f3);
         }
     } else {
         ret.f1 = offset;
@@ -413,6 +442,8 @@
             ret.f0 = 0;
             ret.f2 = Py_None;
             ret.f3 = Py_None;
+            Py_INCREF(ret.f2);
+            Py_INCREF(ret.f3);
         } else {
             ret.f0 = 1;
             ret.f2 = PyTuple_GET_ITEM(item, 0);
@@ -423,9 +454,6 @@
             return ret;
         }
     }
-    // PyDict_Next() returns borrowed references.
-    Py_INCREF(ret.f2);
-    Py_INCREF(ret.f3);
     return ret;
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/function_wrapper.c 
new/librt-0.12.0/function_wrapper.c
--- old/librt-0.11.0/function_wrapper.c 2026-05-10 19:48:09.000000000 +0200
+++ new/librt-0.12.0/function_wrapper.c 2026-06-30 17:48:02.000000000 +0200
@@ -29,7 +29,14 @@
 }
 
 static PyObject* CPyFunction_repr(CPyFunction *op) {
-    return PyUnicode_FromFormat("<function %U at %p>", op->func_name, (void 
*)op);
+    // Use helper to get name for free threading safety.
+    PyObject *name = CPyFunction_get_name((PyObject *)op, NULL);
+    if (unlikely(name == NULL)) {
+        return NULL;
+    }
+    PyObject *result = PyUnicode_FromFormat("<function %U at %p>", name, (void 
*)op);
+    Py_DECREF(name);
+    return result;
 }
 
 static PyObject* CPyFunction_call(PyObject *func, PyObject *args, PyObject 
*kw) {
@@ -59,13 +66,15 @@
 PyObject* CPyFunction_get_name(PyObject *op, void *context) {
     (void)context;
     CPyFunction *func = (CPyFunction *)op;
+    PyObject *result;
+    Py_BEGIN_CRITICAL_SECTION(op);
     if (unlikely(func->func_name == NULL)) {
         func->func_name = PyUnicode_InternFromString(((PyCFunctionObject 
*)func)->m_ml->ml_name);
-        if (unlikely(func->func_name == NULL))
-            return NULL;
     }
-    Py_INCREF(func->func_name);
-    return func->func_name;
+    result = func->func_name;
+    Py_XINCREF(result);
+    Py_END_CRITICAL_SECTION();
+    return result;
 }
 
 int CPyFunction_set_name(PyObject *op, PyObject *value, void *context) {
@@ -77,8 +86,13 @@
     }
 
     Py_INCREF(value);
-    Py_XDECREF(func->func_name);
+    // Decref outside critical section, since it could run arbitrary code.
+    PyObject *old;
+    Py_BEGIN_CRITICAL_SECTION(op);
+    old = func->func_name;
     func->func_name = value;
+    Py_END_CRITICAL_SECTION();
+    Py_XDECREF(old);
     return 0;
 }
 
@@ -182,6 +196,7 @@
 }
 
 
+// Steals ml, name, and code. Borrows module.
 static CPyFunction* CPyFunction_Init(CPyFunction *op, PyMethodDef *ml, 
PyObject* name,
                                      PyObject *module, PyObject* code, bool 
set_self) {
     PyCFunctionObject *cf = (PyCFunctionObject *)op;
@@ -192,12 +207,10 @@
     Py_XINCREF(module);
     cf->m_module = module;
 
-    Py_INCREF(name);
     op->func_name = name;
 
     ((PyCMethodObject *)op)->mm_class = NULL;
 
-    Py_XINCREF(code);
     op->func_code = code;
 
     CPyFunction_func_vectorcall(op) = CPyFunction_Vectorcall;
@@ -229,15 +242,34 @@
                           PyCFunction func, int func_flags, const char 
*func_doc,
                           int first_line, int code_flags, bool has_self_arg) {
     PyMethodDef *method = NULL;
-    PyObject *code = NULL, *op = NULL;
+    PyObject *code = NULL, *name = NULL, *op = NULL;
     bool set_self = false;
 
+#ifdef Py_GIL_DISABLED
+    // Double-checked locking: the common case (type already created) is a
+    // lock-free atomic load. Only the first-time initialization takes the
+    // mutex, which serializes concurrent creators.
+    if (!_Py_atomic_load_ptr_acquire(&CPyFunctionType)) {
+        static PyMutex type_init_mutex = {0};
+        PyMutex_Lock(&type_init_mutex);
+        if (!CPyFunctionType) {
+            PyTypeObject *type = (PyTypeObject 
*)PyType_FromSpec(&CPyFunction_spec);
+            if (unlikely(!type)) {
+                PyMutex_Unlock(&type_init_mutex);
+                goto err;
+            }
+            _Py_atomic_store_ptr_release(&CPyFunctionType, type);
+        }
+        PyMutex_Unlock(&type_init_mutex);
+    }
+#else
     if (!CPyFunctionType) {
         CPyFunctionType = (PyTypeObject *)PyType_FromSpec(&CPyFunction_spec);
         if (unlikely(!CPyFunctionType)) {
             goto err;
         }
     }
+#endif
 
     method = CPyMethodDef_New(funcname, func, func_flags, func_doc);
     if (unlikely(!method)) {
@@ -247,18 +279,24 @@
     if (unlikely(!code)) {
         goto err;
     }
+    name = PyUnicode_FromString(funcname);
+    if (unlikely(!name)) {
+        goto err;
+    }
 
     // Set m_self inside the function wrapper only if the wrapped function has 
no self arg
     // to pass m_self as the self arg when the function is called.
     // When the function has a self arg, it will come in the args vector 
passed to the
     // vectorcall handler.
     set_self = !has_self_arg;
-    op = (PyObject *)CPyFunction_Init(PyObject_GC_New(CPyFunction, 
CPyFunctionType),
-                                      method, PyUnicode_FromString(funcname), 
module,
-                                      code, set_self);
-    if (unlikely(!op)) {
+    CPyFunction *raw = PyObject_GC_New(CPyFunction, CPyFunctionType);
+    if (unlikely(!raw)) {
         goto err;
     }
+    op = (PyObject *)CPyFunction_Init(raw, method, name, module, code, 
set_self);
+    method = NULL;
+    name = NULL;
+    code = NULL;
     PyObject_GC_Track(op);
     return op;
 
@@ -267,5 +305,7 @@
     if (method) {
         PyMem_Free(method);
     }
+    Py_XDECREF(name);
+    Py_XDECREF(code);
     return NULL;
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/getargsfast.c 
new/librt-0.12.0/getargsfast.c
--- old/librt-0.11.0/getargsfast.c      2026-05-10 19:48:09.000000000 +0200
+++ new/librt-0.12.0/getargsfast.c      2026-06-30 17:48:02.000000000 +0200
@@ -18,7 +18,22 @@
 #include <Python.h>
 #include "CPy.h"
 
-#define PARSER_INITED(parser) ((parser)->kwtuple != NULL)
+// The kwtuple field doubles as the "parser has been initialized" flag: it is
+// written last (after all other parser fields) and read first. On 
free-threaded
+// builds the fast paths read it without holding any lock, so the write must 
be a
+// release store and the reads acquire loads. That way a thread that observes a
+// non-NULL kwtuple is guaranteed to also see the fully-initialized 
min/max/etc.
+// fields. On GIL builds these are plain accesses with no overhead.
+#ifdef Py_GIL_DISABLED
+#define PARSER_KWTUPLE(parser) _Py_atomic_load_ptr_acquire(&(parser)->kwtuple)
+#define SET_PARSER_KWTUPLE(parser, value) \
+    _Py_atomic_store_ptr_release(&(parser)->kwtuple, (value))
+#else
+#define PARSER_KWTUPLE(parser) ((parser)->kwtuple)
+#define SET_PARSER_KWTUPLE(parser, value) ((parser)->kwtuple = (value))
+#endif
+
+#define PARSER_INITED(parser) (PARSER_KWTUPLE(parser) != NULL)
 
 /* Forward */
 static int
@@ -115,19 +130,21 @@
 /* List of static parsers. */
 static struct CPyArg_Parser *static_arg_parsers = NULL;
 
+#ifdef Py_GIL_DISABLED
+// Serializes one-time initialization of parsers and insertion into the
+// static_arg_parsers list. Only contended the first time a given compiled
+// function is called; once a parser is initialized the fast paths never lock.
+static PyMutex static_arg_parsers_mutex;
+#endif
+
 static int
-parser_init(CPyArg_Parser *parser)
+parser_init_locked(CPyArg_Parser *parser)
 {
     const char * const *keywords;
     const char *format;
     int i, len, min, max, nkw;
     PyObject *kwtuple;
 
-    assert(parser->keywords != NULL);
-    if (PARSER_INITED(parser)) {
-        return 1;
-    }
-
     keywords = parser->keywords;
     /* scan keywords and count the number of positional-only parameters */
     for (i = 0; keywords[i] && !*keywords[i]; i++) {
@@ -244,14 +261,53 @@
         PyUnicode_InternInPlace(&str);
         PyTuple_SET_ITEM(kwtuple, i, str);
     }
-    parser->kwtuple = kwtuple;
 
     assert(parser->next == NULL);
     parser->next = static_arg_parsers;
     static_arg_parsers = parser;
+
+    // Publish the parser last: storing kwtuple marks it as initialized, so all
+    // other fields (and the list insertion above) must already be in place. On
+    // free-threaded builds this is a release store paired with the acquire 
loads
+    // in PARSER_INITED/PARSER_KWTUPLE.
+    SET_PARSER_KWTUPLE(parser, kwtuple);
     return 1;
 }
 
+// Cold path of parser_init: perform the one-time initialization. On
+// free-threaded builds this is serialized so that only one thread builds the
+// parser and inserts it into the static_arg_parsers list.
+static CPy_NOINLINE int
+parser_init_slow(CPyArg_Parser *parser)
+{
+#ifdef Py_GIL_DISABLED
+    PyMutex_Lock(&static_arg_parsers_mutex);
+    // Re-check now that we hold the lock: another thread may have initialized
+    // the parser while we were waiting.
+    if (PARSER_INITED(parser)) {
+        PyMutex_Unlock(&static_arg_parsers_mutex);
+        return 1;
+    }
+    int retval = parser_init_locked(parser);
+    PyMutex_Unlock(&static_arg_parsers_mutex);
+    return retval;
+#else
+    return parser_init_locked(parser);
+#endif
+}
+
+// Hot path: a parser is almost always already initialized, so keep the common
+// case inline and branch out to parser_init_slow only on first use.
+static inline int
+parser_init(CPyArg_Parser *parser)
+{
+    assert(parser->keywords != NULL);
+    if (likely(PARSER_INITED(parser))) {
+        return 1;
+    }
+    return parser_init_slow(parser);
+}
+
 static PyObject*
 find_keyword(PyObject *kwnames, PyObject *const *kwstack, PyObject *key)
 {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/librt/strings.pyi 
new/librt-0.12.0/librt/strings.pyi
--- old/librt-0.11.0/librt/strings.pyi  2026-05-10 19:48:00.000000000 +0200
+++ new/librt-0.12.0/librt/strings.pyi  2026-06-30 17:47:54.000000000 +0200
@@ -40,3 +40,22 @@
 def write_f64_be(b: BytesWriter, n: float, /) -> None: ...
 def read_f64_le(b: bytes, index: i64, /) -> float: ...
 def read_f64_be(b: bytes, index: i64, /) -> float: ...
+
+# Codepoint classification helpers operating on i32 codepoints (typically
+# obtained via ord(s[i])). Out-of-range inputs (negative, or past the maximum
+# Unicode code point 0x10FFFF) return False.
+def isspace(c: i32, /) -> bool: ...
+def isdigit(c: i32, /) -> bool: ...
+def isalnum(c: i32, /) -> bool: ...
+def isalpha(c: i32, /) -> bool: ...
+def isidentifier(c: i32, /) -> bool: ...
+
+# Codepoint case conversion. For the rare codepoints whose Unicode
+# uppercase / lowercase expands to multiple codepoints (e.g. U+00DF
+# uppercases to "SS", U+FB01 to "FI"), returns the input unchanged so
+# the signature stays i32 -> i32. Use str.upper() / str.lower() for full
+# Unicode case conversion when those cases matter. Out-of-range inputs
+# (negative, or past the maximum Unicode code point 0x10FFFF) are returned
+# unchanged.
+def toupper(c: i32, /) -> i32: ...
+def tolower(c: i32, /) -> i32: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/librt.egg-info/PKG-INFO 
new/librt-0.12.0/librt.egg-info/PKG-INFO
--- old/librt-0.11.0/librt.egg-info/PKG-INFO    2026-05-10 19:48:12.000000000 
+0200
+++ new/librt-0.12.0/librt.egg-info/PKG-INFO    2026-06-30 17:48:05.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: librt
-Version: 0.11.0
+Version: 0.12.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.11.0/list_ops.c new/librt-0.12.0/list_ops.c
--- old/librt-0.11.0/list_ops.c 2026-05-10 19:48:09.000000000 +0200
+++ new/librt-0.12.0/list_ops.c 2026-06-30 17:48:02.000000000 +0200
@@ -301,6 +301,8 @@
 }
 
 // Return -2 or error, -1 if not found, or index of first match otherwise.
+//
+// The caller must hold a critical section on the list.
 static Py_ssize_t _CPyList_Find(PyObject *list, PyObject *obj) {
     Py_ssize_t i;
     for (i = 0; i < Py_SIZE(list); i++) {
@@ -320,27 +322,35 @@
 }
 
 int CPyList_Remove(PyObject *list, PyObject *obj) {
+    int retval;
+    Py_BEGIN_CRITICAL_SECTION(list);
     Py_ssize_t index = _CPyList_Find(list, obj);
     if (index == -2) {
-        return -1;
-    }
-    if (index == -1) {
+        retval = -1;
+    } else if (index == -1) {
         PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
-        return -1;
+        retval = -1;
+    } else {
+        retval = PyList_SetSlice(list, index, index + 1, NULL);
     }
-    return PyList_SetSlice(list, index, index + 1, NULL);
+    Py_END_CRITICAL_SECTION();
+    return retval;
 }
 
 CPyTagged CPyList_Index(PyObject *list, PyObject *obj) {
+    CPyTagged retval;
+    Py_BEGIN_CRITICAL_SECTION(list);
     Py_ssize_t index = _CPyList_Find(list, obj);
     if (index == -2) {
-        return CPY_INT_TAG;
-    }
-    if (index == -1) {
+        retval = CPY_INT_TAG;
+    } else if (index == -1) {
         PyErr_SetString(PyExc_ValueError, "value is not in list");
-        return CPY_INT_TAG;
+        retval = CPY_INT_TAG;
+    } else {
+        retval = index << 1;
     }
-    return index << 1;
+    Py_END_CRITICAL_SECTION();
+    return retval;
 }
 
 PyObject *CPySequence_Sort(PyObject *seq) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/pyproject.toml 
new/librt-0.12.0/pyproject.toml
--- old/librt-0.11.0/pyproject.toml     2026-05-10 19:48:00.000000000 +0200
+++ new/librt-0.12.0/pyproject.toml     2026-06-30 17:47:54.000000000 +0200
@@ -19,7 +19,7 @@
     {name = "Jukka Lehtosalo", email = "[email protected]"},
     {name = "Ivan Levkivskyi", email = "[email protected]"},
 ]
-version = "0.11.0"
+version = "0.12.0"
 license = "MIT"
 classifiers = [
   "Development Status :: 3 - Alpha",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/strings/librt_strings.c 
new/librt-0.12.0/strings/librt_strings.c
--- old/librt-0.11.0/strings/librt_strings.c    2026-05-10 19:48:09.000000000 
+0200
+++ new/librt-0.12.0/strings/librt_strings.c    2026-06-30 17:48:02.000000000 
+0200
@@ -1153,6 +1153,56 @@
     return PyFloat_FromDouble(CPyBytes_ReadF64BEUnsafe(data + index));
 }
 
+// Python-level wrappers (`cp_*`) for interpreted callers. The C-side names
+// are prefixed `cp_` to avoid colliding with libc's <ctype.h> isspace etc.
+// The LibRTStrings_Is* helpers themselves are static inline in librt_strings.h
+// so they compile directly into mypyc-emitted code with no capsule
+// indirection.
+
+// Parse a Python int as i32 codepoint. Returns 0 on success and writes
+// the value to *out; returns -1 on error with a Python exception set.
+static int
+cp_parse_i32(PyObject *arg, int32_t *out) {
+    int overflow;
+    long c = PyLong_AsLongAndOverflow(arg, &overflow);
+    if (c == -1 && PyErr_Occurred())
+        return -1;
+    if (overflow != 0 || c < INT32_MIN || c > INT32_MAX) {
+        PyErr_SetString(PyExc_OverflowError,
+                        "codepoint out of i32 range");
+        return -1;
+    }
+    *out = (int32_t)c;
+    return 0;
+}
+
+#define DEFINE_CP_BOOL_WRAPPER(name, fn)                                    \
+    static PyObject*                                                        \
+    cp_##name(PyObject *module, PyObject *arg) {                            \
+        int32_t c;                                                          \
+        if (cp_parse_i32(arg, &c) < 0)                                      \
+            return NULL;                                                    \
+        return PyBool_FromLong(fn(c));                                      \
+    }
+
+DEFINE_CP_BOOL_WRAPPER(isspace, LibRTStrings_IsSpace)
+DEFINE_CP_BOOL_WRAPPER(isdigit, LibRTStrings_IsDigit)
+DEFINE_CP_BOOL_WRAPPER(isalnum, LibRTStrings_IsAlnum)
+DEFINE_CP_BOOL_WRAPPER(isalpha, LibRTStrings_IsAlpha)
+DEFINE_CP_BOOL_WRAPPER(isidentifier, LibRTStrings_IsIdentifier)
+
+#define DEFINE_CP_I32_WRAPPER(name, fn)                                     \
+    static PyObject*                                                        \
+    cp_##name(PyObject *module, PyObject *arg) {                            \
+        int32_t c;                                                          \
+        if (cp_parse_i32(arg, &c) < 0)                                      \
+            return NULL;                                                    \
+        return PyLong_FromLong((long) fn(c));                               \
+    }
+
+DEFINE_CP_I32_WRAPPER(toupper, LibRTStrings_ToUpper)
+DEFINE_CP_I32_WRAPPER(tolower, LibRTStrings_ToLower)
+
 static PyMethodDef librt_strings_module_methods[] = {
     {"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL,
      PyDoc_STR("Write a 16-bit signed integer to BytesWriter in little-endian 
format")
@@ -1214,6 +1264,27 @@
     {"read_f64_be", (PyCFunction) read_f64_be, METH_FASTCALL,
      PyDoc_STR("Read a 64-bit float from bytes in big-endian format")
     },
+    {"isspace", cp_isspace, METH_O,
+     PyDoc_STR("Test whether a codepoint (i32) is Unicode whitespace.")
+    },
+    {"isdigit", cp_isdigit, METH_O,
+     PyDoc_STR("Test whether a codepoint (i32) is a Unicode digit.")
+    },
+    {"isalnum", cp_isalnum, METH_O,
+     PyDoc_STR("Test whether a codepoint (i32) is alphanumeric.")
+    },
+    {"isalpha", cp_isalpha, METH_O,
+     PyDoc_STR("Test whether a codepoint (i32) is a Unicode letter.")
+    },
+    {"isidentifier", cp_isidentifier, METH_O,
+     PyDoc_STR("Test whether a codepoint (i32) is a valid identifier start 
(XID_Start).")
+    },
+    {"toupper", cp_toupper, METH_O,
+     PyDoc_STR("Single-codepoint uppercase mapping for a codepoint (i32). 
Returns the input unchanged if the Unicode uppercase expands to multiple 
codepoints (e.g. U+00DF uppercases to \"SS\"); use str.upper() for full Unicode 
case conversion.")
+    },
+    {"tolower", cp_tolower, METH_O,
+     PyDoc_STR("Single-codepoint lowercase mapping for a codepoint (i32). 
Returns the input unchanged if the Unicode lowercase expands to multiple 
codepoints; use str.lower() for full Unicode case conversion.")
+    },
     {NULL, NULL, 0, NULL}
 };
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/librt-0.11.0/strings/librt_strings.h 
new/librt-0.12.0/strings/librt_strings.h
--- old/librt-0.11.0/strings/librt_strings.h    2026-05-10 19:48:09.000000000 
+0200
+++ new/librt-0.12.0/strings/librt_strings.h    2026-06-30 17:48:02.000000000 
+0200
@@ -3,6 +3,8 @@
 
 #include <Python.h>
 #include <stdbool.h>
+#include <stdint.h>
+#include "CPy.h"
 #include "librt_strings_common.h"
 
 // ABI version -- only an exact match is compatible. This will only be changed 
in
@@ -28,4 +30,102 @@
     char data[WRITER_EMBEDDED_BUF_LEN];  // Default buffer
 } StringWriterObject;
 
+// Codepoint classification helpers. Inputs are signed i32 for compatibility
+// with mypyc's int32_rprimitive; out-of-range values (negative, or past the
+// maximum Unicode code point 0x10FFFF) are non-codepoints and return false.
+// Defined `static inline` so they compile statically into
+// both the librt.strings module and any mypyc-compiled extension that
+// includes this header, avoiding the capsule indirection that would dwarf
+// the work of a single Py_UNICODE_IS* macro call.
+
+static inline bool LibRTStrings_IsSpace(int32_t c) {
+    return c >= 0 && Py_UNICODE_ISSPACE((Py_UCS4)c);
+}
+
+static inline bool LibRTStrings_IsDigit(int32_t c) {
+    return c >= 0 && Py_UNICODE_ISDIGIT((Py_UCS4)c);
+}
+
+static inline bool LibRTStrings_IsAlnum(int32_t c) {
+    return c >= 0 && Py_UNICODE_ISALNUM((Py_UCS4)c);
+}
+
+static inline bool LibRTStrings_IsAlpha(int32_t c) {
+    return c >= 0 && Py_UNICODE_ISALPHA((Py_UCS4)c);
+}
+
+// True if c could start a valid identifier (XID_Start, per PEP 3131).
+// ASCII fast path covers `[A-Za-z_]`; non-ASCII delegates to CPython's
+// PyUnicode_IsIdentifier on a 1-character string. Aborts via
+// CPyError_OutOfMemory on allocation failure to keep this ERR_NEVER.
+static inline bool LibRTStrings_IsIdentifier(int32_t c) {
+    // Unsigned compare: negatives wrap to large values and skip the fast path.
+    if ((uint32_t)c < 128) {
+        return (c >= 'a' && c <= 'z')
+            || (c >= 'A' && c <= 'Z')
+            || c == '_';
+    }
+    // Reject negatives and code points past the Unicode maximum.
+    if ((uint32_t)c > 0x10FFFF) return false;
+    PyObject *s = PyUnicode_FromOrdinal((int)c);
+    if (s == NULL) {
+        CPyError_OutOfMemory();
+    }
+    int r = PyUnicode_IsIdentifier(s);
+    Py_DECREF(s);
+    return r == 1;
+}
+
+// Shared slow path for LibRTStrings_ToUpper / _ToLower. Round-trips the
+// codepoint through CPython's str.upper / str.lower on a 1-character
+// string. When the conversion expands to multiple codepoints (e.g.
+// 'ß'.upper() == 'SS') we return the input unchanged so the public
+// helpers stay i32 -> i32. Aborts via CPyError_OutOfMemory on allocation
+// failure.
+static inline int32_t LibRTStrings_ChangeCase_slow(int32_t c, const char 
*method) {
+    PyObject *s = PyUnicode_FromOrdinal((int)c);
+    if (s == NULL) {
+        CPyError_OutOfMemory();
+    }
+    PyObject *u = PyObject_CallMethod(s, method, NULL);
+    Py_DECREF(s);
+    if (u == NULL) {
+        CPyError_OutOfMemory();
+    }
+    int32_t result = c;
+    if (PyUnicode_GET_LENGTH(u) == 1) {
+        result = (int32_t)PyUnicode_READ_CHAR(u, 0);
+    }
+    Py_DECREF(u);
+    return result;
+}
+
+// Uppercase a codepoint. ASCII fast path is `a..z -> A..Z` (subtract 32);
+// non-ASCII delegates to str.upper on a 1-character string. Returns the
+// input unchanged when uppercasing expands to multiple codepoints.
+static inline int32_t LibRTStrings_ToUpper(int32_t c) {
+    // Unsigned compare: negatives wrap to large values and skip the fast path.
+    if ((uint32_t)c < 128) {
+        if (c >= 'a' && c <= 'z') return c - 32;
+        return c;
+    }
+    // Negatives and code points past the Unicode maximum are returned 
unchanged.
+    if ((uint32_t)c > 0x10FFFF) return c;
+    return LibRTStrings_ChangeCase_slow(c, "upper");
+}
+
+// Lowercase a codepoint. ASCII fast path is `A..Z -> a..z` (add 32);
+// non-ASCII delegates to str.lower on a 1-character string. Returns the
+// input unchanged when lowercasing expands to multiple codepoints.
+static inline int32_t LibRTStrings_ToLower(int32_t c) {
+    // Unsigned compare: negatives wrap to large values and skip the fast path.
+    if ((uint32_t)c < 128) {
+        if (c >= 'A' && c <= 'Z') return c + 32;
+        return c;
+    }
+    // Negatives and code points past the Unicode maximum are returned 
unchanged.
+    if ((uint32_t)c > 0x10FFFF) return c;
+    return LibRTStrings_ChangeCase_slow(c, "lower");
+}
+
 #endif  // LIBRT_STRINGS_H

Reply via email to