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;

Modules/_cursesmodule.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ _PyCursesCheckFunction(int called, const char *funcname)
226226
if (called == TRUE) {
227227
return 1;
228228
}
229-
PyObject *exc = _PyImport_GetModuleAttrString("_curses", "error");
229+
PyObject *exc = PyImport_ImportModuleAttrString("_curses", "error");
230230
if (exc != NULL) {
231231
PyErr_Format(exc, "must call %s() first", funcname);
232232
Py_DECREF(exc);

Modules/_datetimemodule.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,7 +1839,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
18391839
assert(object && format && timetuple);
18401840
assert(PyUnicode_Check(format));
18411841

1842-
PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime");
1842+
PyObject *strftime = PyImport_ImportModuleAttrString("time", "strftime");
18431843
if (strftime == NULL) {
18441844
return NULL;
18451845
}
@@ -2022,7 +2022,7 @@ static PyObject *
20222022
time_time(void)
20232023
{
20242024
PyObject *result = NULL;
2025-
PyObject *time = _PyImport_GetModuleAttrString("time", "time");
2025+
PyObject *time = PyImport_ImportModuleAttrString("time", "time");
20262026

20272027
if (time != NULL) {
20282028
result = PyObject_CallNoArgs(time);
@@ -2040,7 +2040,7 @@ build_struct_time(int y, int m, int d, int hh, int mm, int ss, int dstflag)
20402040
PyObject *struct_time;
20412041
PyObject *result;
20422042

2043-
struct_time = _PyImport_GetModuleAttrString("time", "struct_time");
2043+
struct_time = PyImport_ImportModuleAttrString("time", "struct_time");
20442044
if (struct_time == NULL) {
20452045
return NULL;
20462046
}

Modules/_decimal/_decimal.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3474,7 +3474,7 @@ pydec_format(PyObject *dec, PyObject *context, PyObject *fmt, decimal_state *sta
34743474
PyObject *u;
34753475

34763476
if (state->PyDecimal == NULL) {
3477-
state->PyDecimal = _PyImport_GetModuleAttrString("_pydecimal", "Decimal");
3477+
state->PyDecimal = PyImport_ImportModuleAttrString("_pydecimal", "Decimal");
34783478
if (state->PyDecimal == NULL) {
34793479
return NULL;
34803480
}

Modules/_elementtree.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
#endif
1717

1818
#include "Python.h"
19-
#include "pycore_import.h" // _PyImport_GetModuleAttrString()
2019
#include "pycore_pyhash.h" // _Py_HashSecret
2120

2221
#include <stddef.h> // offsetof()
@@ -4393,7 +4392,7 @@ module_exec(PyObject *m)
43934392
CREATE_TYPE(m, st->Element_Type, &element_spec);
43944393
CREATE_TYPE(m, st->XMLParser_Type, &xmlparser_spec);
43954394

4396-
st->deepcopy_obj = _PyImport_GetModuleAttrString("copy", "deepcopy");
4395+
st->deepcopy_obj = PyImport_ImportModuleAttrString("copy", "deepcopy");
43974396
if (st->deepcopy_obj == NULL) {
43984397
goto error;
43994398
}
@@ -4403,7 +4402,7 @@ module_exec(PyObject *m)
44034402
goto error;
44044403

44054404
/* link against pyexpat */
4406-
if (!(st->expat_capsule = _PyImport_GetModuleAttrString("pyexpat", "expat_CAPI")))
4405+
if (!(st->expat_capsule = PyImport_ImportModuleAttrString("pyexpat", "expat_CAPI")))
44074406
goto error;
44084407
if (!(st->expat_capi = PyCapsule_GetPointer(st->expat_capsule, PyExpat_CAPSULE_NAME)))
44094408
goto error;

Modules/_json.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end)
302302
/* Use JSONDecodeError exception to raise a nice looking ValueError subclass */
303303
_Py_DECLARE_STR(json_decoder, "json.decoder");
304304
PyObject *JSONDecodeError =
305-
_PyImport_GetModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError));
305+
PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError));
306306
if (JSONDecodeError == NULL) {
307307
return;
308308
}

Modules/_lsprof.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ _lsprof_Profiler_enable_impl(ProfilerObject *self, int subcalls,
775775
return NULL;
776776
}
777777

