Skip to content

Commit

Permalink
gh-128911: Add PyImport_ImportModuleAttr() function (#128912)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
vstinner authored Jan 30, 2025
1 parent f927204 commit 3bebe46
Show file tree
Hide file tree
Showing 40 changed files with 194 additions and 56 deletions.
21 changes: 21 additions & 0 deletions Doc/c-api/import.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,24 @@ Importing Modules
If Python is initialized multiple times, :c:func:`PyImport_AppendInittab` or
:c:func:`PyImport_ExtendInittab` must be called before each Python
initialization.
.. c:function:: PyObject* PyImport_ImportModuleAttr(PyObject *mod_name, PyObject *attr_name)
Import the module *mod_name* and get its attribute *attr_name*.
Names must be Python :class:`str` objects.
Helper function combining :c:func:`PyImport_Import` and
:c:func:`PyObject_GetAttr`. For example, it can raise :exc:`ImportError` if
the module is not found, and :exc:`AttributeError` if the attribute doesn't
exist.
.. versionadded:: 3.14
.. c:function:: PyObject* PyImport_ImportModuleAttrString(const char *mod_name, const char *attr_name)
Similar to :c:func:`PyImport_ImportModuleAttr`, but names are UTF-8 encoded
strings instead of Python :class:`str` objects.
.. versionadded:: 3.14
8 changes: 8 additions & 0 deletions Doc/data/refcounts.dat
Original file line number Diff line number Diff line change
Expand Up @@ -3052,3 +3052,11 @@ _Py_c_quot:Py_complex:divisor::
_Py_c_sum:Py_complex:::
_Py_c_sum:Py_complex:left::
_Py_c_sum:Py_complex:right::

PyImport_ImportModuleAttr:PyObject*::+1:
PyImport_ImportModuleAttr:PyObject*:mod_name:0:
PyImport_ImportModuleAttr:PyObject*:attr_name:0:

PyImport_ImportModuleAttrString:PyObject*::+1:
PyImport_ImportModuleAttrString:const char *:mod_name::
PyImport_ImportModuleAttrString:const char *:attr_name::
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,11 @@ New features
* Add :c:func:`PyUnstable_IsImmortal` for determining whether an object is :term:`immortal`,
for debugging purposes.

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


Limited C API changes
---------------------
Expand Down
7 changes: 7 additions & 0 deletions Include/cpython/import.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ struct _frozen {
collection of frozen modules: */

PyAPI_DATA(const struct _frozen *) PyImport_FrozenModules;

PyAPI_FUNC(PyObject*) PyImport_ImportModuleAttr(
PyObject *mod_name,
PyObject *attr_name);
PyAPI_FUNC(PyObject*) PyImport_ImportModuleAttrString(
const char *mod_name,
const char *attr_name);
6 changes: 0 additions & 6 deletions Include/internal/pycore_import.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ extern int _PyImport_FixupBuiltin(
PyObject *modules
);

// Export for many shared extensions, like '_json'
PyAPI_FUNC(PyObject*) _PyImport_GetModuleAttr(PyObject *, PyObject *);

// Export for many shared extensions, like '_datetime'
PyAPI_FUNC(PyObject*) _PyImport_GetModuleAttrString(const char *, const char *);


struct _import_runtime_state {
/* The builtin modules (defined in config.c). */
Expand Down
56 changes: 55 additions & 1 deletion Lib/test/test_capi/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from test.support import import_helper
from test.support.warnings_helper import check_warnings

_testcapi = import_helper.import_module('_testcapi')
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
NULL = None

Expand Down Expand Up @@ -148,7 +149,7 @@ def check_frozen_import(self, import_frozen_module):
try:
self.assertEqual(import_frozen_module('zipimport'), 1)

# import zipimport again
# import zipimport again
self.assertEqual(import_frozen_module('zipimport'), 1)
finally:
sys.modules['zipimport'] = old_zipimport
Expand Down Expand Up @@ -317,6 +318,59 @@ def test_executecodemoduleobject(self):
# CRASHES execute_code_func(NULL, code, NULL, NULL)
# CRASHES execute_code_func(name, NULL, NULL, NULL)

def check_importmoduleattr(self, importmoduleattr):
self.assertIs(importmoduleattr('sys', 'argv'), sys.argv)
self.assertIs(importmoduleattr('types', 'ModuleType'), types.ModuleType)

# module name containing a dot
attr = importmoduleattr('email.message', 'Message')
from email.message import Message
self.assertIs(attr, Message)

with self.assertRaises(ImportError):
# nonexistent module
importmoduleattr('nonexistentmodule', 'attr')
with self.assertRaises(AttributeError):
# nonexistent attribute
importmoduleattr('sys', 'nonexistentattr')
with self.assertRaises(AttributeError):
# attribute name containing a dot
importmoduleattr('sys', 'implementation.name')

def test_importmoduleattr(self):
# Test PyImport_ImportModuleAttr()
importmoduleattr = _testcapi.PyImport_ImportModuleAttr
self.check_importmoduleattr(importmoduleattr)

# Invalid module name type
for mod_name in (object(), 123, b'bytes'):
with self.subTest(mod_name=mod_name):
with self.assertRaises(TypeError):
importmoduleattr(mod_name, "attr")

# Invalid attribute name type
for attr_name in (object(), 123, b'bytes'):
with self.subTest(attr_name=attr_name):
with self.assertRaises(TypeError):
importmoduleattr("sys", attr_name)

with self.assertRaises(SystemError):
importmoduleattr(NULL, "argv")
# CRASHES importmoduleattr("sys", NULL)

def test_importmoduleattrstring(self):
# Test PyImport_ImportModuleAttrString()
importmoduleattr = _testcapi.PyImport_ImportModuleAttrString
self.check_importmoduleattr(importmoduleattr)

with self.assertRaises(UnicodeDecodeError):
importmoduleattr(b"sys\xff", "argv")
with self.assertRaises(UnicodeDecodeError):
importmoduleattr("sys", b"argv\xff")

# CRASHES importmoduleattr(NULL, "argv")
# CRASHES importmoduleattr("sys", NULL)

# TODO: test PyImport_GetImporter()
# TODO: test PyImport_ReloadModule()
# TODO: test PyImport_ExtendInittab()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :c:func:`PyImport_ImportModuleAttr` and :c:func:`PyImport_ImportModuleAttrString`
helper functions to import a module and get an attribute of the module. Patch
by Victor Stinner.
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
@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
@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
@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
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
Expand Down
2 changes: 1 addition & 1 deletion Modules/_ctypes/callbacks.c
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ long Call_GetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv)
if (context == NULL)
context = PyUnicode_InternFromString("_ctypes.DllGetClassObject");

func = _PyImport_GetModuleAttrString("ctypes", "DllGetClassObject");
func = PyImport_ImportModuleAttrString("ctypes", "DllGetClassObject");
if (!func) {
PyErr_WriteUnraisable(context ? context : Py_None);
/* There has been a warning before about this already */
Expand Down
2 changes: 1 addition & 1 deletion Modules/_ctypes/stgdict.c
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct
goto error;
}

PyObject *layout_func = _PyImport_GetModuleAttrString("ctypes._layout",
PyObject *layout_func = PyImport_ImportModuleAttrString("ctypes._layout",
"get_layout");
if (!layout_func) {
goto error;
Expand Down
2 changes: 1 addition & 1 deletion Modules/_cursesmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ _PyCursesCheckFunction(int called, const char *funcname)
if (called == TRUE) {
return 1;
}
PyObject *exc = _PyImport_GetModuleAttrString("_curses", "error");
PyObject *exc = PyImport_ImportModuleAttrString("_curses", "error");
if (exc != NULL) {
PyErr_Format(exc, "must call %s() first", funcname);
Py_DECREF(exc);
Expand Down
6 changes: 3 additions & 3 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1839,7 +1839,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
assert(object && format && timetuple);
assert(PyUnicode_Check(format));

PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime");
PyObject *strftime = PyImport_ImportModuleAttrString("time", "strftime");
if (strftime == NULL) {
return NULL;
}
Expand Down Expand Up @@ -2022,7 +2022,7 @@ static PyObject *
time_time(void)
{
PyObject *result = NULL;
PyObject *time = _PyImport_GetModuleAttrString("time", "time");
PyObject *time = PyImport_ImportModuleAttrString("time", "time");

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

struct_time = _PyImport_GetModuleAttrString("time", "struct_time");
struct_time = PyImport_ImportModuleAttrString("time", "struct_time");
if (struct_time == NULL) {
return NULL;
}
Expand Down
2 changes: 1 addition & 1 deletion Modules/_decimal/_decimal.c
Original file line number Diff line number Diff line change
Expand Up @@ -3474,7 +3474,7 @@ pydec_format(PyObject *dec, PyObject *context, PyObject *fmt, decimal_state *sta
PyObject *u;

if (state->PyDecimal == NULL) {
state->PyDecimal = _PyImport_GetModuleAttrString("_pydecimal", "Decimal");
state->PyDecimal = PyImport_ImportModuleAttrString("_pydecimal", "Decimal");
if (state->PyDecimal == NULL) {
return NULL;
}
Expand Down
5 changes: 2 additions & 3 deletions Modules/_elementtree.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
#endif

#include "Python.h"
#include "pycore_import.h" // _PyImport_GetModuleAttrString()
#include "pycore_pyhash.h" // _Py_HashSecret

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

st->deepcopy_obj = _PyImport_GetModuleAttrString("copy", "deepcopy");
st->deepcopy_obj = PyImport_ImportModuleAttrString("copy", "deepcopy");
if (st->deepcopy_obj == NULL) {
goto error;
}
Expand All @@ -4403,7 +4402,7 @@ module_exec(PyObject *m)
goto error;

/* link against pyexpat */
if (!(st->expat_capsule = _PyImport_GetModuleAttrString("pyexpat", "expat_CAPI")))
if (!(st->expat_capsule = PyImport_ImportModuleAttrString("pyexpat", "expat_CAPI")))
goto error;
if (!(st->expat_capi = PyCapsule_GetPointer(st->expat_capsule, PyExpat_CAPSULE_NAME)))
goto error;
Expand Down
2 changes: 1 addition & 1 deletion Modules/_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end)
/* Use JSONDecodeError exception to raise a nice looking ValueError subclass */
_Py_DECLARE_STR(json_decoder, "json.decoder");
PyObject *JSONDecodeError =
_PyImport_GetModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError));
PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError));
if (JSONDecodeError == NULL) {
return;
}
Expand Down
6 changes: 3 additions & 3 deletions Modules/_lsprof.c
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ _lsprof_Profiler_enable_impl(ProfilerObject *self, int subcalls,
return NULL;
}

PyObject* monitoring = _PyImport_GetModuleAttrString("sys", "monitoring");
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");
if (!monitoring) {
return NULL;
}
Expand Down Expand Up @@ -857,7 +857,7 @@ _lsprof_Profiler_disable_impl(ProfilerObject *self)
}
if (self->flags & POF_ENABLED) {
PyObject* result = NULL;
PyObject* monitoring = _PyImport_GetModuleAttrString("sys", "monitoring");
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");

if (!monitoring) {
return NULL;
Expand Down Expand Up @@ -973,7 +973,7 @@ profiler_init_impl(ProfilerObject *self, PyObject *timer, double timeunit,
Py_XSETREF(self->externalTimer, Py_XNewRef(timer));
self->tool_id = PY_MONITORING_PROFILER_ID;

PyObject* monitoring = _PyImport_GetModuleAttrString("sys", "monitoring");
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");
if (!monitoring) {
return -1;
}
Expand Down
2 changes: 1 addition & 1 deletion Modules/_operator.c
Original file line number Diff line number Diff line change
Expand Up @@ -1868,7 +1868,7 @@ methodcaller_reduce(methodcallerobject *mc, PyObject *Py_UNUSED(ignored))
PyObject *constructor;
PyObject *newargs[2];

partial = _PyImport_GetModuleAttrString("functools", "partial");
partial = PyImport_ImportModuleAttrString("functools", "partial");
if (!partial)
return NULL;

Expand Down
4 changes: 2 additions & 2 deletions Modules/_pickle.c
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ _Pickle_InitState(PickleState *st)
}
Py_CLEAR(compat_pickle);

st->codecs_encode = _PyImport_GetModuleAttrString("codecs", "encode");
st->codecs_encode = PyImport_ImportModuleAttrString("codecs", "encode");
if (st->codecs_encode == NULL) {
goto error;
}
Expand All @@ -373,7 +373,7 @@ _Pickle_InitState(PickleState *st)
goto error;
}

st->partial = _PyImport_GetModuleAttrString("functools", "partial");
st->partial = PyImport_ImportModuleAttrString("functools", "partial");
if (!st->partial)
goto error;

Expand Down
3 changes: 1 addition & 2 deletions Modules/_sqlite/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
#include "prepare_protocol.h"
#include "util.h"

#include "pycore_import.h" // _PyImport_GetModuleAttrString()
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
#include "pycore_pyerrors.h" // _PyErr_ChainExceptions1()
#include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing()
Expand Down Expand Up @@ -2000,7 +1999,7 @@ pysqlite_connection_iterdump_impl(pysqlite_Connection *self,
return NULL;
}

PyObject *iterdump = _PyImport_GetModuleAttrString(MODULE_NAME ".dump", "_iterdump");
PyObject *iterdump = PyImport_ImportModuleAttrString(MODULE_NAME ".dump", "_iterdump");
if (!iterdump) {
if (!PyErr_Occurred()) {
PyErr_SetString(self->OperationalError,
Expand Down
4 changes: 1 addition & 3 deletions Modules/_sqlite/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@
#include "row.h"
#include "blob.h"

#include "pycore_import.h" // _PyImport_GetModuleAttrString()

#if SQLITE_VERSION_NUMBER < 3015002
#error "SQLite 3.15.2 or higher required"
#endif
Expand Down Expand Up @@ -234,7 +232,7 @@ static int
load_functools_lru_cache(PyObject *module)
{
pysqlite_state *state = pysqlite_get_state(module);
state->lru_cache = _PyImport_GetModuleAttrString("functools", "lru_cache");
state->lru_cache = PyImport_ImportModuleAttrString("functools", "lru_cache");
if (state->lru_cache == NULL) {
return -1;
}
Expand Down
2 changes: 1 addition & 1 deletion Modules/_sre/sre.c
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,7 @@ compile_template(_sremodulestate *module_state,
/* delegate to Python code */
PyObject *func = module_state->compile_template;
if (func == NULL) {
func = _PyImport_GetModuleAttrString("re", "_compile_template");
func = PyImport_ImportModuleAttrString("re", "_compile_template");
if (func == NULL) {
return NULL;
}
Expand Down
Loading

0 comments on commit 3bebe46

Please sign in to comment.