Skip to content

Commit 09270e1

Browse files
gh-76595: Add tests for PyCapsule_Import()
Add _testcapi helpers to create a capsule with an arbitrary name and to call PyCapsule_Import(), and Python tests covering the current behavior: only the first component of the dotted name is imported, the rest are attribute lookups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent eef2349 commit 09270e1

8 files changed

Lines changed: 242 additions & 1 deletion

File tree

Lib/test/test_capi/test_capsule.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import importlib
2+
import os
3+
import sys
4+
import textwrap
5+
import unittest
6+
from test.support import import_helper, os_helper
7+
8+
_testcapi = import_helper.import_module('_testcapi')
9+
10+
11+
class CapsuleImportTests(unittest.TestCase):
12+
"""Tests for PyCapsule_Import()."""
13+
14+
@classmethod
15+
def setUpClass(cls):
16+
tmp = cls.tmp = cls.enterClassContext(os_helper.temp_dir())
17+
cls.enterClassContext(import_helper.DirsOnSysPath(tmp))
18+
cls.write_file(os.path.join(tmp, 'capsule_mod.py'), '''
19+
import _testcapi
20+
21+
capsule = _testcapi.capsule_new('capsule_mod.capsule')
22+
капсула = _testcapi.capsule_new('capsule_mod.капсула')
23+
mismatched = _testcapi.capsule_new('other.name')
24+
nonutf8 = _testcapi.capsule_new(b'capsule_mod.nonutf8\\xff')
25+
nullname = _testcapi.capsule_new(None)
26+
not_capsule = 42
27+
28+
class ns:
29+
nested = _testcapi.capsule_new('capsule_mod.ns.nested')
30+
31+
def __getattr__(name):
32+
if name == 'bad_attr':
33+
raise FloatingPointError('bad attribute')
34+
raise AttributeError(name)
35+
''')
36+
pkg = os.path.join(tmp, 'capsule_pkg')
37+
os.mkdir(pkg)
38+
cls.write_file(os.path.join(pkg, '__init__.py'), '')
39+
cls.write_file(os.path.join(pkg, 'sub.py'), '''
40+
import _testcapi
41+
42+
capsule = _testcapi.capsule_new('capsule_pkg.sub.capsule')
43+
''')
44+
autopkg = os.path.join(tmp, 'capsule_autopkg')
45+
os.mkdir(autopkg)
46+
cls.write_file(os.path.join(autopkg, '__init__.py'), 'from . import sub\n')
47+
cls.write_file(os.path.join(autopkg, 'sub.py'), '''
48+
import _testcapi
49+
50+
capsule = _testcapi.capsule_new('capsule_autopkg.sub.capsule')
51+
''')
52+
cls.write_file(os.path.join(tmp, 'capsule_broken.py'), '1/0\n')
53+
importlib.invalidate_caches()
54+
55+
def setUp(self):
56+
for name in ('capsule_mod', 'capsule_pkg.sub', 'capsule_pkg',
57+
'capsule_autopkg.sub', 'capsule_autopkg',
58+
'capsule_broken'):
59+
self.addCleanup(import_helper.unload, name)
60+
61+
@staticmethod
62+
def write_file(path, source):
63+
with open(path, 'w', encoding='utf-8') as f:
64+
f.write(textwrap.dedent(source))
65+
66+
def check_import(self, name, *args):
67+
# _testcapi.PyCapsule_Import() returns the name stored as the
68+
# pointer by _testcapi.capsule_new().
69+
self.assertEqual(_testcapi.PyCapsule_Import(name, *args), name)
70+
71+
def test_import(self):
72+
# The module is imported if not already imported.
73+
self.assertNotIn('capsule_mod', sys.modules)
74+
self.check_import('capsule_mod.capsule')
75+
# Attributes after the first component are plain attribute lookups.
76+
self.check_import('capsule_mod.ns.nested')
77+
# Non-ASCII capsule and attribute name.
78+
self.check_import('capsule_mod.капсула')
79+
# The no_block argument is ignored.
80+
self.check_import('capsule_mod.capsule', 1)
81+
82+
@unittest.skipUnless(os_helper.TESTFN_NONASCII,
83+
'requires non-ASCII file name support')
84+
def test_non_ascii_module_name(self):
85+
name = os_helper.TESTFN_NONASCII
86+
self.write_file(os.path.join(self.tmp, name + '.py'), f'''
87+
import _testcapi
88+
89+
capsule = _testcapi.capsule_new('{name}.capsule')
90+
''')
91+
importlib.invalidate_caches()
92+
self.addCleanup(import_helper.unload, name)
93+
self.check_import(f'{name}.capsule')
94+
95+
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+
_testcapi.PyCapsule_Import, 'capsule_pkg.sub.capsule')
100+
# It is found after explicit import.
101+
importlib.import_module('capsule_pkg.sub')
102+
self.check_import('capsule_pkg.sub.capsule')
103+
# A submodule imported by its package is found.
104+
self.check_import('capsule_autopkg.sub.capsule')
105+
106+
def test_invalid_name(self):
107+
pycapsule_import = _testcapi.PyCapsule_Import
108+
# Non-existing module.
109+
self.assertRaises(ImportError,
110+
pycapsule_import, 'capsule_nonexistent.capsule')
111+
# Non-UTF-8 module name.
112+
self.assertRaises(ImportError, pycapsule_import, b'\xff\xfe.capsule')
113+
# Empty module name.
114+
self.assertRaises(ImportError, pycapsule_import, '.capsule_mod.capsule')
115+
# Empty name.
116+
self.assertRaises(ImportError, pycapsule_import, '')
117+
# Only a dot.
118+
self.assertRaises(ImportError, pycapsule_import, '.')
119+
# Non-existing attribute.
120+
self.assertRaises(AttributeError,
121+
pycapsule_import, 'capsule_mod.nonexistent')
122+
# Empty attribute name.
123+
self.assertRaises(AttributeError, pycapsule_import, 'capsule_mod.')
124+
# Consecutive dots.
125+
self.assertRaises(AttributeError,
126+
pycapsule_import, 'capsule_mod..capsule')
127+
# Attribute of an object which is not a module.
128+
self.assertRaises(AttributeError,
129+
pycapsule_import, 'capsule_mod.not_capsule.capsule')
130+
# No attribute name.
131+
self.assertRaisesRegex(AttributeError, 'is not valid',
132+
pycapsule_import, 'capsule_mod')
133+
134+
# CRASHES pycapsule_import(NULL)
135+
136+
def test_invalid_capsule(self):
137+
pycapsule_import = _testcapi.PyCapsule_Import
138+
# The attribute is not a capsule.
139+
self.assertRaisesRegex(AttributeError, 'is not valid',
140+
pycapsule_import, 'capsule_mod.not_capsule')
141+
# The capsule name does not match the requested name.
142+
self.assertRaisesRegex(AttributeError, 'is not valid',
143+
pycapsule_import, 'capsule_mod.mismatched')
144+
# The capsule name contains a byte not decodable from UTF-8.
145+
self.assertRaisesRegex(AttributeError, 'is not valid',
146+
pycapsule_import, 'capsule_mod.nonutf8')
147+
# Even the exactly matching name fails: the attribute lookup
148+
# requires a name decodable from UTF-8.
149+
self.assertRaises(UnicodeDecodeError,
150+
pycapsule_import, b'capsule_mod.nonutf8\xff')
151+
# The capsule name is NULL.
152+
self.assertRaisesRegex(AttributeError, 'is not valid',
153+
pycapsule_import, 'capsule_mod.nullname')
154+
155+
def test_error_from_import(self):
156+
# The exception raised during importing the module is replaced
157+
# with generic ImportError.
158+
self.assertRaises(ImportError,
159+
_testcapi.PyCapsule_Import, 'capsule_broken.capsule')
160+
161+
def test_error_from_attribute_lookup(self):
162+
self.assertRaises(FloatingPointError,
163+
_testcapi.PyCapsule_Import, 'capsule_mod.bad_attr')
164+
self.assertRaises(FloatingPointError,
165+
_testcapi.PyCapsule_Import, 'capsule_mod.bad_attr.capsule')
166+
167+
168+
if __name__ == "__main__":
169+
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add tests for :c:func:`PyCapsule_Import`.

