Skip to content

Commit 183f44b

Browse files
authored
Add PyImport_ImportModuleAttr() and PyType_GetQualName() (#189)
Add functions: * PyImport_ImportModuleAttr() * PyImport_ImportModuleAttrString() * PyType_GetFullyQualifiedName() * PyType_GetModuleName() * PyType_GetQualName() * PyType_GetName() Fix PyUnicode_EqualToUTF8AndSize() on PyPy: call PyUnicode_READY(). runtests.py logs elapsed time. Closes #188
1 parent ebbf075 commit 183f44b

5 files changed

Lines changed: 211 additions & 4 deletions

File tree

‎docs/api.rst‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,14 @@ Python 3.14
306306

307307
Not available on PyPy. ``Py_REFCNT()`` semantics is different on PyPy.
308308

309+
.. c:function:: PyObject* PyImport_ImportModuleAttr(PyObject *mod_name, PyObject *attr_name)
310+
311+
See `PyImport_ImportModuleAttr() documentation <https://docs.python.org/dev/c-api/import.html#c.PyImport_ImportModuleAttr>`__.
312+
313+
.. c:function:: PyObject* PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name)
314+
315+
See `PyImport_ImportModuleAttrString() documentation <https://docs.python.org/dev/c-api/import.html#c.PyImport_ImportModuleAttrString>`__.
316+
309317
Not supported:
310318

311319
* ``PyConfig_Names()``
@@ -500,6 +508,14 @@ Python 3.13
500508

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

511+
.. c:function:: PyObject* PyType_GetModuleName(PyTypeObject *type)
512+
513+
See `PyType_GetModuleName() documentation <https://docs.python.org/dev/c-api/type.html#c.PyType_GetModuleName>`__.
514+
515+
.. c:function:: PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type)
516+
517+
See `PyType_GetFullyQualifiedName() documentation <https://docs.python.org/dev/c-api/type.html#c.PyType_GetFullyQualifiedName>`__.
518+
503519

504520
Not supported:
505521

@@ -510,8 +526,6 @@ Not supported:
510526
* ``PyObject_GenericHash()``.
511527
* ``PySys_Audit()``.
512528
* ``PySys_AuditTuple()``.
513-
* ``PyType_GetFullyQualifiedName()``
514-
* ``PyType_GetModuleName()``
515529

516530
Python 3.12
517531
-----------
@@ -653,11 +667,18 @@ Python 3.11
653667

654668
Not available on PyPy nor on limited C API 3.10 and older.
655669

670+
.. c:function:: PyObject* PyType_GetName(PyTypeObject *type)
671+
672+
See `PyType_GetName() documentation <https://docs.python.org/dev/c-api/type.html#c.PyType_GetName>`__.
673+
674+
.. c:function:: PyObject* PyType_GetQualName(PyTypeObject *type)
675+
676+
See `PyType_GetQualName() documentation <https://docs.python.org/dev/c-api/type.html#c.PyType_GetQualName>`__.
677+
678+
656679
Not supported:
657680

658681
* ``PyType_GetModuleByDef()``.
659-
* ``PyType_GetName()``.
660-
* ``PyType_GetQualName()``.
661682
* ``Py_Version`` constant.
662683
* ``PyErr_GetHandledException()``, ``PyErr_SetHandledException()``.
663684
* ``PyFrame_GetGenerator()``.

‎docs/changelog.rst‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Changelog
22
=========
33

4+
* 2026-10-06: Add functions:
5+
6+
* ``PyImport_ImportModuleAttr()``
7+
* ``PyImport_ImportModuleAttrString()``
8+
* ``PyType_GetFullyQualifiedName()``
9+
* ``PyType_GetModuleName()``
10+
* ``PyType_GetName()``
11+
* ``PyType_GetQualName()``
12+
413
* 2026-10-04: Add support for the limited C API (``Py_LIMITED_API``).
514
* 2026-09-23: ``PyUnstable_Object_IsUniquelyReferenced()``,
615
``PyUnstable_TryIncRef()`` and ``PyUnstable_EnableTryIncRef()`` are no longer

