Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add test for _Py_OPAQUE_PYOBJECT
  • Loading branch information
encukou committed Jul 11, 2025
commit 33d9ae7cbeea2702baa8566617d48d855e0242f7
21 changes: 16 additions & 5 deletions Lib/test/test_cext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
from test import support


SOURCE = os.path.join(os.path.dirname(__file__), 'extension.c')
SOURCES = [
os.path.join(os.path.dirname(__file__), 'extension.c'),
os.path.join(os.path.dirname(__file__), 'create_moduledef.c'),
]
SETUP = os.path.join(os.path.dirname(__file__), 'setup.py')


Expand Down Expand Up @@ -51,24 +54,32 @@ def test_build_limited(self):
def test_build_limited_c11(self):
self.check_build('_test_limited_c11_cext', limited=True, std='c11')

def check_build(self, extension_name, std=None, limited=False):
def test_build_opaque(self):
# Test with _Py_OPAQUE_PYOBJECT
self.check_build('_test_limited_opaque_cext', limited=True, opaque=True)

def check_build(self, extension_name, std=None, limited=False, opaque=False):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to rename "opaque" to "opaque_pyobject" (also in _check_build).

venv_dir = 'env'
with support.setup_venv_with_pip_setuptools(venv_dir) as python_exe:
self._check_build(extension_name, python_exe,
std=std, limited=limited)
std=std, limited=limited, opaque=opaque)

def _check_build(self, extension_name, python_exe, std, limited):
def _check_build(self, extension_name, python_exe, std, limited, opaque):
pkg_dir = 'pkg'
os.mkdir(pkg_dir)
shutil.copy(SETUP, os.path.join(pkg_dir, os.path.basename(SETUP)))
shutil.copy(SOURCE, os.path.join(pkg_dir, os.path.basename(SOURCE)))
for source in SOURCES:
dest = os.path.join(pkg_dir, os.path.basename(source))
shutil.copy(source, dest)

def run_cmd(operation, cmd):
env = os.environ.copy()
if std:
env['CPYTHON_TEST_STD'] = std
if limited:
env['CPYTHON_TEST_LIMITED'] = '1'
if opaque:
env['CPYTHON_TEST_OPAQUE_PYOBJECT'] = '1'
env['CPYTHON_TEST_EXT_NAME'] = extension_name
if support.verbose:
print('Run:', ' '.join(map(shlex.quote, cmd)))
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_cext/create_moduledef.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Workaround for testing _Py_OPAQUE_PYOBJECT.
// See end of 'extension.c'


#undef _Py_OPAQUE_PYOBJECT
#undef Py_LIMITED_API
#include "Python.h"


// (repeated definition to avoid creating a header)
extern PyObject *testcext_create_moduledef(
const char *name, const char *doc,
PyMethodDef *methods, PyModuleDef_Slot *slots);

PyObject *testcext_create_moduledef(
const char *name, const char *doc,
PyMethodDef *methods, PyModuleDef_Slot *slots) {

static struct PyModuleDef _testcext_module = {
PyModuleDef_HEAD_INIT,
};
if (!_testcext_module.m_name) {
_testcext_module.m_name = name;
_testcext_module.m_doc = doc;
_testcext_module.m_methods = methods;
_testcext_module.m_slots = slots;
}
return PyModuleDef_Init(&_testcext_module);
}
31 changes: 27 additions & 4 deletions Lib/test/test_cext/extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ _testcext_exec(
return 0;
}

#define _FUNC_NAME(NAME) PyInit_ ## NAME
#define FUNC_NAME(NAME) _FUNC_NAME(NAME)

// Converting from function pointer to void* has undefined behavior, but
// works on all known platforms, and CPython's module and type slots currently
// need it.
Expand All @@ -76,9 +79,10 @@ static PyModuleDef_Slot _testcext_slots[] = {

_Py_COMP_DIAG_POP


PyDoc_STRVAR(_testcext_doc, "C test extension.");

#ifndef _Py_OPAQUE_PYOBJECT

static struct PyModuleDef _testcext_module = {
PyModuleDef_HEAD_INIT, // m_base
STR(MODULE_NAME), // m_name
Expand All @@ -92,11 +96,30 @@ static struct PyModuleDef _testcext_module = {
};


#define _FUNC_NAME(NAME) PyInit_ ## NAME
#define FUNC_NAME(NAME) _FUNC_NAME(NAME)

PyMODINIT_FUNC
FUNC_NAME(MODULE_NAME)(void)
{
return PyModuleDef_Init(&_testcext_module);
}

#else // _Py_OPAQUE_PYOBJECT

// Opaque PyObject means that PyModuleDef is also opaque and cannot be
// declared statically. See PEP 793.
// So, this part of module creation is split into a separate source file
// which uses non-limited API.

// (repeated definition to avoid creating a header)
extern PyObject *testcext_create_moduledef(
const char *name, const char *doc,
PyMethodDef *methods, PyModuleDef_Slot *slots);


PyMODINIT_FUNC
FUNC_NAME(MODULE_NAME)(void)
{
return testcext_create_moduledef(
STR(MODULE_NAME), _testcext_doc, _testcext_methods, _testcext_slots);
}

#endif // _Py_OPAQUE_PYOBJECT
11 changes: 10 additions & 1 deletion Lib/test/test_cext/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ def main():
std = os.environ.get("CPYTHON_TEST_STD", "")
module_name = os.environ["CPYTHON_TEST_EXT_NAME"]
limited = bool(os.environ.get("CPYTHON_TEST_LIMITED", ""))
opaque = bool(os.environ.get("CPYTHON_TEST_OPAQUE_PYOBJECT", ""))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to rename opaque to opaque_pyobject.


sources = [SOURCE]

cflags = list(CFLAGS)
cflags.append(f'-DMODULE_NAME={module_name}')
Expand Down Expand Up @@ -75,6 +78,12 @@ def main():
version = sys.hexversion
cflags.append(f'-DPy_LIMITED_API={version:#x}')

# Define _Py_OPAQUE_PYOBJECT macro
if opaque:
version = sys.hexversion
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

version is not used, it can be removed.

cflags.append(f'-D_Py_OPAQUE_PYOBJECT')
sources.append('create_moduledef.c')

# On Windows, add PCbuild\amd64\ to include and library directories
include_dirs = []
library_dirs = []
Expand All @@ -99,7 +108,7 @@ def main():

ext = Extension(
module_name,
sources=[SOURCE],
sources=sources,
extra_compile_args=cflags,
include_dirs=include_dirs,
library_dirs=library_dirs)
Expand Down
Loading