Skip to content

Commit 7c906a1

Browse files
gh-76595: PyCapsule_Import() now imports submodules if needed (GH-6898)
A submodule not imported by its package is now imported if needed. Errors raised during importing the module or looking up an attribute are now propagated instead of being replaced with generic ImportError or AttributeError. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2e519dd commit 7c906a1

4 files changed

Lines changed: 63 additions & 71 deletions

File tree

Doc/c-api/capsule.rst

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,25 +108,20 @@ Refer to :ref:`using-capsules` for more information on using these objects.
108108
109109
Import a pointer to a C object from a capsule attribute in a module. The
110110
*name* parameter should specify the full name to the attribute, as in
111-
``module.attribute``. The *name* stored in the capsule must match this
112-
string exactly.
113-
114-
This function splits *name* on the ``.`` character, and imports the first
115-
element. It then processes further elements using attribute lookups.
111+
``package.module.attribute``.
112+
Modules are imported if needed,
113+
other components are looked up as attributes.
114+
The *name* stored in the capsule must match this string exactly.
116115
117116
Return the capsule's internal *pointer* on success. On failure, set an
118117
exception and return ``NULL``.
119118
120-
.. note::
121-
122-
If *name* points to an attribute of some submodule or subpackage, this
123-
submodule or subpackage must be previously imported using other means
124-
(for example, by using :c:func:`PyImport_ImportModule`) for the
125-
attribute lookups to succeed.
126-
127119
.. versionchanged:: 3.3
128120
*no_block* has no effect anymore.
129121
122+
.. versionchanged:: next
123+
Submodules are now imported if needed.
124+
130125
131126
.. c:function:: int PyCapsule_IsValid(PyObject *capsule, const char *name)
132127

Lib/test/test_capi/test_capsule.py

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -93,49 +93,46 @@ def test_non_ascii_module_name(self):
9393
self.check_import(f'{name}.capsule')
9494

9595
def test_submodule(self):
96-
# Only the first component is imported; a submodule not imported
97-
# by its package is not found.
98-
self.assertRaises(AttributeError,
99-
_testlimitedcapi.PyCapsule_Import, 'capsule_pkg.sub.capsule')
100-
# It is found after explicit import.
101-
importlib.import_module('capsule_pkg.sub')
96+
# A submodule not imported by its package is imported if needed.
97+
self.assertNotIn('capsule_pkg.sub', sys.modules)
98+
self.check_import('capsule_pkg.sub.capsule')
99+
self.assertIn('capsule_pkg.sub', sys.modules)
100+
# It is also found if already imported.
102101
self.check_import('capsule_pkg.sub.capsule')
103102
# A submodule imported by its package is found.
104103
self.check_import('capsule_autopkg.sub.capsule')
105104

106105
def test_invalid_name(self):
107106
pycapsule_import = _testlimitedcapi.PyCapsule_Import
108107
# Non-existing module.
109-
self.assertRaisesRegex(ImportError,
110-
'PyCapsule_Import could not import module "capsule_nonexistent"',
108+
self.assertRaisesRegex(ModuleNotFoundError,
109+
"No module named 'capsule_nonexistent'",
111110
pycapsule_import, 'capsule_nonexistent.capsule')
112111
# Non-UTF-8 module name.
113-
self.assertRaisesRegex(ImportError,
114-
'PyCapsule_Import could not import module',
115-
pycapsule_import, b'\xff\xfe.capsule')
112+
self.assertRaises(UnicodeDecodeError,
113+
pycapsule_import, b'\xff\xfe.capsule')
116114
# Empty module name.
117-
self.assertRaisesRegex(ImportError,
118-
'PyCapsule_Import could not import module ""',
119-
pycapsule_import, '.capsule_mod.capsule')
115+
self.assertRaisesRegex(ValueError, 'Empty module name',
116+
pycapsule_import, '.capsule_mod.capsule')
120117
# Empty name.
121-
self.assertRaisesRegex(ImportError,
122-
'PyCapsule_Import could not import module ""',
123-
pycapsule_import, '')
118+
self.assertRaisesRegex(AttributeError, 'is not valid',
119+
pycapsule_import, '')
124120
# Only a dot.
125-
self.assertRaisesRegex(ImportError,
126-
'PyCapsule_Import could not import module ""',
127-
pycapsule_import, '.')
121+
self.assertRaisesRegex(ValueError, 'Empty module name',
122+
pycapsule_import, '.')
128123
# Non-existing attribute.
129-
self.assertRaises(AttributeError,
130-
pycapsule_import, 'capsule_mod.nonexistent')
124+
self.assertRaisesRegex(AttributeError, 'is not valid',
125+
pycapsule_import, 'capsule_mod.nonexistent')
131126
# Empty attribute name.
132-
self.assertRaises(AttributeError, pycapsule_import, 'capsule_mod.')
127+
self.assertRaisesRegex(AttributeError, 'is not valid',
128+
pycapsule_import, 'capsule_mod.')
133129
# Consecutive dots.
134-
self.assertRaises(AttributeError,
135-
pycapsule_import, 'capsule_mod..capsule')
130+
self.assertRaisesRegex(ModuleNotFoundError,
131+
"No module named 'capsule_mod.'",
132+
pycapsule_import, 'capsule_mod..capsule')
136133
# Attribute of an object which is not a module.
137-
self.assertRaises(AttributeError,
138-
pycapsule_import, 'capsule_mod.not_capsule.capsule')
134+
self.assertRaisesRegex(AttributeError, 'is not valid',
135+
pycapsule_import, 'capsule_mod.not_capsule.capsule')
139136
# No attribute name.
140137
self.assertRaisesRegex(AttributeError, 'is not valid',
141138
pycapsule_import, 'capsule_mod')
@@ -162,13 +159,9 @@ def test_invalid_capsule(self):
162159
pycapsule_import, 'capsule_mod.nullname')
163160