‎pythoncapi_compat.h‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,13 @@ PyUnicode_EqualToUTF8AndSize(PyObject *unicode, const char *str, Py_ssize_t str_
10101010
PyErr_Fetch(&exc_type, &exc_value, &exc_tb);
10111011

10121012
#ifndef Py_LIMITED_API
1013+
if (PyUnicode_READY(unicode) < 0) {
1014+
// Memory allocation failure. The API cannot report error,
1015+
// so ignore the exception and return 0.
1016+
res = 0;
1017+
goto done;
1018+
}
1019+
10131020
if (PyUnicode_IS_ASCII(unicode)) {
10141021
utf8 = PyUnicode_DATA(unicode);
10151022
len = PyUnicode_GET_LENGTH(unicode);
@@ -2858,6 +2865,100 @@ PyUnstable_SetImmortal(PyObject *op)
28582865
}
28592866
#endif
28602867

2868+
2869+
// Python 3.14 added PyImport_ImportModuleAttr()
2870+
// and PyImport_ImportModuleAttrString()
2871+
#if PY_VERSION_HEX < 0x030E0000 || defined(Py_LIMITED_API)
2872+
static inline PyObject*
2873+
PyImport_ImportModuleAttr(PyObject *mod_name, PyObject *attr_name)
2874+
{
2875+
PyObject *mod = PyImport_Import(mod_name);
2876+
if (mod == NULL) {
2877+
return NULL;
2878+
}
2879+
2880+
PyObject *result = PyObject_GetAttr(mod, attr_name);
2881+
Py_DECREF(mod);
2882+
return result;
2883+
}
2884+
2885+
static inline PyObject *
2886+
PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name)
2887+
{
2888+
PyObject *mod_name_obj = PyUnicode_FromString(mod_name);
2889+
if (mod_name_obj == NULL) {
2890+
return NULL;
2891+
}
2892+
PyObject *attr_name_obj = PyUnicode_FromString(attr_name);
2893+
if (attr_name_obj == NULL) {
2894+
Py_DECREF(mod_name_obj);
2895+
return NULL;
2896+
}
2897+
2898+
PyObject *result = PyImport_ImportModuleAttr(mod_name_obj, attr_name_obj);
2899+
Py_DECREF(attr_name_obj);
2900+
Py_DECREF(mod_name_obj);
2901+
return result;
2902+
}
2903+
#endif
2904+
2905+
2906+
// Python 3.11 added PyType_GetName() and PyType_GetQualName()
2907+
#if (PY_VERSION_HEX < 0x030B0000 \
2908+
|| (defined(Py_LIMITED_API) && Py_LIMITED_API+0 < 0x030B0000))
2909+
static inline PyObject*
2910+
PyType_GetName(PyTypeObject *type)
2911+
{
2912+
return PyObject_GetAttrString(_PyObject_CAST(type), "__name__");
2913+
}
2914+
2915+
static inline PyObject*
2916+
PyType_GetQualName(PyTypeObject *type)
2917+
{
2918+
return PyObject_GetAttrString(_PyObject_CAST(type), "__qualname__");
2919+
}
2920+
#endif
2921+
2922+
2923+
// Python 3.13 added PyType_GetModuleName() and PyType_GetFullyQualifiedName()
2924+
#if PY_VERSION_HEX < 0x030D0000
2925+
static inline PyObject*
2926+
PyType_GetModuleName(PyTypeObject *type)
2927+
{
2928+
return PyObject_GetAttrString(_PyObject_CAST(type), "__module__");
2929+
}
2930+
2931+
static inline PyObject *
2932+
PyType_GetFullyQualifiedName(PyTypeObject *type)
2933+
{
2934+
PyObject *qualname = PyType_GetQualName(type);
2935+
if (qualname == NULL) {
2936+
return NULL;
2937+
}
2938+
2939+
PyObject *module = PyType_GetModuleName(type);
2940+
if (module == NULL) {
2941+
Py_DECREF(qualname);
2942+
return NULL;
2943+
}
2944+
2945+
PyObject *result;
2946+
if (PyUnicode_Check(module)
2947+
&& PyUnicode_EqualToUTF8(module, "builtins") == 0
2948+
&& PyUnicode_EqualToUTF8(module, "__main__") == 0)
2949+
{
2950+
result = PyUnicode_FromFormat("%U.%U", module, qualname);
2951+
}
2952+
else {
2953+
result = Py_NewRef(qualname);
2954+
}
2955+
Py_DECREF(module);
2956+
Py_DECREF(qualname);
2957+
return result;
2958+
}
2959+
#endif
2960+
2961+
28612962
#ifdef __cplusplus
28622963
}
28632964
#endif

‎runtests.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import os.path
1313
import shutil
1414
import sys
15+
import time
1516
from shutil import which
1617

1718

@@ -85,6 +86,7 @@ def parse_args():
8586

8687

