From 782842d0e168939346f9dc2a9cf72ba2043d2642 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 14:24:35 +0200 Subject: [PATCH 1/6] gh-153903: Use `@ctypes.util.wrap_dll_function()` --- Lib/platform.py | 9 ++++--- Lib/test/pythoninfo.py | 15 +++++------ Lib/test/test_android.py | 12 ++++++--- Lib/test/test_embed.py | 36 ++++++++++++++++--------- Lib/test/test_ntpath.py | 18 ++++++++++--- Lib/test/test_os/test_windows.py | 45 ++++++++++++++++++-------------- Lib/test/win_console_handler.py | 11 ++++---- 7 files changed, 91 insertions(+), 55 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index 36489d4fdd98ae..f632d3b02323e5 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -550,14 +550,17 @@ def android_ver(release="", api_level=0, manufacturer="", model="", device="", # An NDK developer confirmed that this is an officially-supported # API (https://stackoverflow.com/a/28416743). Use `getattr` to avoid # private name mangling. - system_property_get = getattr(CDLL("libc.so"), "__system_property_get") - system_property_get.argtypes = (c_char_p, c_char_p) + libc = CDLL("libc.so") + + @ctypes.util.wrap_dll_function(libc) + def __system_property_get(name: c_char_p, value: c_char_p) -> c_int: + pass def getprop(name, default): # https://android.googlesource.com/platform/bionic/+/refs/tags/android-5.0.0_r1/libc/include/sys/system_properties.h#39 PROP_VALUE_MAX = 92 buffer = create_string_buffer(PROP_VALUE_MAX) - length = system_property_get(name.encode("UTF-8"), buffer) + length = __system_property_get(name.encode("UTF-8"), buffer) if length == 0: # This API doesn’t distinguish between an empty property and # a missing one. diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 3227b91bd82a86..4e711b6224a0af 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -1004,20 +1004,19 @@ def collect_windows(info_add): ntdll = ctypes.WinDLL('ntdll') BOOLEAN = ctypes.c_ubyte try: - RtlAreLongPathsEnabled = ntdll.RtlAreLongPathsEnabled + @ctypes.util.wrap_dll_function(ntdll) + def RtlAreLongPathsEnabled() -> BOOLEAN: + pass except AttributeError: res = '' else: - RtlAreLongPathsEnabled.restype = BOOLEAN - RtlAreLongPathsEnabled.argtypes = () res = bool(RtlAreLongPathsEnabled()) info_add('windows.RtlAreLongPathsEnabled', res) - shell32 = ctypes.windll.shell32 - IsUserAnAdmin = shell32.IsUserAnAdmin - IsUserAnAdmin.restype = BOOLEAN - IsUserAnAdmin.argtypes = () - info_add('windows.is_admin', IsUserAnAdmin()) + @ctypes.util.wrap_dll_function(ctypes.windll.shell32) + def IsUserAnAdmin() -> BOOLEAN: + pass + info_add('windows.is_admin', bool(IsUserAnAdmin())) try: import _winapi diff --git a/Lib/test/test_android.py b/Lib/test/test_android.py index 31daafbc3d6300..628b30b965adb7 100644 --- a/Lib/test/test_android.py +++ b/Lib/test/test_android.py @@ -41,13 +41,17 @@ def logcat_thread(): try: from ctypes import CDLL, c_char_p, c_int - android_log_write = getattr(CDLL("liblog.so"), "__android_log_write") - android_log_write.argtypes = (c_int, c_char_p, c_char_p) - ANDROID_LOG_INFO = 4 + liblog = CDLL("liblog.so") + + @ctypes.util.wrap_dll_function(liblog) + def __android_log_write(prio: c_int, tag: c_char_p, + text: c_char_p) -> c_int: + pass # Separate tests using a marker line with a different tag. + ANDROID_LOG_INFO = 4 tag, message = "python.test", f"{self.id()} {time()}" - android_log_write( + __android_log_write( ANDROID_LOG_INFO, tag.encode("UTF-8"), message.encode("UTF-8")) self.assert_log("I", tag, message, skip=True) except: diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 40036609a19055..13ab7bda79c0e8 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1884,19 +1884,31 @@ def test_global_pathconfig(self): # The global path configuration (_Py_path_config) must be a copy # of the path configuration of PyInterpreter.config (PyConfig). ctypes = import_helper.import_module('ctypes') + import ctypes.util - def get_func(name): - func = getattr(ctypes.pythonapi, name) - func.argtypes = () - func.restype = ctypes.c_wchar_p - return func - - Py_GetPath = get_func('Py_GetPath') - Py_GetPrefix = get_func('Py_GetPrefix') - Py_GetExecPrefix = get_func('Py_GetExecPrefix') - Py_GetProgramName = get_func('Py_GetProgramName') - Py_GetProgramFullPath = get_func('Py_GetProgramFullPath') - Py_GetPythonHome = get_func('Py_GetPythonHome') + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_GetPath() -> ctypes.c_wchar_p: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_GetPrefix() -> ctypes.c_wchar_p: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_GetExecPrefix() -> ctypes.c_wchar_p: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_GetProgramName() -> ctypes.c_wchar_p: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_GetProgramFullPath() -> ctypes.c_wchar_p: + pass + + @ctypes.util.wrap_dll_function(ctypes.pythonapi) + def Py_GetPythonHome() -> ctypes.c_wchar_p: + pass config = _testinternalcapi.get_configs()['config'] diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index a3728b58335e63..936332bf94ffe7 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -31,19 +31,29 @@ HAVE_GETFINALPATHNAME = True try: - import ctypes + import ctypes.util + import ctypes.wintypes except ImportError: HAVE_GETSHORTPATHNAME = False else: HAVE_GETSHORTPATHNAME = True def _getshortpathname(path): - GSPN = ctypes.WinDLL("kernel32", use_last_error=True).GetShortPathNameW - GSPN.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32] - GSPN.restype = ctypes.c_uint32 + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + + @ctypes.util.wrap_dll_function(kernel32) + def GetShortPathNameW( + lpszLongPath: ctypes.c_wchar_p, + lpszShortPath: ctypes.c_wchar_p, + cchBuffer: ctypes.wintypes.DWORD, + ) -> ctypes.wintypes.DWORD: + pass + GSPN = GetShortPathNameW + result_len = GSPN(path, None, 0) if not result_len: raise OSError("failed to get short path name 0x{:08X}" .format(ctypes.get_last_error())) + result = ctypes.create_unicode_buffer(result_len) result_len = GSPN(path, result, result_len) return result[:result_len] diff --git a/Lib/test/test_os/test_windows.py b/Lib/test/test_os/test_windows.py index f1c6283f60d35e..b21dd8a4dca660 100644 --- a/Lib/test/test_os/test_windows.py +++ b/Lib/test/test_os/test_windows.py @@ -27,7 +27,7 @@ def _kill(self, sig): # subprocess to the parent that the interpreter is ready. When it # becomes ready, send *sig* via os.kill to the subprocess and check # that the return code is equal to *sig*. - import ctypes + import ctypes.util from ctypes import wintypes import msvcrt @@ -35,14 +35,17 @@ def _kill(self, sig): # process has exited, use PeekNamedPipe to see what's inside stdout # without waiting. This is done so we can tell that the interpreter # is started and running at a point where it could handle a signal. - PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe - PeekNamedPipe.restype = wintypes.BOOL - PeekNamedPipe.argtypes = (wintypes.HANDLE, # Pipe handle - ctypes.POINTER(ctypes.c_char), # stdout buf - wintypes.DWORD, # Buffer size - ctypes.POINTER(wintypes.DWORD), # bytes read - ctypes.POINTER(wintypes.DWORD), # bytes avail - ctypes.POINTER(wintypes.DWORD)) # bytes left + @ctypes.util.wrap_dll_function(ctypes.windll.kernel32) + def PeekNamedPipe( + hNamedPipe: wintypes.HANDLE, + lpBuffer: ctypes.POINTER(ctypes.c_char), + nBufferSize: wintypes.DWORD, + lpBytesRead: ctypes.POINTER(wintypes.DWORD), + lpTotalBytesAvail: ctypes.POINTER(wintypes.DWORD), + lpBytesLeftThisMessage: ctypes.POINTER(wintypes.DWORD), + ) -> wintypes.BOOL: + pass + msg = "running" proc = subprocess.Popen([sys.executable, "-c", "import sys;" @@ -126,10 +129,11 @@ def test_CTRL_C_EVENT(self): # Make a NULL value by creating a pointer with no argument. NULL = ctypes.POINTER(ctypes.c_int)() - SetConsoleCtrlHandler = ctypes.windll.kernel32.SetConsoleCtrlHandler - SetConsoleCtrlHandler.argtypes = (ctypes.POINTER(ctypes.c_int), - wintypes.BOOL) - SetConsoleCtrlHandler.restype = wintypes.BOOL + + @ctypes.util.wrap_dll_function(ctypes.windll.kernel32) + def SetConsoleCtrlHandler(HandlerRoutine: ctypes.POINTER(ctypes.c_int), + Add: wintypes.BOOL) -> wintypes.BOOL: + pass # Calling this with NULL and FALSE causes the calling process to # handle Ctrl+C, rather than ignore it. This property is inherited @@ -458,17 +462,20 @@ def test_getfinalpathname_handles(self): import ctypes.wintypes # noqa: F811 kernel = ctypes.WinDLL('Kernel32.dll', use_last_error=True) - kernel.GetCurrentProcess.restype = ctypes.wintypes.HANDLE + @ctypes.util.wrap_dll_function(kernel) + def GetCurrentProcess() -> ctypes.wintypes.HANDLE: + pass - kernel.GetProcessHandleCount.restype = ctypes.wintypes.BOOL - kernel.GetProcessHandleCount.argtypes = (ctypes.wintypes.HANDLE, - ctypes.wintypes.LPDWORD) + @ctypes.util.wrap_dll_function(kernel) + def GetProcessHandleCount(khProcess: ctypes.wintypes.HANDLE, + pdwHandleCount: ctypes.wintypes.LPDWORD) -> ctypes.wintypes.BOOL: + pass # This is a pseudo-handle that doesn't need to be closed - hproc = kernel.GetCurrentProcess() + hproc = GetCurrentProcess() handle_count = ctypes.wintypes.DWORD() - ok = kernel.GetProcessHandleCount(hproc, ctypes.byref(handle_count)) + ok = GetProcessHandleCount(hproc, ctypes.byref(handle_count)) self.assertEqual(1, ok) before_count = handle_count.value diff --git a/Lib/test/win_console_handler.py b/Lib/test/win_console_handler.py index e7779b9363503b..0e2a8e1fe1545b 100644 --- a/Lib/test/win_console_handler.py +++ b/Lib/test/win_console_handler.py @@ -15,7 +15,7 @@ import sys # Function prototype for the handler function. Returns BOOL, takes a DWORD. -HandlerRoutine = WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD) +HANDLER_ROUTINE = WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD) def _ctrl_handler(sig): """Handle a sig event and return 0 to terminate the process""" @@ -27,12 +27,13 @@ def _ctrl_handler(sig): print("UNKNOWN EVENT") return 0 -ctrl_handler = HandlerRoutine(_ctrl_handler) +ctrl_handler = HANDLER_ROUTINE(_ctrl_handler) -SetConsoleCtrlHandler = ctypes.windll.kernel32.SetConsoleCtrlHandler -SetConsoleCtrlHandler.argtypes = (HandlerRoutine, wintypes.BOOL) -SetConsoleCtrlHandler.restype = wintypes.BOOL +@ctypes.util.wrap_dll_function(ctypes.windll.kernel32): +def SetConsoleCtrlHandler(HandlerRoutine: HANDLER_ROUTINE, + Add: wintypes.BOOL) -> wintypes.BOOL: + pass if __name__ == "__main__": # Add our console control handling function with value 1 From de3ba21777ae429420ed7714b8c9117340e51cb2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 15:21:52 +0200 Subject: [PATCH 2/6] Fix linter warnings/errors --- Lib/test/test_embed.py | 2 +- Lib/test/win_console_handler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 13ab7bda79c0e8..1ff600e30bf4cb 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1884,7 +1884,7 @@ def test_global_pathconfig(self): # The global path configuration (_Py_path_config) must be a copy # of the path configuration of PyInterpreter.config (PyConfig). ctypes = import_helper.import_module('ctypes') - import ctypes.util + import ctypes.util # noqa: F811 @ctypes.util.wrap_dll_function(ctypes.pythonapi) def Py_GetPath() -> ctypes.c_wchar_p: diff --git a/Lib/test/win_console_handler.py b/Lib/test/win_console_handler.py index 0e2a8e1fe1545b..a1c7c42c109330 100644 --- a/Lib/test/win_console_handler.py +++ b/Lib/test/win_console_handler.py @@ -30,7 +30,7 @@ def _ctrl_handler(sig): ctrl_handler = HANDLER_ROUTINE(_ctrl_handler) -@ctypes.util.wrap_dll_function(ctypes.windll.kernel32): +@ctypes.util.wrap_dll_function(ctypes.windll.kernel32) def SetConsoleCtrlHandler(HandlerRoutine: HANDLER_ROUTINE, Add: wintypes.BOOL) -> wintypes.BOOL: pass From 5c6329beb32f04aefc5f7521831b618a657d3b34 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 15:24:59 +0200 Subject: [PATCH 3/6] Fix platform on Android --- Lib/platform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/platform.py b/Lib/platform.py index f632d3b02323e5..84d41140d3e28c 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -544,6 +544,7 @@ def android_ver(release="", api_level=0, manufacturer="", model="", device="", if sys.platform == "android": try: from ctypes import CDLL, c_char_p, create_string_buffer + from ctypes.util import wrap_dll_function except ImportError: pass else: @@ -552,7 +553,7 @@ def android_ver(release="", api_level=0, manufacturer="", model="", device="", # private name mangling. libc = CDLL("libc.so") - @ctypes.util.wrap_dll_function(libc) + @wrap_dll_function(libc) def __system_property_get(name: c_char_p, value: c_char_p) -> c_int: pass From cf7980e294076cdbcacfe6b2c6d9eb2c0195fa59 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 15:26:31 +0200 Subject: [PATCH 4/6] Fix test.pythoninfo on Windows --- Lib/test/pythoninfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 4e711b6224a0af..13a3199b1f1267 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -995,7 +995,7 @@ def collect_windows(info_add): # windows.RtlAreLongPathsEnabled: RtlAreLongPathsEnabled() # windows.is_admin: IsUserAnAdmin() try: - import ctypes + import ctypes.util if not hasattr(ctypes, 'WinDLL'): raise ImportError except ImportError: From 4815bdc4dc1325b1b67c205353c5e3e82b0b6a5c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 15:41:35 +0200 Subject: [PATCH 5/6] Fix again platform on Android --- Lib/platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/platform.py b/Lib/platform.py index 84d41140d3e28c..3141998e02dfe2 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -543,7 +543,7 @@ def android_ver(release="", api_level=0, manufacturer="", model="", device="", is_emulator=False): if sys.platform == "android": try: - from ctypes import CDLL, c_char_p, create_string_buffer + from ctypes import CDLL, c_int, c_char_p, create_string_buffer from ctypes.util import wrap_dll_function except ImportError: pass From 63ca97817b1dc91bb962d5c284515215f73c9eae Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 16:23:56 +0200 Subject: [PATCH 6/6] Fix test_anroid: add missing import --- Lib/test/test_android.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_android.py b/Lib/test/test_android.py index 628b30b965adb7..201d701aff11eb 100644 --- a/Lib/test/test_android.py +++ b/Lib/test/test_android.py @@ -41,9 +41,10 @@ def logcat_thread(): try: from ctypes import CDLL, c_char_p, c_int + from ctypes.util import wrap_dll_function liblog = CDLL("liblog.so") - @ctypes.util.wrap_dll_function(liblog) + @wrap_dll_function(liblog) def __android_log_write(prio: c_int, tag: c_char_p, text: c_char_p) -> c_int: pass