Skip to content

Commit 3bebe46

Browse files
authored
gh-128911: Add PyImport_ImportModuleAttr() function (#128912)
Add PyImport_ImportModuleAttr() and PyImport_ImportModuleAttrString() functions. * Add unit tests. * Replace _PyImport_GetModuleAttr() with PyImport_ImportModuleAttr(). * Replace _PyImport_GetModuleAttrString() with PyImport_ImportModuleAttrString(). * Remove "pycore_import.h" includes, no longer needed.
1 parent f927204 commit 3bebe46

40 files changed

+194
-56
lines changed

Doc/c-api/import.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,24 @@ Importing Modules
325325
If Python is initialized multiple times, :c:func:`PyImport_AppendInittab` or
326326
:c:func:`PyImport_ExtendInittab` must be called before each Python
327327
initialization.
328+
329+
330+
.. c:function:: PyObject* PyImport_ImportModuleAttr(PyObject *mod_name, PyObject *attr_name)
331+
332+
Import the module *mod_name* and get its attribute *attr_name*.
333+
334+
Names must be Python :class:`str` objects.
335+
336+
Helper function combining :c:func:`PyImport_Import` and
337+
:c:func:`PyObject_GetAttr`. For example, it can raise :exc:`ImportError` if
338+
the module is not found, and :exc:`AttributeError` if the attribute doesn't
339+
exist.
340+
341+
.. versionadded:: 3.14
342+
343+
.. c:function:: PyObject* PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name)
344+
345+
Similar to :c:func:`PyImport_ImportModuleAttr`, but names are UTF-8 encoded
346+
strings instead of Python :class:`str` objects.
347+
348+
.. versionadded:: 3.14

Doc/data/refcounts.dat

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3052,3 +3052,11 @@ _Py_c_quot:Py_complex:divisor::
30523052
_Py_c_sum:Py_complex:::
30533053
_Py_c_sum:Py_complex:left::
30543054
_Py_c_sum:Py_complex:right::
3055+
3056+
PyImport_ImportModuleAttr:PyObject*::+1:
3057+
PyImport_ImportModuleAttr:PyObject*:mod_name:0:
3058+
PyImport_ImportModuleAttr:PyObject*:attr_name:0:
3059+
3060+
PyImport_ImportModuleAttrString:PyObject*::+1:
3061+
PyImport_ImportModuleAttrString:const char *:mod_name::
3062+
PyImport_ImportModuleAttrString:const char *:attr_name::

Doc/whatsnew/3.14.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,11 @@ New features
13221322
* Add :c:func:`PyUnstable_IsImmortal` for determining whether an object is :term:`immortal`,
13231323
for debugging purposes.
13241324

1325+
* Add :c:func:`PyImport_ImportModuleAttr` and
1326+
:c:func:`PyImport_ImportModuleAttrString` helper functions to import a module
1327+
and get an attribute of the module.
1328+
(Contributed by Victor Stinner in :gh:`128911`.)
1329+
13251330

13261331
Limited C API changes
13271332
---------------------