8788
def main():
89+
start_time = time.perf_counter()
8890
args = parse_args()
8991

9092
path = os.path.join(TEST_DIR, 'build')
@@ -112,6 +114,10 @@ def main():
112114
print("Tested: %s Python executables" % len(tested))
113115
else:
114116
run_tests_exe(sys.executable, args.verbose, tested)
117+
print()
118+
119+
dt = time.perf_counter() - start_time
120+
print(f"Total time: {dt:.1f} seconds")
115121

116122

117123
if __name__ == "__main__":

‎tests/test_pythoncapi_compat_cext.c‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,28 @@ test_import(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
757757
Py_DECREF(mod2);
758758
Py_DECREF(mod);
759759

760+
// Test PyImport_ImportModuleAttr()
761+
PyObject *mod_name = PyUnicode_FromString("sys");
762+
assert(mod_name != NULL);
763+
PyObject *attr_name = PyUnicode_FromString("platform");
764+
assert(attr_name != NULL);
765+
PyObject *attr = PyImport_ImportModuleAttr(mod_name, attr_name);
766+
Py_DECREF(mod_name);
767+
Py_DECREF(attr_name);
768+
if (attr == NULL) {
769+
return NULL;
770+
}
771+
assert(PyUnicode_Check(attr));
772+
Py_DECREF(attr);
773+
774+
// Test PyImport_ImportModuleAttrString()
775+
attr = PyImport_ImportModuleAttrString("sys", "platform");
776+
if (attr == NULL) {
777+
return NULL;
778+
}
779+
assert(PyUnicode_Check(attr));
780+
Py_DECREF(attr);
781+
760782
Py_RETURN_NONE;
761783
}
762784

@@ -2540,6 +2562,53 @@ test_set_immortal(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
25402562
#endif
25412563

25422564

2565+
static PyObject *
2566+
test_type(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
2567+
{
2568+
PyObject *attr = PyImport_ImportModuleAttrString("datetime", "time");
2569+
if (attr == NULL) {
2570+
return NULL;
2571+
}
2572+
assert(PyType_Check(attr));
2573+
PyTypeObject *type = (PyTypeObject*)attr;
2574+
2575+
// Test PyType_GetModuleName()
2576+
PyObject *modname = PyType_GetModuleName(type);
2577+
assert(modname != NULL);
2578+
#if !defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030C0000
2579+
assert(PyUnicode_EqualToUTF8(modname, "datetime") == 1);
2580+
#else
2581+
assert(PyUnicode_EqualToUTF8(modname, "_pydatetime") == 1);
2582+
#endif
2583+
Py_DECREF(modname);
2584+
2585+
// Test PyType_GetName()
2586+
PyObject *name = PyType_GetName(type);
2587+
assert(name != NULL);
2588+
assert(PyUnicode_EqualToUTF8(name, "time") == 1);
2589+
Py_DECREF(name);
2590+
2591+
// Test PyType_GetQualName()
2592+
PyObject *qualname = PyType_GetQualName(type);
2593+
assert(qualname != NULL);
2594+
assert(PyUnicode_EqualToUTF8(qualname, "time") == 1);
2595+
Py_DECREF(qualname);
2596+
2597+
// Test PyType_GetFullyQualifiedName()
2598+
PyObject *fqn = PyType_GetFullyQualifiedName(type);
2599+
assert(fqn != NULL);
2600+
#if !defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030C0000
2601+
assert(PyUnicode_EqualToUTF8(fqn, "datetime.time") == 1);
2602+
#else
2603+
assert(PyUnicode_EqualToUTF8(fqn, "_pydatetime.time") == 1);
2604+
#endif
2605+
Py_DECREF(fqn);
2606+
2607+
Py_DECREF(type);
2608+
Py_RETURN_NONE;
2609+
}
2610+
2611+
25432612
static struct PyMethodDef methods[] = {
25442613
{"test_object", test_object, METH_NOARGS, _Py_NULL},
25452614
{"test_py_is", test_py_is, METH_NOARGS, _Py_NULL},
@@ -2614,6 +2683,7 @@ static struct PyMethodDef methods[] = {
26142683
#if 0x030D0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) && !defined(Py_LIMITED_API)
26152684
{"test_set_immortal", test_set_immortal, METH_NOARGS, _Py_NULL},
26162685
#endif
2686+
{"test_type", test_type, METH_NOARGS, _Py_NULL},
26172687
{_Py_NULL, _Py_NULL, 0, _Py_NULL}
26182688
};
26192689

0 commit comments

Comments
 (0)