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 @@ [](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
