--- /dev/null
+import importlib
+import os
+import sys
+import textwrap
+import unittest
+from test.support import import_helper, os_helper
+
+_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
+
+
+class CapsuleImportTests(unittest.TestCase):
+ """Tests for PyCapsule_Import()."""
+
+ @classmethod
+ def setUpClass(cls):
+ tmp = cls.tmp = cls.enterClassContext(os_helper.temp_dir())
+ cls.enterClassContext(import_helper.DirsOnSysPath(tmp))
+ cls.write_file(os.path.join(tmp, 'capsule_mod.py'), '''
+ import _testlimitedcapi
+
+ capsule = _testlimitedcapi.capsule_new('capsule_mod.capsule')
+ капсула = _testlimitedcapi.capsule_new('capsule_mod.капсула')
+ mismatched = _testlimitedcapi.capsule_new('other.name')
+ nonutf8 = _testlimitedcapi.capsule_new(b'capsule_mod.nonutf8\\xff')
+ nullname = _testlimitedcapi.capsule_new(None)
+ not_capsule = 42
+
+ class ns:
+ nested = _testlimitedcapi.capsule_new('capsule_mod.ns.nested')
+
+ def __getattr__(name):
+ if name == 'bad_attr':
+ raise FloatingPointError('bad attribute')
+ raise AttributeError(name)
+ ''')
+ pkg = os.path.join(tmp, 'capsule_pkg')
+ os.mkdir(pkg)
+ cls.write_file(os.path.join(pkg, '__init__.py'), '')
+ cls.write_file(os.path.join(pkg, 'sub.py'), '''
+ import _testlimitedcapi
+
+ capsule = _testlimitedcapi.capsule_new('capsule_pkg.sub.capsule')
+ ''')
+ autopkg = os.path.join(tmp, 'capsule_autopkg')
+ os.mkdir(autopkg)
+ cls.write_file(os.path.join(autopkg, '__init__.py'), 'from . import sub\n')
+ cls.write_file(os.path.join(autopkg, 'sub.py'), '''
+ import _testlimitedcapi
+
+ capsule = _testlimitedcapi.capsule_new('capsule_autopkg.sub.capsule')
+ ''')
+ cls.write_file(os.path.join(tmp, 'capsule_broken.py'), '1/0\n')
+ importlib.invalidate_caches()
+
+ def setUp(self):
+ for name in ('capsule_mod', 'capsule_pkg.sub', 'capsule_pkg',
+ 'capsule_autopkg.sub', 'capsule_autopkg',
+ 'capsule_broken'):
+ self.addCleanup(import_helper.unload, name)
+
+ @staticmethod
+ def write_file(path, source):
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(textwrap.dedent(source))
+
+ def check_import(self, name, no_block=0):
+ # _testlimitedcapi.PyCapsule_Import() returns the name stored as the
+ # pointer by _testlimitedcapi.capsule_new().
+ self.assertEqual(_testlimitedcapi.PyCapsule_Import(name, no_block), name)
+
+ def test_import(self):
+ # The module is imported if not already imported.
+ self.assertNotIn('capsule_mod', sys.modules)
+ self.check_import('capsule_mod.capsule')
+ # Attributes after the first component are plain attribute lookups.
+ self.check_import('capsule_mod.ns.nested')
+ # Non-ASCII capsule and attribute name.
+ self.check_import('capsule_mod.капсула')
+ # The no_block argument is ignored.
+ self.check_import('capsule_mod.capsule', 1)
+
+ @unittest.skipUnless(os_helper.TESTFN_NONASCII,
+ 'requires non-ASCII file name support')
+ def test_non_ascii_module_name(self):
+ name = os_helper.TESTFN_NONASCII
+ self.write_file(os.path.join(self.tmp, name + '.py'), f'''
+ import _testlimitedcapi
+
+ capsule = _testlimitedcapi.capsule_new('{name}.capsule')
+ ''')
+ importlib.invalidate_caches()
+ self.addCleanup(import_helper.unload, name)
+ self.check_import(f'{name}.capsule')
+
+ def test_submodule(self):
+ # Only the first component is imported; a submodule not imported
+ # by its package is not found.
+ self.assertRaises(AttributeError,
+ _testlimitedcapi.PyCapsule_Import, 'capsule_pkg.sub.capsule')
+ # It is found after explicit import.
+ importlib.import_module('capsule_pkg.sub')
+ self.check_import('capsule_pkg.sub.capsule')
+ # A submodule imported by its package is found.
+ self.check_import('capsule_autopkg.sub.capsule')
+
+ def test_invalid_name(self):
+ pycapsule_import = _testlimitedcapi.PyCapsule_Import
+ # Non-existing module.
+ self.assertRaisesRegex(ImportError,
+ 'PyCapsule_Import could not import module "capsule_nonexistent"',
+ pycapsule_import, 'capsule_nonexistent.capsule')
+ # Non-UTF-8 module name.
+ self.assertRaisesRegex(ImportError,
+ 'PyCapsule_Import could not import module',
+ pycapsule_import, b'\xff\xfe.capsule')
+ # Empty module name.
+ self.assertRaisesRegex(ImportError,
+ 'PyCapsule_Import could not import module ""',
+ pycapsule_import, '.capsule_mod.capsule')
+ # Empty name.
+ self.assertRaisesRegex(ImportError,
+ 'PyCapsule_Import could not import module ""',
+ pycapsule_import, '')
+ # Only a dot.
+ self.assertRaisesRegex(ImportError,
+ 'PyCapsule_Import could not import module ""',
+ pycapsule_import, '.')
+ # Non-existing attribute.
+ self.assertRaises(AttributeError,
+ pycapsule_import, 'capsule_mod.nonexistent')
+ # Empty attribute name.
+ self.assertRaises(AttributeError, pycapsule_import, 'capsule_mod.')
+ # Consecutive dots.
+ self.assertRaises(AttributeError,
+ pycapsule_import, 'capsule_mod..capsule')
+ # Attribute of an object which is not a module.
+ self.assertRaises(AttributeError,
+ pycapsule_import, 'capsule_mod.not_capsule.capsule')
+ # No attribute name.
+ self.assertRaisesRegex(AttributeError, 'is not valid',
+ pycapsule_import, 'capsule_mod')
+
+ # CRASHES pycapsule_import(NULL)
+
+ def test_invalid_capsule(self):
+ pycapsule_import = _testlimitedcapi.PyCapsule_Import
+ # The attribute is not a capsule.
+ self.assertRaisesRegex(AttributeError, 'is not valid',
+ pycapsule_import, 'capsule_mod.not_capsule')
+ # The capsule name does not match the requested name.
+ self.assertRaisesRegex(AttributeError, 'is not valid',
+ pycapsule_import, 'capsule_mod.mismatched')
+ # The capsule name contains a byte not decodable from UTF-8.
+ self.assertRaisesRegex(AttributeError, 'is not valid',
+ pycapsule_import, 'capsule_mod.nonutf8')
+ # Even the exactly matching name fails: the attribute lookup
+ # requires a name decodable from UTF-8.
+ self.assertRaises(UnicodeDecodeError,
+ pycapsule_import, b'capsule_mod.nonutf8\xff')
+ # The capsule name is NULL.
+ self.assertRaisesRegex(AttributeError, 'is not valid',
+ pycapsule_import, 'capsule_mod.nullname')
+
+ def test_error_from_import(self):
+ # The exception raised during importing the module is replaced
+ # with generic ImportError.
+ with self.assertRaises(ImportError) as cm:
+ _testlimitedcapi.PyCapsule_Import('capsule_broken.capsule')
+ self.assertEqual(str(cm.exception),
+ 'PyCapsule_Import could not import '
+ 'module "capsule_broken"')
+
+ def test_error_from_attribute_lookup(self):
+ self.assertRaises(FloatingPointError,
+ _testlimitedcapi.PyCapsule_Import, 'capsule_mod.bad_attr')
+ self.assertRaises(FloatingPointError,
+ _testlimitedcapi.PyCapsule_Import, 'capsule_mod.bad_attr.capsule')
+
+
+if __name__ == "__main__":
+ unittest.main()
--- /dev/null
+#include "parts.h"
+
+static void
+capsule_destructor(PyObject *op)
+{
+ /* If non-NULL, the name and the pointer are the same allocation. */
+ free((char *)PyCapsule_GetName(op));
+}
+
+static PyObject *
+capsule_new(PyObject *self, PyObject *arg)
+{
+ const char *name;
+ Py_ssize_t size;
+ if (!PyArg_Parse(arg, "z#", &name, &size)) {
+ return NULL;
+ }
+ char *name_copy = NULL;
+ if (name != NULL) {
+ name_copy = strdup(name);
+ if (name_copy == NULL) {
+ return PyErr_NoMemory();
+ }
+ }
+ static const char dummy = 0;
+ void *pointer = name_copy != NULL ? (void *)name_copy : (void *)&dummy;
+ PyObject *capsule = PyCapsule_New(pointer, name_copy, capsule_destructor);
+ if (capsule == NULL) {
+ free(name_copy);
+ }
+ return capsule;
+}
+
+static PyObject *
+pycapsule_import(PyObject *self, PyObject *args)
+{
+ const char *name;
+ Py_ssize_t size;
+ int no_block = 0;
+ if (!PyArg_ParseTuple(args, "z#|i", &name, &size, &no_block)) {
+ return NULL;
+ }
+ void *pointer = PyCapsule_Import(name, no_block);
+ if (pointer == NULL) {
+ return NULL;
+ }
+ /* Capsules created by capsule_new() store a copy of their name as the
+ pointer, so a successful import round-trips the name. Only use this
+ function with such capsules. */
+ return PyUnicode_FromString((const char *)pointer);
+}
+
+static PyMethodDef test_methods[] = {
+ {"capsule_new", capsule_new, METH_O},
+ {"PyCapsule_Import", pycapsule_import, METH_VARARGS},
+ {NULL},
+};
+
+int
+_PyTestLimitedCAPI_Init_Capsule(PyObject *m)
+{
+ return PyModule_AddFunctions(m, test_methods);
+}