diff --git a/docs/api.rst b/docs/api.rst index e3ded07..a25b9f9 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,18 @@ 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 `__. + + 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..f593147 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ Changelog ========= +* 2026-10-06: Add functions: + + * ``PyImport_ImportModuleAttr()`` + * ``PyImport_ImportModuleAttrString()`` + * ``PyType_GetFullyQualifiedName()`` + * ``PyType_GetModuleName()`` + * ``PyType_GetName()`` + * ``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..d423a17 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,100 @@ 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_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) +{ + 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/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__": diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index c9a91cc..1da8c5b 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,53 @@ 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; + + // Test PyType_GetModuleName() + 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); + + // 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 + 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 +2683,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} };