From f53af5c8df6833b6e9b31f51cba17bcb6185dd81 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 8 Oct 2026 14:41:11 +0200 Subject: [PATCH 1/4] Add PyImport_ImportModuleAttr() and PyType_GetQualName() Add functions: * PyImport_ImportModuleAttr() * PyImport_ImportModuleAttrString() * PyType_GetFullyQualifiedName() * PyType_GetModuleName() * PyType_GetQualName() Fix PyUnicode_EqualToUTF8AndSize() on PyPy: call PyUnicode_READY(). Closes #188 --- docs/api.rst | 24 +++++++- docs/changelog.rst | 8 +++ pythoncapi_compat.h | 93 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 61 +++++++++++++++++++ 4 files changed, 183 insertions(+), 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e3ded07..8e9b77c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -306,6 +306,14 @@ Python 3.14 Not available on PyPy. ``Py_REFCNT()`` semantics is different on PyPy. +.. c:function:: PyObject* PyImport_ImportModuleAttr(PyObject *mod_name, PyObject *attr_name) + + See `PyImport_ImportModuleAttr() documentation `__. + +.. c:function:: PyObject* PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name) + + See `PyImport_ImportModuleAttrString() documentation `__. + Not supported: * ``PyConfig_Names()`` @@ -500,6 +508,14 @@ Python 3.13 See `PyDict_SetDefaultRef() documentation `__. +.. c:function:: PyObject* PyType_GetModuleName(PyTypeObject *type) + + See `PyType_GetModuleName() documentation `__. + +.. c:function:: PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type) + + See `PyType_GetFullyQualifiedName() documentation `__. + Not supported: @@ -510,8 +526,6 @@ Not supported: * ``PyObject_GenericHash()``. * ``PySys_Audit()``. * ``PySys_AuditTuple()``. -* ``PyType_GetFullyQualifiedName()`` -* ``PyType_GetModuleName()`` Python 3.12 ----------- @@ -653,11 +667,15 @@ Python 3.11 Not available on PyPy nor on limited C API 3.10 and older. +.. c:function:: PyObject* PyType_GetQualName(PyTypeObject *type) + + See `PyType_GetQualName() documentation `__. + + Not supported: * ``PyType_GetModuleByDef()``. * ``PyType_GetName()``. -* ``PyType_GetQualName()``. * ``Py_Version`` constant. * ``PyErr_GetHandledException()``, ``PyErr_SetHandledException()``. * ``PyFrame_GetGenerator()``. diff --git a/docs/changelog.rst b/docs/changelog.rst index 4513151..8bdecb2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ Changelog ========= +* 2026-10-06: Add functions: + + * ``PyImport_ImportModuleAttr()`` + * ``PyImport_ImportModuleAttrString()`` + * ``PyType_GetFullyQualifiedName()`` + * ``PyType_GetModuleName()`` + * ``PyType_GetQualName()`` + * 2026-10-04: Add support for the limited C API (``Py_LIMITED_API``). * 2026-09-23: ``PyUnstable_Object_IsUniquelyReferenced()``, ``PyUnstable_TryIncRef()`` and ``PyUnstable_EnableTryIncRef()`` are no longer diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 570ce85..ab177e4 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1010,6 +1010,13 @@ PyUnicode_EqualToUTF8AndSize(PyObject *unicode, const char *str, Py_ssize_t str_ PyErr_Fetch(&exc_type, &exc_value, &exc_tb); #ifndef Py_LIMITED_API + if (PyUnicode_READY(unicode) < 0) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + if (PyUnicode_IS_ASCII(unicode)) { utf8 = PyUnicode_DATA(unicode); len = PyUnicode_GET_LENGTH(unicode); @@ -2858,6 +2865,92 @@ PyUnstable_SetImmortal(PyObject *op) } #endif + +// Python 3.14 added PyImport_ImportModuleAttr() +// and PyImport_ImportModuleAttrString() +#if PY_VERSION_HEX < 0x030E0000 || defined(Py_LIMITED_API) +static inline PyObject* +PyImport_ImportModuleAttr(PyObject *mod_name, PyObject *attr_name) +{ + PyObject *mod = PyImport_Import(mod_name); + if (mod == NULL) { + return NULL; + } + PyObject *result = PyObject_GetAttr(mod, attr_name); + Py_DECREF(mod); + return result; +} + +static inline PyObject * +PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name) +{ + PyObject *mod_name_obj = PyUnicode_FromString(mod_name); + if (mod_name_obj == NULL) { + return NULL; + } + PyObject *attr_name_obj = PyUnicode_FromString(attr_name); + if (attr_name_obj == NULL) { + Py_DECREF(mod_name_obj); + return NULL; + } + PyObject *result = PyImport_ImportModuleAttr(mod_name_obj, attr_name_obj); + Py_DECREF(attr_name_obj); + Py_DECREF(mod_name_obj); + return result; +} +#endif + + +// Python 3.11 added PyType_GetQualName() +#if (PY_VERSION_HEX < 0x030B0000 \ + || (defined(Py_LIMITED_API) && Py_LIMITED_API+0 < 0x030B0000)) +static inline PyObject* +PyType_GetQualName(PyTypeObject *type) +{ + return PyObject_GetAttrString(_PyObject_CAST(type), "__qualname__"); +} +#endif + + +// Python 3.13 added PyType_GetModuleName() and PyType_GetFullyQualifiedName() +#if PY_VERSION_HEX < 0x030D0000 +static inline PyObject* +PyType_GetModuleName(PyTypeObject *type) +{ + return PyObject_GetAttrString(_PyObject_CAST(type), "__module__"); +} + +static inline PyObject * +PyType_GetFullyQualifiedName(PyTypeObject *type) +{ + PyObject *qualname = PyType_GetQualName(type); + if (qualname == NULL) { + return NULL; + } + + PyObject *module = PyType_GetModuleName(type); + if (module == NULL) { + Py_DECREF(qualname); + return NULL; + } + + PyObject *result; + if (PyUnicode_Check(module) + && PyUnicode_EqualToUTF8(module, "builtins") == 0 + && PyUnicode_EqualToUTF8(module, "__main__") == 0) + { + result = PyUnicode_FromFormat("%U.%U", module, qualname); + } + else { + result = Py_NewRef(qualname); + } + Py_DECREF(module); + Py_DECREF(qualname); + return result; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index c9a91cc..c67fff1 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -757,6 +757,28 @@ test_import(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) Py_DECREF(mod2); Py_DECREF(mod); + // Test PyImport_ImportModuleAttr() + PyObject *mod_name = PyUnicode_FromString("sys"); + assert(mod_name != NULL); + PyObject *attr_name = PyUnicode_FromString("platform"); + assert(attr_name != NULL); + PyObject *attr = PyImport_ImportModuleAttr(mod_name, attr_name); + Py_DECREF(mod_name); + Py_DECREF(attr_name); + if (attr == NULL) { + return NULL; + } + assert(PyUnicode_Check(attr)); + Py_DECREF(attr); + + // Test PyImport_ImportModuleAttrString() + attr = PyImport_ImportModuleAttrString("sys", "platform"); + if (attr == NULL) { + return NULL; + } + assert(PyUnicode_Check(attr)); + Py_DECREF(attr); + Py_RETURN_NONE; } @@ -2540,6 +2562,44 @@ test_set_immortal(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) #endif +static PyObject * +test_type(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject *attr = PyImport_ImportModuleAttrString("datetime", "time"); + if (attr == NULL) { + return NULL; + } + assert(PyType_Check(attr)); + PyTypeObject *type = (PyTypeObject*)attr; + + PyObject *modname = PyType_GetModuleName(type); + assert(modname != NULL); +#if !defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030C0000 + assert(PyUnicode_EqualToUTF8(modname, "datetime") == 1); +#else + assert(PyUnicode_EqualToUTF8(modname, "_pydatetime") == 1); +#endif + Py_DECREF(modname); + + PyObject *qualname = PyType_GetQualName(type); + assert(qualname != NULL); + assert(PyUnicode_EqualToUTF8(qualname, "time") == 1); + Py_DECREF(qualname); + + PyObject *fqn = PyType_GetFullyQualifiedName(type); + assert(fqn != NULL); +#if !defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030C0000 + assert(PyUnicode_EqualToUTF8(fqn, "datetime.time") == 1); +#else + assert(PyUnicode_EqualToUTF8(fqn, "_pydatetime.time") == 1); +#endif + Py_DECREF(fqn); + + Py_DECREF(type); + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -2614,6 +2674,7 @@ static struct PyMethodDef methods[] = { #if 0x030D0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) && !defined(Py_LIMITED_API) {"test_set_immortal", test_set_immortal, METH_NOARGS, _Py_NULL}, #endif + {"test_type", test_type, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 66ad07d0b5d607644c7bba403d8acb6fafa7ead7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 8 Oct 2026 16:52:25 +0200 Subject: [PATCH 2/4] runtests.py logs elapsed time --- runtests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/runtests.py b/runtests.py index 2754616..0493285 100755 --- a/runtests.py +++ b/runtests.py @@ -12,6 +12,7 @@ import os.path import shutil import sys +import time from shutil import which @@ -85,6 +86,7 @@ def parse_args(): def main(): + start_time = time.perf_counter() args = parse_args() path = os.path.join(TEST_DIR, 'build') @@ -112,6 +114,10 @@ def main(): print("Tested: %s Python executables" % len(tested)) else: run_tests_exe(sys.executable, args.verbose, tested) + print() + + dt = time.perf_counter() - start_time + print(f"Total time: {dt:.1f} seconds") if __name__ == "__main__": From 72ccb09ed0e2c86380b8e5edfcecd5046ff03499 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 8 Oct 2026 17:01:29 +0200 Subject: [PATCH 3/4] Add PyType_GetName() --- docs/api.rst | 5 ++++- docs/changelog.rst | 1 + pythoncapi_compat.h | 8 +++++++- tests/test_pythoncapi_compat_cext.c | 5 +++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 8e9b77c..a25b9f9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -667,6 +667,10 @@ Python 3.11 Not available on PyPy nor on limited C API 3.10 and older. +.. c:function:: PyObject* PyType_GetName(PyTypeObject *type) + + See `PyType_GetName() documentation `__. + .. c:function:: PyObject* PyType_GetQualName(PyTypeObject *type) See `PyType_GetQualName() documentation `__. @@ -675,7 +679,6 @@ Python 3.11 Not supported: * ``PyType_GetModuleByDef()``. -* ``PyType_GetName()``. * ``Py_Version`` constant. * ``PyErr_GetHandledException()``, ``PyErr_SetHandledException()``. * ``PyFrame_GetGenerator()``. diff --git a/docs/changelog.rst b/docs/changelog.rst index 8bdecb2..f593147 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ Changelog * ``PyImport_ImportModuleAttrString()`` * ``PyType_GetFullyQualifiedName()`` * ``PyType_GetModuleName()`` + * ``PyType_GetName()`` * ``PyType_GetQualName()`` * 2026-10-04: Add support for the limited C API (``Py_LIMITED_API``). diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index ab177e4..8442998 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2901,9 +2901,15 @@ PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name) #endif -// Python 3.11 added PyType_GetQualName() +// Python 3.11 added PyType_GetName() and PyType_GetQualName() #if (PY_VERSION_HEX < 0x030B0000 \ || (defined(Py_LIMITED_API) && Py_LIMITED_API+0 < 0x030B0000)) +static inline PyObject* +PyType_GetName(PyTypeObject *type) +{ + return PyObject_GetAttrString(_PyObject_CAST(type), "__name__"); +} + static inline PyObject* PyType_GetQualName(PyTypeObject *type) { diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index c67fff1..74180f5 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2581,6 +2581,11 @@ test_type(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) #endif Py_DECREF(modname); + PyObject *name = PyType_GetName(type); + assert(name != NULL); + assert(PyUnicode_EqualToUTF8(name, "time") == 1); + Py_DECREF(name); + PyObject *qualname = PyType_GetQualName(type); assert(qualname != NULL); assert(PyUnicode_EqualToUTF8(qualname, "time") == 1); From dab6aa19fd0bb70f17394a065eaf0cb5c6afafcd Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 8 Oct 2026 17:04:53 +0200 Subject: [PATCH 4/4] Add comments --- pythoncapi_compat.h | 2 ++ tests/test_pythoncapi_compat_cext.c | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 8442998..d423a17 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2876,6 +2876,7 @@ PyImport_ImportModuleAttr(PyObject *mod_name, PyObject *attr_name) if (mod == NULL) { return NULL; } + PyObject *result = PyObject_GetAttr(mod, attr_name); Py_DECREF(mod); return result; @@ -2893,6 +2894,7 @@ PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name) Py_DECREF(mod_name_obj); return NULL; } + PyObject *result = PyImport_ImportModuleAttr(mod_name_obj, attr_name_obj); Py_DECREF(attr_name_obj); Py_DECREF(mod_name_obj); diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 74180f5..1da8c5b 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2572,6 +2572,7 @@ test_type(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(PyType_Check(attr)); PyTypeObject *type = (PyTypeObject*)attr; + // Test PyType_GetModuleName() PyObject *modname = PyType_GetModuleName(type); assert(modname != NULL); #if !defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030C0000 @@ -2581,16 +2582,19 @@ test_type(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) #endif Py_DECREF(modname); + // Test PyType_GetName() PyObject *name = PyType_GetName(type); assert(name != NULL); assert(PyUnicode_EqualToUTF8(name, "time") == 1); Py_DECREF(name); + // Test PyType_GetQualName() PyObject *qualname = PyType_GetQualName(type); assert(qualname != NULL); assert(PyUnicode_EqualToUTF8(qualname, "time") == 1); Py_DECREF(qualname); + // Test PyType_GetFullyQualifiedName() PyObject *fqn = PyType_GetFullyQualifiedName(type); assert(fqn != NULL); #if !defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030C0000