Modules/Setup.stdlib.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@
173173
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
174174
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
175175
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c
176-
@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/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c
176+
@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/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/capsule.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c
177177
@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/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c
178178
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
179179
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

Modules/_testcapi/capsule.c

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#include "parts.h"
2+
3+
static void
4+
capsule_destructor(PyObject *op)
5+
{
6+
/* If non-NULL, the name and the pointer are the same allocation. */
7+
free((char *)PyCapsule_GetName(op));
8+
}
9+
10+
static PyObject *
11+
capsule_new(PyObject *self, PyObject *arg)
12+
{
13+
const char *name;
14+
Py_ssize_t size;
15+
if (!PyArg_Parse(arg, "z#", &name, &size)) {
16+
return NULL;
17+
}
18+
char *name_copy = NULL;
19+
if (name != NULL) {
20+
name_copy = strdup(name);
21+
if (name_copy == NULL) {
22+
return PyErr_NoMemory();
23+
}
24+
}
25+
static char dummy;
26+
void *pointer = name_copy != NULL ? (void *)name_copy : (void *)&dummy;
27+
PyObject *capsule = PyCapsule_New(pointer, name_copy, capsule_destructor);
28+
if (capsule == NULL) {
29+
free(name_copy);
30+
}
31+
return capsule;
32+
}
33+
34+
static PyObject *
35+
pycapsule_import(PyObject *self, PyObject *args)
36+
{
37+
const char *name;
38+
Py_ssize_t size;
39+
int no_block = 0;
40+
if (!PyArg_ParseTuple(args, "z#|i", &name, &size, &no_block)) {
41+
return NULL;
42+
}
43+
void *pointer = PyCapsule_Import(name, no_block);
44+
if (pointer == NULL) {
45+
return NULL;
46+
}
47+
/* Capsules created by capsule_new() store a copy of their name as the
48+
pointer, so a successful import round-trips the name. Only use this
49+
function with such capsules. */
50+
return PyUnicode_FromString((const char *)pointer);
51+
}
52+
53+
static PyMethodDef test_methods[] = {
54+
{"capsule_new", capsule_new, METH_O},
55+
{"PyCapsule_Import", pycapsule_import, METH_VARARGS},
56+
{NULL},
57+
};
58+
59+
int
60+
_PyTestCapi_Init_Capsule(PyObject *m)
61+
{
62+
return PyModule_AddFunctions(m, test_methods);
63+
}

