gh-153903: Use `@ctypes.util.wrap_dll_function()` with pythonapi (#154030)

vstinner <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/84897d2e30b1b478f3de4fb9a7cd41fc2363da80
commit: 84897d2e30b1b478f3de4fb9a7cd41fc2363da80
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-07-19T12:04:30Z
summary:

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.

files:
M Lib/test/test_capi/test_misc.py
M Lib/test/test_code.py
M Lib/test/test_ctypes/test_refcounts.py
M Lib/test/test_exceptions.py
M Lib/test/test_frame.py
M Lib/test/test_free_threading/test_capi.py
M Lib/test/test_free_threading/test_code.py
M Lib/test/test_gc.py
M Lib/test/test_threading.py

diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py
index 6d84f0b8c305dfb..7d668843d07debc 100644
--- a/Lib/test/test_capi/test_misc.py
+++ b/Lib/test/test_capi/test_misc.py
@@ -3043,22 +3043,37 @@ def test_pack_version(self):
 
     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 bd2fa1d7b70421a..631e5fd2499b13e 100644
--- a/Lib/test/test_code.py
+++ b/Lib/test/test_code.py
@@ -1540,21 +1540,26 @@ async def afunc():
             [(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 @@ def myfree(ptr):
         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 1815649ceb5fff9..d79937277e7df69 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 cc7faef93e1af7c..6e458017298dffe 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -440,10 +440,16 @@ def test_WindowsError(self):
     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 fc50e16575da2b9..a3a329cb578a8dc 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 146d7cfc97adb7d..b0be94629af009d 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.threading_helper import run_concurrently
 
 
-_PyImport_AddModuleRef = ctypes.pythonapi.PyImport_AddModuleRef
-_PyImport_AddModuleRef.argtypes = (ctypes.c_char_p,)
-_PyImport_AddModuleRef.restype = ctypes.py_object
[email protected]_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 @@ def test_pyimport_addmoduleref_thread_safe(self):
         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 2fc5eea3773c399..121f00c1d7a92d9 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.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 3fc084ea6e9c6e9..4c721c01a34f070 100644
--- a/Lib/test/test_gc.py
+++ b/Lib/test/test_gc.py
@@ -1434,6 +1434,11 @@ def test_refcount_errors(self):
         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 @@ def test_refcount_errors(self):
             # 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 3d01804513bde98..96b43936be92cda 100644
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -326,9 +326,13 @@ def f(mutex):
     @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 @@ def test_finalize_running_thread(self):
         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 @@ def test_finalize_running_thread(self):
             # 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)

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.