164161
def test_error_from_import(self):
165-
# The exception raised during importing the module is replaced
166-
# with generic ImportError.
167-
with self.assertRaises(ImportError) as cm:
168-
_testlimitedcapi.PyCapsule_Import('capsule_broken.capsule')
169-
self.assertEqual(str(cm.exception),
170-
'PyCapsule_Import could not import '
171-
'module "capsule_broken"')
162+
# The exception raised during importing the module is propagated.
163+
self.assertRaises(ZeroDivisionError,
164+
_testlimitedcapi.PyCapsule_Import, 'capsule_broken.capsule')
172165

173166
def test_error_from_attribute_lookup(self):
174167
self.assertRaises(FloatingPointError,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:c:func:`PyCapsule_Import` now imports submodules if needed. Previously
2+
names like ``package.module.attribute`` worked only if ``package.module``
3+
was already imported.

Objects/capsule.c

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "pycore_capsule.h" // export _PyCapsule_SetTraverse()
55
#include "pycore_gc.h" // _PyObject_GC_IS_TRACKED()
66
#include "pycore_object.h" // _PyObject_GC_TRACK()
7+
#include "pycore_pymem.h" // _PyMem_Strdup()
78

89

910
/* Internal structure of PyCapsule */
@@ -227,58 +228,58 @@ _PyCapsule_SetTraverse(PyObject *op, traverseproc traverse_func, inquiry clear_f
227228

228229

229230
void *
230-
PyCapsule_Import(const char *name, int no_block)
231+
PyCapsule_Import(const char *name, int Py_UNUSED(no_block))
231232
{
232233
PyObject *object = NULL;
233234
void *return_value = NULL;
234-
char *trace;
235-
size_t name_length = (strlen(name) + 1) * sizeof(char);
236-
char *name_dup = (char *)PyMem_Malloc(name_length);
235+
char *name_dup = _PyMem_Strdup(name);
237236

238237
if (!name_dup) {
239238
return PyErr_NoMemory();
240239
}
241240

242-
memcpy(name_dup, name, name_length);
243-
244-
trace = name_dup;
245-
while (trace) {
241+
char *trace = name_dup;
242+
while (1) {
246243
char *dot = strchr(trace, '.');
247244
if (dot) {
248-
*dot++ = '\0';
245+
*dot = '\0';
249246
}
250-
251-
if (object == NULL) {
252-
object = PyImport_ImportModule(trace);
253-
if (!object) {
254-
PyErr_Format(PyExc_ImportError, "PyCapsule_Import could not import module \"%s\"", trace);
247+
if (object) {
248+
PyObject *attr;
249+
if (PyObject_GetOptionalAttrString(object, trace, &attr) < 0) {
250+
Py_CLEAR(object);
251+
break;
255252
}
256-
} else {
257-
PyObject *object2 = PyObject_GetAttrString(object, trace);
258-
Py_SETREF(object, object2);
253+
Py_SETREF(object, attr);
259254
}
260-
if (!object) {
261-
goto EXIT;
255+
if (!dot) {
256+
// We are done
257+
break;
262258
}
263259

264-
trace = dot;
260+
if (!object) {
261+
object = PyImport_ImportModule(name_dup);
262+
if (!object) {
263+
break;
264+
}
265+
}
266+
*dot = '.';
267+
trace = dot + 1;
265268
}
266269

267270
/* compare attribute name to module.name by hand */
268271
if (PyCapsule_IsValid(object, name)) {
269272
PyCapsule *capsule = (PyCapsule *)object;
270273
return_value = capsule->pointer;
271-
} else {
274+
}
275+
else if (!PyErr_Occurred()) {
272276
PyErr_Format(PyExc_AttributeError,
273277
"PyCapsule_Import \"%s\" is not valid",
274278
name);
275279
}
276280

277-
EXIT:
278281
Py_XDECREF(object);
279-
if (name_dup) {
280-
PyMem_Free(name_dup);
281-
}
282+
PyMem_Free(name_dup);
282283
return return_value;
283284
}
284285

0 commit comments

Comments
 (0)