Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-128911: Add PyImport_GetModuleAttrString() function #128912

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
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_GetModuleAttr(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_GetModuleAttrString(const char *mod_name, const char *attr_name)

Similar to :c:func:`PyImport_GetModuleAttr`, but names are UTF-8 encoded
strings instead of Python :class:`str` objects.

.. versionadded:: 3.14
4 changes: 4 additions & 0 deletions Doc/data/refcounts.dat
Original file line number Diff line number Diff line change
Expand Up @@ -3052,3 +3052,7 @@ _Py_c_quot:Py_complex:divisor::
_Py_c_sum:Py_complex:::
_Py_c_sum:Py_complex:left::
_Py_c_sum:Py_complex:right::

PyImport_GetModuleAttrString:PyObject*::+1:
PyImport_GetModuleAttrString:const char *:mod_name::
PyImport_GetModuleAttrString: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 @@ -1301,6 +1301,11 @@ New features
bit-packing Python version numbers.
(Contributed by Petr Viktorin in :gh:`128629`.)

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


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

PyAPI_DATA(const struct _frozen *) PyImport_FrozenModules;

PyAPI_FUNC(PyObject*) PyImport_GetModuleAttr(
PyObject *mod_name,
PyObject *attr_name);
PyAPI_FUNC(PyObject*) PyImport_GetModuleAttrString(
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
27 changes: 27 additions & 0 deletions 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 @@ -317,6 +318,32 @@ def test_executecodemoduleobject(self):
# CRASHES execute_code_func(NULL, code, NULL, NULL)
# CRASHES execute_code_func(name, NULL, NULL, NULL)

def check_getmoduleattr(self, getmoduleattr):
self.assertIs(getmoduleattr('sys', 'argv'), sys.argv)
self.assertIs(getmoduleattr('types', 'ModuleType'), types.ModuleType)

def test_getmoduleattr(self):
# Test PyImport_GetModuleAttr()
getmoduleattr = _testcapi.PyImport_GetModuleAttr
self.check_getmoduleattr(getmoduleattr)

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

def test_getmoduleattrstring(self):
# Test PyImport_GetModuleAttrString()
getmoduleattr = _testcapi.PyImport_GetModuleAttrString
self.check_getmoduleattr(getmoduleattr)

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

# CRASHES getmoduleattr(NULL, "argv")
# CRASHES getmoduleattr("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_GetModuleAttr` and :c:func:`PyImport_GetModuleAttrString`
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_GetModuleAttrString("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_GetModuleAttrString("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_GetModuleAttrString("_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_GetModuleAttrString("time", "strftime");
if (strftime == NULL) {
return NULL;
}
Expand Down Expand Up @@ -2021,7 +2021,7 @@ static PyObject *
time_time(void)
{
PyObject *result = NULL;
PyObject *time = _PyImport_GetModuleAttrString("time", "time");
PyObject *time = PyImport_GetModuleAttrString("time", "time");

if (time != NULL) {
result = PyObject_CallNoArgs(time);
Expand All @@ -2039,7 +2039,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_GetModuleAttrString("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 @@ -3464,7 +3464,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_GetModuleAttrString("_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_GetModuleAttrString("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_GetModuleAttrString("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_GetModuleAttr(&_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_GetModuleAttrString("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_GetModuleAttrString("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_GetModuleAttrString("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_GetModuleAttrString("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_GetModuleAttrString("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_GetModuleAttrString("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 @@ -1995,7 +1994,7 @@ pysqlite_connection_iterdump_impl(pysqlite_Connection *self,
return NULL;
}

PyObject *iterdump = _PyImport_GetModuleAttrString(MODULE_NAME ".dump", "_iterdump");
PyObject *iterdump = PyImport_GetModuleAttrString(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_GetModuleAttrString("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_GetModuleAttrString("re", "_compile_template");
if (func == NULL) {
return NULL;
}
Expand Down
44 changes: 44 additions & 0 deletions Modules/_testcapi/import.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include "parts.h"
#include "util.h"

// Test PyImport_GetModuleAttr()
static PyObject *
pyimport_getmoduleattr(PyObject *self, PyObject *args)
{
PyObject *mod_name, *attr_name;
if (!PyArg_ParseTuple(args, "OO", &mod_name, &attr_name)) {
return NULL;
}
NULLABLE(mod_name);
NULLABLE(attr_name);

return PyImport_GetModuleAttr(mod_name, attr_name);
}


// Test PyImport_GetModuleAttrString()
static PyObject *
pyimport_getmoduleattrstring(PyObject *self, PyObject *args)
{
char *mod_name, *attr_name;
Py_ssize_t len;
if (!PyArg_ParseTuple(args, "z#z#", &mod_name, &len, &attr_name, &len)) {
return NULL;
}

return PyImport_GetModuleAttrString(mod_name, attr_name);
}


static PyMethodDef test_methods[] = {
{"PyImport_GetModuleAttr", pyimport_getmoduleattr, METH_VARARGS},
{"PyImport_GetModuleAttrString", pyimport_getmoduleattrstring, METH_VARARGS},
{NULL},
};

int
_PyTestCapi_Init_Import(PyObject *m)
{
return PyModule_AddFunctions(m, test_methods);
}

1 change: 1 addition & 0 deletions Modules/_testcapi/parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ int _PyTestCapi_Init_Time(PyObject *module);
int _PyTestCapi_Init_Monitoring(PyObject *module);
int _PyTestCapi_Init_Object(PyObject *module);
int _PyTestCapi_Init_Config(PyObject *mod);
int _PyTestCapi_Init_Import(PyObject *mod);

#endif // Py_TESTCAPI_PARTS_H
3 changes: 3 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4400,6 +4400,9 @@ PyInit__testcapi(void)
if (_PyTestCapi_Init_Config(m) < 0) {
return NULL;
}
if (_PyTestCapi_Init_Import(m) < 0) {
return NULL;
}

PyState_AddModule(m, &_testcapimodule);
return m;
Expand Down
Loading
Loading