Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.python.org/dev/c-api/import.html#c.PyImport_ImportModuleAttr>`__.

.. c:function:: PyObject* PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name)

See `PyImport_ImportModuleAttrString() documentation <https://docs.python.org/dev/c-api/import.html#c.PyImport_ImportModuleAttrString>`__.

Not supported:

* ``PyConfig_Names()``
Expand Down Expand Up @@ -500,6 +508,14 @@ Python 3.13

See `PyDict_SetDefaultRef() documentation <https://docs.python.org/dev/c-api/dict.html#c.PyDict_SetDefaultRef>`__.

.. c:function:: PyObject* PyType_GetModuleName(PyTypeObject *type)

See `PyType_GetModuleName() documentation <https://docs.python.org/dev/c-api/type.html#c.PyType_GetModuleName>`__.

.. c:function:: PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type)

See `PyType_GetFullyQualifiedName() documentation <https://docs.python.org/dev/c-api/type.html#c.PyType_GetFullyQualifiedName>`__.


Not supported:

Expand All @@ -510,8 +526,6 @@ Not supported:
* ``PyObject_GenericHash()``.
* ``PySys_Audit()``.
* ``PySys_AuditTuple()``.
* ``PyType_GetFullyQualifiedName()``
* ``PyType_GetModuleName()``

Python 3.12
-----------
Expand Down Expand Up @@ -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 <https://docs.python.org/dev/c-api/type.html#c.PyType_GetName>`__.

.. c:function:: PyObject* PyType_GetQualName(PyTypeObject *type)

See `PyType_GetQualName() documentation <https://docs.python.org/dev/c-api/type.html#c.PyType_GetQualName>`__.


Not supported:

* ``PyType_GetModuleByDef()``.
* ``PyType_GetName()``.
* ``PyType_GetQualName()``.
* ``Py_Version`` constant.
* ``PyErr_GetHandledException()``, ``PyErr_SetHandledException()``.
* ``PyFrame_GetGenerator()``.
Expand Down
9 changes: 9 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down
101 changes: 101 additions & 0 deletions pythoncapi_compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import os.path
import shutil
import sys
import time
from shutil import which


Expand Down Expand Up @@ -85,6 +86,7 @@ def parse_args():


def main():
start_time = time.perf_counter()
args = parse_args()

path = os.path.join(TEST_DIR, 'build')
Expand Down Expand Up @@ -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__":
Expand Down
70 changes: 70 additions & 0 deletions tests/test_pythoncapi_compat_cext.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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}
};

Expand Down
Loading