From: Victor Stinner Date: Sun, 19 Jul 2026 12:04:30 +0000 (+0200) Subject: gh-153903: Use `@ctypes.util.wrap_dll_function()` with pythonapi (#154030) X-Git-Url: http://git.ipfire.org/index.cgi?a=commitdiff_plain;h=84897d2e30b1b478f3de4fb9a7cd41fc2363da80;p=thirdparty%2FPython%2Fcpython.git gh-153903: Use `@ctypes.util.wrap_dll_function()` with pythonapi (#154030) Use the `@ctypes.util.wrap_dll_function()` decorator with ctypes.pythonapi to wrap Python C API functions in the test suite. --- diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 6d84f0b8c305..7d668843d07d 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -3043,22 +3043,37 @@ class TestVersions(unittest.TestCase): def test_pack_full_version_ctypes(self): ctypes = import_helper.import_module('ctypes') - ctypes_func = ctypes.pythonapi.Py_PACK_FULL_VERSION - ctypes_func.restype = ctypes.c_uint32 - ctypes_func.argtypes = [ctypes.c_int] * 5 + import ctypes.util # noqa: F811 + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_PACK_FULL_VERSION( + x: ctypes.c_int, + y: ctypes.c_int, + z: ctypes.c_int, + level: ctypes.c_int, + serial: ctypes.c_int, + ) -> ctypes.c_uint32: + pass + for *args, expected in self.full_cases: with self.subTest(hexversion=hex(expected)): - result = ctypes_func(*args) + result = Py_PACK_FULL_VERSION(*args) self.assertEqual(result, expected) def test_pack_version_ctypes(self): ctypes = import_helper.import_module('ctypes') - ctypes_func = ctypes.pythonapi.Py_PACK_VERSION - ctypes_func.restype = ctypes.c_uint32 - ctypes_func.argtypes = [ctypes.c_int] * 2 + import ctypes.util # noqa: F811 + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_PACK_VERSION( + x: ctypes.c_int, + y: ctypes.c_int, + ) -> ctypes.c_uint32: + pass + for *args, expected in self.xy_cases: with self.subTest(hexversion=hex(expected)): - result = ctypes_func(*args) + result = Py_PACK_VERSION(*args) self.assertEqual(result, expected) diff --git a/Lib/test/test_code.py b/Lib/test/test_code.py index bd2fa1d7b704..631e5fd2499b 100644 --- a/Lib/test/test_code.py +++ b/Lib/test/test_code.py @@ -1540,21 +1540,26 @@ class CodeLocationTest(unittest.TestCase): [(1,1,3)]) if check_impl_detail(cpython=True) and ctypes is not None: - py = ctypes.pythonapi - freefunc = ctypes.CFUNCTYPE(None,ctypes.c_voidp) - - RequestCodeExtraIndex = py.PyUnstable_Eval_RequestCodeExtraIndex - RequestCodeExtraIndex.argtypes = (freefunc,) - RequestCodeExtraIndex.restype = ctypes.c_ssize_t - - SetExtra = py.PyUnstable_Code_SetExtra - SetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t, ctypes.c_voidp) - SetExtra.restype = ctypes.c_int - - GetExtra = py.PyUnstable_Code_GetExtra - GetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t, - ctypes.POINTER(ctypes.c_voidp)) - GetExtra.restype = ctypes.c_int + import ctypes.util + freefunc = ctypes.CFUNCTYPE(None, ctypes.c_voidp) + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyUnstable_Eval_RequestCodeExtraIndex(free: freefunc) -> ctypes.c_ssize_t: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyUnstable_Code_SetExtra(code: ctypes.py_object, + index: ctypes.c_ssize_t, + extra: ctypes.c_voidp) -> ctypes.c_int: + pass + SetExtra = PyUnstable_Code_SetExtra + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyUnstable_Code_GetExtra(code: ctypes.py_object, + index: ctypes.c_ssize_t, + extra: ctypes.POINTER(ctypes.c_voidp)) -> ctypes.c_int: + pass + GetExtra = PyUnstable_Code_GetExtra LAST_FREED = None def myfree(ptr): @@ -1562,7 +1567,7 @@ if check_impl_detail(cpython=True) and ctypes is not None: LAST_FREED = ptr FREE_FUNC = freefunc(myfree) - FREE_INDEX = RequestCodeExtraIndex(FREE_FUNC) + FREE_INDEX = PyUnstable_Eval_RequestCodeExtraIndex(FREE_FUNC) # Make sure myfree sticks around at least as long as the interpreter, # since we (currently) can't unregister the function and leaving a # dangling pointer will cause a crash on deallocation of code objects if diff --git a/Lib/test/test_ctypes/test_refcounts.py b/Lib/test/test_ctypes/test_refcounts.py index 1815649ceb5f..d79937277e7d 100644 --- a/Lib/test/test_ctypes/test_refcounts.py +++ b/Lib/test/test_ctypes/test_refcounts.py @@ -127,9 +127,9 @@ class PyObjectRestypeTest(unittest.TestCase): def test_restype_py_object_with_null_return(self): # Test that a function which returns a NULL PyObject * # without setting an exception does not crash. - PyErr_Occurred = ctypes.pythonapi.PyErr_Occurred - PyErr_Occurred.argtypes = [] - PyErr_Occurred.restype = ctypes.py_object + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyErr_Occurred() -> ctypes.py_object: + pass # At this point, there's no exception set, so PyErr_Occurred # returns NULL. Given the restype is py_object, the diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index cc7faef93e1a..6e458017298d 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -440,10 +440,16 @@ class ExceptionTests(unittest.TestCase): def test_windows_message(self): """Should fill in unknown error code in Windows error message""" ctypes = import_module('ctypes') + import ctypes.util # noqa: F811 + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyErr_SetFromWindowsErr(ierr: ctypes.c_int) -> ctypes.py_object: + pass + # this error code has no message, Python formats it as hexadecimal code = 3765269347 - with self.assertRaisesRegex(OSError, 'Windows Error 0x%x' % code): - ctypes.pythonapi.PyErr_SetFromWindowsErr(code) + with self.assertRaisesRegex(OSError, f'Windows Error 0x{code:x}'): + PyErr_SetFromWindowsErr(code) def testAttributes(self): # test that exception attributes are happy diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index fc50e16575da..a3a329cb578a 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -821,28 +821,37 @@ class TestFrameCApi(unittest.TestCase): def test_basic(self): x = 1 ctypes = import_helper.import_module('ctypes') - PyEval_GetFrameLocals = ctypes.pythonapi.PyEval_GetFrameLocals - PyEval_GetFrameLocals.restype = ctypes.py_object + import ctypes.util # noqa: F811 + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyEval_GetFrameLocals() -> ctypes.py_object: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyEval_GetFrameGlobals() -> ctypes.py_object: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyEval_GetFrameBuiltins() -> ctypes.py_object: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyFrame_GetLocals(frame: ctypes.py_object) -> ctypes.py_object: + pass + frame_locals = PyEval_GetFrameLocals() self.assertTrue(type(frame_locals), dict) self.assertEqual(frame_locals['x'], 1) frame_locals['x'] = 2 self.assertEqual(x, 1) - PyEval_GetFrameGlobals = ctypes.pythonapi.PyEval_GetFrameGlobals - PyEval_GetFrameGlobals.restype = ctypes.py_object frame_globals = PyEval_GetFrameGlobals() self.assertTrue(type(frame_globals), dict) self.assertIs(frame_globals, globals()) - PyEval_GetFrameBuiltins = ctypes.pythonapi.PyEval_GetFrameBuiltins - PyEval_GetFrameBuiltins.restype = ctypes.py_object frame_builtins = PyEval_GetFrameBuiltins() self.assertEqual(frame_builtins, __builtins__) - PyFrame_GetLocals = ctypes.pythonapi.PyFrame_GetLocals - PyFrame_GetLocals.argtypes = [ctypes.py_object] - PyFrame_GetLocals.restype = ctypes.py_object frame = sys._getframe() f_locals = PyFrame_GetLocals(frame) self.assertTrue(f_locals['x'], 1) diff --git a/Lib/test/test_free_threading/test_capi.py b/Lib/test/test_free_threading/test_capi.py index 146d7cfc97ad..b0be94629af0 100644 --- a/Lib/test/test_free_threading/test_capi.py +++ b/Lib/test/test_free_threading/test_capi.py @@ -1,4 +1,4 @@ -import ctypes +import ctypes.util import sys import unittest @@ -6,9 +6,9 @@ from test.support import threading_helper from test.support.threading_helper import run_concurrently -_PyImport_AddModuleRef = ctypes.pythonapi.PyImport_AddModuleRef -_PyImport_AddModuleRef.argtypes = (ctypes.c_char_p,) -_PyImport_AddModuleRef.restype = ctypes.py_object +@ctypes.util.wrap_dll_function(ctypes.pythonapi) +def PyImport_AddModuleRef(name: ctypes.c_char_p) -> ctypes.py_object: + pass @threading_helper.requires_working_threading() @@ -26,7 +26,7 @@ class TestImportCAPI(unittest.TestCase): results = [] def worker(): - module = _PyImport_AddModuleRef(module_name_bytes) + module = PyImport_AddModuleRef(module_name_bytes) results.append(module) for _ in range(NUM_ITERS): diff --git a/Lib/test/test_free_threading/test_code.py b/Lib/test/test_free_threading/test_code.py index 2fc5eea3773c..121f00c1d7a9 100644 --- a/Lib/test/test_free_threading/test_code.py +++ b/Lib/test/test_free_threading/test_code.py @@ -12,25 +12,28 @@ from test.support import threading_helper from test.support.threading_helper import run_concurrently if ctypes is not None: - capi = ctypes.pythonapi + import ctypes.util freefunc = ctypes.CFUNCTYPE(None, ctypes.c_voidp) - RequestCodeExtraIndex = capi.PyUnstable_Eval_RequestCodeExtraIndex - RequestCodeExtraIndex.argtypes = (freefunc,) - RequestCodeExtraIndex.restype = ctypes.c_ssize_t - - SetExtra = capi.PyUnstable_Code_SetExtra - SetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t, ctypes.c_voidp) - SetExtra.restype = ctypes.c_int - - GetExtra = capi.PyUnstable_Code_GetExtra - GetExtra.argtypes = ( - ctypes.py_object, - ctypes.c_ssize_t, - ctypes.POINTER(ctypes.c_voidp), - ) - GetExtra.restype = ctypes.c_int + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyUnstable_Eval_RequestCodeExtraIndex(free: freefunc) -> ctypes.c_ssize_t: + pass + RequestCodeExtraIndex = PyUnstable_Eval_RequestCodeExtraIndex + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyUnstable_Code_SetExtra(code: ctypes.py_object, + index: ctypes.c_ssize_t, + extra: ctypes.c_voidp) -> ctypes.c_int: + pass + SetExtra = PyUnstable_Code_SetExtra + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyUnstable_Code_GetExtra(code: ctypes.py_object, + index: ctypes.c_ssize_t, + extra: ctypes.POINTER(ctypes.c_voidp)) -> ctypes.c_int: + pass + GetExtra = PyUnstable_Code_GetExtra # Note: each call to RequestCodeExtraIndex permanently allocates a slot # (the counter is monotonically increasing), up to MAX_CO_EXTRA_USERS (255). diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 3fc084ea6e9c..4c721c01a34f 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1434,6 +1434,11 @@ class GCCallbackTests(unittest.TestCase): import subprocess code = textwrap.dedent(''' from test.support import gc_collect, SuppressCrashReport + import ctypes.util + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_DecRef(o: ctypes.py_object) -> None: + pass a = [1, 2, 3] b = [a, a] @@ -1445,8 +1450,7 @@ class GCCallbackTests(unittest.TestCase): # Simulate the refcount of "a" being too low (compared to the # references held on it by live data), but keeping it above zero # (to avoid deallocating it): - import ctypes - ctypes.pythonapi.Py_DecRef(ctypes.py_object(a)) + Py_DecRef(a) del a del b diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 3d01804513bd..96b43936be92 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -326,9 +326,13 @@ class ThreadTests(BaseTestCase): @cpython_only def test_PyThreadState_SetAsyncExc(self): ctypes = import_module("ctypes") + import ctypes.util # noqa: F811 - set_async_exc = ctypes.pythonapi.PyThreadState_SetAsyncExc - set_async_exc.argtypes = (ctypes.c_ulong, ctypes.py_object) + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyThreadState_SetAsyncExc(id: ctypes.c_ulong, + exc: ctypes.py_object) -> ctypes.c_int: + pass + set_async_exc = PyThreadState_SetAsyncExc class AsyncExc(Exception): pass @@ -485,7 +489,17 @@ class ThreadTests(BaseTestCase): import_module("ctypes") rc, out, err = assert_python_failure("-c", """if 1: - import ctypes, sys, time, _thread + import ctypes.util, sys, time, _thread + + PyGILState_STATE = ctypes.c_int # enum + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyGILState_Ensure() -> PyGILState_STATE: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def PyGILState_Release(oldstate: PyGILState_STATE) -> None: + pass # This lock is used as a simple event variable. ready = _thread.allocate_lock() @@ -494,8 +508,8 @@ class ThreadTests(BaseTestCase): # Module globals are cleared before __del__ is run # So we save the functions in class dict class C: - ensure = ctypes.pythonapi.PyGILState_Ensure - release = ctypes.pythonapi.PyGILState_Release + ensure = PyGILState_Ensure + release = PyGILState_Release def __del__(self): state = self.ensure() self.release(state)