778-
PyObject* monitoring = _PyImport_GetModuleAttrString("sys", "monitoring");
778+
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");
779779
if (!monitoring) {
780780
return NULL;
781781
}
@@ -857,7 +857,7 @@ _lsprof_Profiler_disable_impl(ProfilerObject *self)
857857
}
858858
if (self->flags & POF_ENABLED) {
859859
PyObject* result = NULL;
860-
PyObject* monitoring = _PyImport_GetModuleAttrString("sys", "monitoring");
860+
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");
861861

862862
if (!monitoring) {
863863
return NULL;
@@ -973,7 +973,7 @@ profiler_init_impl(ProfilerObject *self, PyObject *timer, double timeunit,
973973
Py_XSETREF(self->externalTimer, Py_XNewRef(timer));
974974
self->tool_id = PY_MONITORING_PROFILER_ID;
975975

976-
PyObject* monitoring = _PyImport_GetModuleAttrString("sys", "monitoring");
976+
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");
977977
if (!monitoring) {
978978
return -1;
979979
}

Modules/_operator.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1868,7 +1868,7 @@ methodcaller_reduce(methodcallerobject *mc, PyObject *Py_UNUSED(ignored))
18681868
PyObject *constructor;
18691869
PyObject *newargs[2];
18701870

1871-
partial = _PyImport_GetModuleAttrString("functools", "partial");
1871+
partial = PyImport_ImportModuleAttrString("functools", "partial");
18721872
if (!partial)
18731873
return NULL;
18741874

Modules/_pickle.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ _Pickle_InitState(PickleState *st)
362362
}
363363
Py_CLEAR(compat_pickle);
364364

365-
st->codecs_encode = _PyImport_GetModuleAttrString("codecs", "encode");
365+
st->codecs_encode = PyImport_ImportModuleAttrString("codecs", "encode");
366366
if (st->codecs_encode == NULL) {
367367
goto error;
368368
}
@@ -373,7 +373,7 @@ _Pickle_InitState(PickleState *st)
373373
goto error;
374374
}
375375

376-
st->partial = _PyImport_GetModuleAttrString("functools", "partial");
376+
st->partial = PyImport_ImportModuleAttrString("functools", "partial");
377377
if (!st->partial)
378378
goto error;
379379

Modules/_sqlite/connection.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
#include "prepare_protocol.h"
3535
#include "util.h"
3636

37-
#include "pycore_import.h" // _PyImport_GetModuleAttrString()
3837
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
3938
#include "pycore_pyerrors.h" // _PyErr_ChainExceptions1()
4039
#include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing()
@@ -2000,7 +1999,7 @@ pysqlite_connection_iterdump_impl(pysqlite_Connection *self,
20001999
return NULL;
20012000
}
20022001

2003-
PyObject *iterdump = _PyImport_GetModuleAttrString(MODULE_NAME ".dump", "_iterdump");
2002+
PyObject *iterdump = PyImport_ImportModuleAttrString(MODULE_NAME ".dump", "_iterdump");
20042003
if (!iterdump) {
20052004
if (!PyErr_Occurred()) {
20062005
PyErr_SetString(self->OperationalError,

Modules/_sqlite/module.c

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@
3333
#include "row.h"
3434
#include "blob.h"
3535

36-
#include "pycore_import.h" // _PyImport_GetModuleAttrString()
37-
3836
#if SQLITE_VERSION_NUMBER < 3015002
3937
#error "SQLite 3.15.2 or higher required"
4038
#endif
@@ -234,7 +232,7 @@ static int
234232
load_functools_lru_cache(PyObject *module)
235233
{
236234
pysqlite_state *state = pysqlite_get_state(module);
237-
state->lru_cache = _PyImport_GetModuleAttrString("functools", "lru_cache");
235+
state->lru_cache = PyImport_ImportModuleAttrString("functools", "lru_cache");
238236
if (state->lru_cache == NULL) {
239237
return -1;
240238
}

Modules/_sre/sre.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1169,7 +1169,7 @@ compile_template(_sremodulestate *module_state,
11691169
/* delegate to Python code */
11701170
PyObject *func = module_state->compile_template;
11711171
if (func == NULL) {
1172-
func = _PyImport_GetModuleAttrString("re", "_compile_template");
1172+
func = PyImport_ImportModuleAttrString("re", "_compile_template");
11731173
if (func == NULL) {
11741174
return NULL;
11751175
}

0 commit comments

Comments
 (0)