Include/cpython/import.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,10 @@ struct _frozen {
2121
collection of frozen modules: */
2222

2323
PyAPI_DATA(const struct _frozen *) PyImport_FrozenModules;
24+
25+
PyAPI_FUNC(PyObject*) PyImport_ImportModuleAttr(
26+
PyObject *mod_name,
27+
PyObject *attr_name);
28+
PyAPI_FUNC(PyObject*) PyImport_ImportModuleAttrString(
29+
const char *mod_name,
30+
const char *attr_name);

Include/internal/pycore_import.h

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,6 @@ extern int _PyImport_FixupBuiltin(
3131
PyObject *modules
3232
);
3333

34-
// Export for many shared extensions, like '_json'
35-
PyAPI_FUNC(PyObject*) _PyImport_GetModuleAttr(PyObject *, PyObject *);
36-
37-
// Export for many shared extensions, like '_datetime'
38-
PyAPI_FUNC(PyObject*) _PyImport_GetModuleAttrString(const char *, const char *);
39-
4034

4135
struct _import_runtime_state {
4236
/* The builtin modules (defined in config.c). */

Lib/test/test_capi/test_import.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from test.support import import_helper
88
from test.support.warnings_helper import check_warnings
99

10+
_testcapi = import_helper.import_module('_testcapi')
1011
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
1112
NULL = None
1213

@@ -148,7 +149,7 @@ def check_frozen_import(self, import_frozen_module):
148149
try:
149150
self.assertEqual(import_frozen_module('zipimport'), 1)
150151

151-
# import zipimport again
152+
# import zipimport again
152153
self.assertEqual(import_frozen_module('zipimport'), 1)
153154
finally:
154155
sys.modules['zipimport'] = old_zipimport
@@ -317,6 +318,59 @@ def test_executecodemoduleobject(self):
317318
# CRASHES execute_code_func(NULL, code, NULL, NULL)
318319
# CRASHES execute_code_func(name, NULL, NULL, NULL)
319320

321+
def check_importmoduleattr(self, importmoduleattr):
322+
self.assertIs(importmoduleattr('sys', 'argv'), sys.argv)
323+
self.assertIs(importmoduleattr('types', 'ModuleType'), types.ModuleType)
324+
325+
# module name containing a dot
326+
attr = importmoduleattr('email.message', 'Message')
327+
from email.message import Message
328+
self.assertIs(attr, Message)
329+
330+
with self.assertRaises(ImportError):
331+
# nonexistent module
332+
importmoduleattr('nonexistentmodule', 'attr')
333+
with self.assertRaises(AttributeError):
334+
# nonexistent attribute
335+
importmoduleattr('sys', 'nonexistentattr')
336+
with self.assertRaises(AttributeError):
337+
# attribute name containing a dot
338+
importmoduleattr('sys', 'implementation.name')
339+
340+
def test_importmoduleattr(self):
341+
# Test PyImport_ImportModuleAttr()
342+
importmoduleattr = _testcapi.PyImport_ImportModuleAttr
343+
self.check_importmoduleattr(importmoduleattr)
344+
345+
# Invalid module name type
346+
for mod_name in (object(), 123, b'bytes'):
347+
with self.subTest(mod_name=mod_name):
348+
with self.assertRaises(TypeError):
349+
importmoduleattr(mod_name, "attr")
350+
351+
# Invalid attribute name type
352+
for attr_name in (object(), 123, b'bytes'):
353+
with self.subTest(attr_name=attr_name):
354+
with self.assertRaises(TypeError):
355+
importmoduleattr("sys", attr_name)
356+
357+
with self.assertRaises(SystemError):
358+
importmoduleattr(NULL, "argv")
359+
# CRASHES importmoduleattr("sys", NULL)
360+
361+
def test_importmoduleattrstring(self):
362+
# Test PyImport_ImportModuleAttrString()
363+
importmoduleattr = _testcapi.PyImport_ImportModuleAttrString
364+
self.check_importmoduleattr(importmoduleattr)
365+
366+
with self.assertRaises(UnicodeDecodeError):
367+
importmoduleattr(b"sys\xff", "argv")
368+
with self.assertRaises(UnicodeDecodeError):
369+
importmoduleattr("sys", b"argv\xff")
370+
371+
# CRASHES importmoduleattr(NULL, "argv")
372+
# CRASHES importmoduleattr("sys", NULL)
373+
320374
# TODO: test PyImport_GetImporter()
321375
# TODO: test PyImport_ReloadModule()
322376
# TODO: test PyImport_ExtendInittab()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :c:func:`PyImport_ImportModuleAttr` and :c:func:`PyImport_ImportModuleAttrString`
2+
helper functions to import a module and get an attribute of the module. Patch
3+
by Victor Stinner.

Modules/Setup.stdlib.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
163163
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
164164
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
165-
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c _testcapi/config.c
165+
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c
166166
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c
167167
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
168168
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

Modules/_ctypes/callbacks.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ long Call_GetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv)
492492
if (context == NULL)
493493
context = PyUnicode_InternFromString("_ctypes.DllGetClassObject");
494494

495-
func = _PyImport_GetModuleAttrString("ctypes", "DllGetClassObject");
495+
func = PyImport_ImportModuleAttrString("ctypes", "DllGetClassObject");
496496
if (!func) {
497497
PyErr_WriteUnraisable(context ? context : Py_None);
498498
/* There has been a warning before about this already */

Modules/_ctypes/stgdict.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct
257257
goto error;
258258
}
259259

260-
PyObject *layout_func = _PyImport_GetModuleAttrString("ctypes._layout",
260+
PyObject *layout_func = PyImport_ImportModuleAttrString("ctypes._layout",
261261
"get_layout");
262262
if (!layout_func) {
263263
goto error;

0 commit comments

Comments
 (0)