Modules/_testcapi/parts.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ int _PyTestCapi_Init_Monitoring(PyObject *module);
6363
int _PyTestCapi_Init_Object(PyObject *module);
6464
int _PyTestCapi_Init_Config(PyObject *mod);
6565
int _PyTestCapi_Init_Import(PyObject *mod);
66+
int _PyTestCapi_Init_Capsule(PyObject *mod);
6667
int _PyTestCapi_Init_Frame(PyObject *mod);
6768
int _PyTestCapi_Init_Type(PyObject *mod);
6869
int _PyTestCapi_Init_Function(PyObject *mod);

Modules/_testcapimodule.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3928,6 +3928,9 @@ _testcapi_exec(PyObject *m)
39283928
if (_PyTestCapi_Init_Import(m) < 0) {
39293929
return -1;
39303930
}
3931+
if (_PyTestCapi_Init_Capsule(m) < 0) {
3932+
return -1;
3933+
}
39313934
if (_PyTestCapi_Init_Frame(m) < 0) {
39323935
return -1;
39333936
}

PCbuild/_testcapi.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
<ClCompile Include="..\Modules\_testcapi\monitoring.c" />
131131
<ClCompile Include="..\Modules\_testcapi\config.c" />
132132
<ClCompile Include="..\Modules\_testcapi\import.c" />
133+
<ClCompile Include="..\Modules\_testcapi\capsule.c" />
133134
<ClCompile Include="..\Modules\_testcapi\frame.c" />
134135
<ClCompile Include="..\Modules\_testcapi\type.c" />
135136
<ClCompile Include="..\Modules\_testcapi\function.c" />

PCbuild/_testcapi.vcxproj.filters

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@
123123
<ClCompile Include="..\Modules\_testcapi\import.c">
124124
<Filter>Source Files</Filter>
125125
</ClCompile>
126+
<ClCompile Include="..\Modules\_testcapi\capsule.c">
127+
<Filter>Source Files</Filter>
128+
</ClCompile>
126129
<ClCompile Include="..\Modules\_testcapi\frame.c">
127130
<Filter>Source Files</Filter>
128131
</ClCompile>

0 commit comments

Comments
 (0)