From 5380589615515fa9e2c48820073a3fd8e32a930d Mon Sep 17 00:00:00 2001 From: "Dominic H." Date: Sun, 19 Jul 2026 13:58:06 +0200 Subject: [PATCH 01/16] Add test for explicit `cls` argument bypassing default JSON decoder (#154094) * Add test for skipped `cls=None` case * Fix lint * Add new line between classes --- Lib/test/test_json/test_decode.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Lib/test/test_json/test_decode.py b/Lib/test/test_json/test_decode.py index 1d51fb2de0e69e4..6d05ef8f8eb0918 100644 --- a/Lib/test/test_json/test_decode.py +++ b/Lib/test/test_json/test_decode.py @@ -1,4 +1,5 @@ import decimal +import unittest.mock from io import StringIO from collections import OrderedDict from test.test_json import PyTest, CTest @@ -155,6 +156,15 @@ def test_limit_int(self): with self.assertRaises(ValueError): self.loads('1' * (maxdigits + 1)) + def test_explicit_cls_skips_json_decoder_default(self): + class CustomDecoder: + pass + + with unittest.mock.patch.object( + CustomDecoder, 'decode', create=True) as mock_decode: + self.loads('{}', cls=CustomDecoder) + mock_decode.assert_called_once() + class TestPyDecode(TestDecode, PyTest): pass class TestCDecode(TestDecode, CTest): pass From 84897d2e30b1b478f3de4fb9a7cd41fc2363da80 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 14:04:30 +0200 Subject: [PATCH 02/16] 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. --- Lib/test/test_capi/test_misc.py | 31 ++++++++++++++----- Lib/test/test_code.py | 37 +++++++++++++---------- Lib/test/test_ctypes/test_refcounts.py | 6 ++-- Lib/test/test_exceptions.py | 10 ++++-- Lib/test/test_frame.py | 27 +++++++++++------ Lib/test/test_free_threading/test_capi.py | 10 +++--- Lib/test/test_free_threading/test_code.py | 35 +++++++++++---------- Lib/test/test_gc.py | 8 +++-- Lib/test/test_threading.py | 24 ++++++++++++--- 9 files changed, 122 insertions(+), 66 deletions(-) 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 +@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 @@ 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) From ee50ed3ed010e26495300778b30b57da5cadc933 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 14:08:56 +0200 Subject: [PATCH 03/16] gh-104533: Use `@ctypes.util.struct` decorator (#154031) Use also `@ctypes.util.wrap_dll_function()` decorator. --- Lib/test/_test_multiprocessing.py | 16 ++++----- Lib/test/support/__init__.py | 35 +++++++++++++++---- Lib/test/test_buffer.py | 22 ++++++++---- Lib/test/test_codecs.py | 22 +++++++----- Lib/test/test_ctypes/test_refcounts.py | 6 ++-- Lib/test/test_ctypes/test_unicode.py | 8 +++-- .../test_win32_com_foreign_func.py | 15 ++++---- Lib/test/test_io/utils.py | 5 +-- 8 files changed, 84 insertions(+), 45 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 115a187a8a85882..8f665b3a98a0371 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -161,9 +161,10 @@ def wait_for_handle(handle, timeout): # try: - from ctypes import Structure, c_int, c_double, c_longlong + from ctypes.util import struct as ctypes_struct + from ctypes import c_int, c_double, c_longlong except ImportError: - Structure = object + def ctypes_struct(cls): return cls c_int = c_double = c_longlong = None @@ -4390,12 +4391,11 @@ def test_free_from_gc(self): # # -class _Foo(Structure): - _fields_ = [ - ('x', c_int), - ('y', c_double), - ('z', c_longlong,) - ] +@ctypes_struct +class _Foo: + x: c_int + y: c_double + z: c_longlong class _TestSharedCTypes(BaseTestCase): diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index d17d9a2ecf8d9b0..e5bc2fd9b3d0954 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -236,20 +236,41 @@ def _is_gui_available(): # if Python is running as a service (such as the buildbot service), # gui interaction may be disallowed import ctypes + import ctypes.util import ctypes.wintypes + UOI_FLAGS = 1 WSF_VISIBLE = 0x0001 - class USEROBJECTFLAGS(ctypes.Structure): - _fields_ = [("fInherit", ctypes.wintypes.BOOL), - ("fReserved", ctypes.wintypes.BOOL), - ("dwFlags", ctypes.wintypes.DWORD)] - dll = ctypes.windll.user32 - h = dll.GetProcessWindowStation() + + @ctypes.util.struct + class USEROBJECTFLAGS: + fInherit: ctypes.wintypes.BOOL + fReserved: ctypes.wintypes.BOOL + dwFlags: ctypes.wintypes.DWORD + + user32 = ctypes.windll.user32 + + @ctypes.util.wrap_dll_function(user32) + def GetProcessWindowStation() -> ctypes.wintypes.HANDLE: + ... + + h = GetProcessWindowStation() if not h: raise ctypes.WinError() + + @ctypes.util.wrap_dll_function(user32) + def GetUserObjectInformationW( + hObj: ctypes.wintypes.HANDLE, + nIndex: ctypes.c_int, + pvInfo: ctypes.c_void_p, + nLength: ctypes.wintypes.DWORD, + lpnLengthNeeded: ctypes.POINTER(ctypes.wintypes.DWORD), + ) -> ctypes.wintypes.BOOL: + ... + uof = USEROBJECTFLAGS() needed = ctypes.wintypes.DWORD() - res = dll.GetUserObjectInformationW(h, + res = GetUserObjectInformationW(h, UOI_FLAGS, ctypes.byref(uof), ctypes.sizeof(uof), diff --git a/Lib/test/test_buffer.py b/Lib/test/test_buffer.py index 3213a4751273431..453dafe2709eb2f 100644 --- a/Lib/test/test_buffer.py +++ b/Lib/test/test_buffer.py @@ -37,6 +37,7 @@ try: import ctypes + import ctypes.util except ImportError: ctypes = None @@ -2849,8 +2850,11 @@ def test_memoryview_cast_1D_ND(self): if ctypes: # format: "T{>l:x:>d:y:}" - class BEPoint(ctypes.BigEndianStructure): - _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_double)] + @ctypes.util.struct(endian='big') + class BEPoint: + x: ctypes.c_long + y: ctypes.c_double + point = BEPoint(100, 200.1) m1 = memoryview(point) m2 = m1.cast('B') @@ -3250,8 +3254,11 @@ def test_memoryview_compare_special_cases(self): # Some ctypes format strings are unknown to the struct module. if ctypes: # format: "T{>l:x:>l:y:}" - class BEPoint(ctypes.BigEndianStructure): - _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)] + @ctypes.util.struct(endian='big') + class BEPoint: + x: ctypes.c_long + y: ctypes.c_long + point = BEPoint(100, 200) a = memoryview(point) b = memoryview(point) @@ -3988,8 +3995,11 @@ def test_memoryview_tobytes(self): # Unknown formats are handled: tobytes() purely depends on itemsize. if ctypes: # format: "T{>l:x:>l:y:}" - class BEPoint(ctypes.BigEndianStructure): - _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)] + @ctypes.util.struct(endian='big') + class BEPoint: + x: ctypes.c_long + y: ctypes.c_long + point = BEPoint(100, 200) a = memoryview(point) self.assertEqual(a.tobytes(), bytes(point)) diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py index c18b203f42f59f3..31704955df3e14e 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -36,21 +36,25 @@ def check(input, expect): self.assertEqual(coder(input), (expect, len(input))) return check -# On small versions of Windows like Windows IoT or Windows Nano Server not all codepages are present +# On small versions of Windows like Windows IoT or Windows Nano Server, +# not all codepages are present def is_code_page_present(cp): - from ctypes import POINTER, WINFUNCTYPE, WinDLL, Structure + from ctypes import POINTER, WINFUNCTYPE, WinDLL + from ctypes.util import struct from ctypes.wintypes import BOOL, BYTE, WCHAR, UINT, DWORD MAX_LEADBYTES = 12 # 5 ranges, 2 bytes ea., 0 term. MAX_DEFAULTCHAR = 2 # single or double byte MAX_PATH = 260 - class CPINFOEXW(Structure): - _fields_ = [("MaxCharSize", UINT), - ("DefaultChar", BYTE*MAX_DEFAULTCHAR), - ("LeadByte", BYTE*MAX_LEADBYTES), - ("UnicodeDefaultChar", WCHAR), - ("CodePage", UINT), - ("CodePageName", WCHAR*MAX_PATH)] + + @struct + class CPINFOEXW: + MaxCharSize: UINT + DefaultChar: BYTE * MAX_DEFAULTCHAR + LeadByte: BYTE * MAX_LEADBYTES + UnicodeDefaultChar: WCHAR + CodePage: UINT + CodePageName: WCHAR * MAX_PATH prototype = WINFUNCTYPE(BOOL, UINT, DWORD, POINTER(CPINFOEXW)) GetCPInfoEx = prototype(("GetCPInfoExW", WinDLL("kernel32"))) diff --git a/Lib/test/test_ctypes/test_refcounts.py b/Lib/test/test_ctypes/test_refcounts.py index d79937277e7df69..5191862658c5671 100644 --- a/Lib/test/test_ctypes/test_refcounts.py +++ b/Lib/test/test_ctypes/test_refcounts.py @@ -54,8 +54,10 @@ def func(*args): gc.collect() self.assertEqual(sys.getrefcount(func), orig_refcount) - class X(ctypes.Structure): - _fields_ = [("a", OtherCallback)] + @ctypes.util.struct + class X: + a: OtherCallback + x = X() x.a = OtherCallback(func) diff --git a/Lib/test/test_ctypes/test_unicode.py b/Lib/test/test_ctypes/test_unicode.py index d9e17371d135724..8e91f8f2c566803 100644 --- a/Lib/test/test_ctypes/test_unicode.py +++ b/Lib/test/test_ctypes/test_unicode.py @@ -1,4 +1,4 @@ -import ctypes +import ctypes.util import unittest from test.support import import_helper _ctypes_test = import_helper.import_module("_ctypes_test") @@ -26,8 +26,10 @@ def test_buffers(self): self.assertEqual(buf[6:5:-1], "") def test_embedded_null(self): - class TestStruct(ctypes.Structure): - _fields_ = [("unicode", ctypes.c_wchar_p)] + @ctypes.util.struct + class TestStruct: + unicode: ctypes.c_wchar_p + t = TestStruct() # This would raise a ValueError: t.unicode = "foo\0bar\0\0" diff --git a/Lib/test/test_ctypes/test_win32_com_foreign_func.py b/Lib/test/test_ctypes/test_win32_com_foreign_func.py index 7e54f8f6c31d33f..c797ab98cf1e424 100644 --- a/Lib/test/test_ctypes/test_win32_com_foreign_func.py +++ b/Lib/test/test_ctypes/test_win32_com_foreign_func.py @@ -1,4 +1,4 @@ -import ctypes +import ctypes.util import gc import sys import unittest @@ -20,14 +20,13 @@ E_NOINTERFACE = -2147467262 -class GUID(ctypes.Structure): +@ctypes.util.struct +class GUID: # https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid - _fields_ = [ - ("Data1", DWORD), - ("Data2", WORD), - ("Data3", WORD), - ("Data4", BYTE * 8), - ] + Data1: DWORD + Data2: WORD + Data3: WORD + Data4: BYTE * 8 def create_proto_com_method(name, index, restype, *argtypes): diff --git a/Lib/test/test_io/utils.py b/Lib/test/test_io/utils.py index 3b1faec2140fbc6..dde49337a24f0b9 100644 --- a/Lib/test/test_io/utils.py +++ b/Lib/test/test_io/utils.py @@ -8,12 +8,13 @@ try: - import ctypes + import ctypes.util except ImportError: def byteslike(*pos, **kw): return array.array("b", bytes(*pos, **kw)) else: - class EmptyStruct(ctypes.Structure): + @ctypes.util.struct + class EmptyStruct: pass def byteslike(*pos, **kw): From 104c397d8f1eeb3649d0c676eec95cd0c9c3f7ce Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 19 Jul 2026 15:26:47 +0300 Subject: [PATCH 04/16] gh-154055: Gate optional curses functions absent on old SVr4 curses (GH-154057) Build hygiene so the curses modules build against a limited curses (e.g. the native SVr4 curses of illumos/Solaris), matching how other optional functions are already gated: * Probe and #ifdef-gate the X/Open attr_t functions (window.attr_get/attr_set/ attr_on/attr_off/color_set), the soft-label attribute functions (slk_attr_on/off/set, slk_color) and scr_set(); scr_set is probed separately from the scr_dump family, which SVr4 has without it. * Stop gating update_lines_cols() on resizeterm(): it only reads LINES/COLS and is used unconditionally (e.g. by set_term()), so a build without resizeterm() failed to link. * On Solaris/illumos define _BOOL (and include ) so the SVr4 "typedef char bool" does not clash with C's bool. test.test_curses: skip or guard the tests that use the now-optional functions, split test_attributes so its chtype-based part still runs, and treat the native curses of NetBSD and illumos/Solaris as having broken newterm() (they crash on repeated newterm()/delscreen(), like ncurses before 6.5). Co-authored-by: Claude Opus 4.8 --- Include/py_curses.h | 9 + Lib/test/test_curses.py | 38 +- ...-07-19-07-48-43.gh-issue-154055.FemUJU.rst | 4 + Modules/_cursesmodule.c | 27 +- Modules/clinic/_cursesmodule.c.h | 86 ++- configure | 600 ++++++++++++++++++ configure.ac | 16 +- pyconfig.h.in | 30 + 8 files changed, 781 insertions(+), 29 deletions(-) create mode 100644 Misc/NEWS.d/next/Build/2026-07-19-07-48-43.gh-issue-154055.FemUJU.rst diff --git a/Include/py_curses.h b/Include/py_curses.h index a5f5f3d76075827..85a540086c83844 100644 --- a/Include/py_curses.h +++ b/Include/py_curses.h @@ -43,6 +43,15 @@ # define PDC_NCMOUSE #endif +/* On Solaris/illumos, the SVr4 does "typedef char bool;", which + clashes with C's bool from . Define _BOOL to suppress it, and + include for the bool the header then needs. ncurses ignores + _BOOL. */ +#if defined(__sun) && !defined(_BOOL) +# include +# define _BOOL +#endif + #if defined(HAVE_NCURSESW_NCURSES_H) # include #elif defined(HAVE_NCURSESW_CURSES_H) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index b0592d3f1d92b95..4ca5dd73f55afee 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -84,10 +84,15 @@ def wrapped(self, *args, **kwargs): term = os.environ.get('TERM') SHORT_MAX = 0x7fff -# ncurses before 6.5 can crash on repeated newterm(). Fall back to initscr() -# and skip the tests that need several screens. +# ncurses before 6.5, and the native curses of NetBSD and illumos/Solaris, +# crash on repeated newterm()/delscreen(); fall back to initscr() and skip the +# multi-screen tests. The native ones are keyed off the platform so a fixed +# version can be excluded later. _ncurses_version = getattr(curses, 'ncurses_version', None) -BROKEN_NEWTERM = _ncurses_version is not None and _ncurses_version < (6, 5) +if _ncurses_version is not None: + BROKEN_NEWTERM = _ncurses_version < (6, 5) +else: + BROKEN_NEWTERM = sys.platform.startswith(('netbsd', 'sunos')) USE_NEWTERM = hasattr(curses, 'newterm') and not BROKEN_NEWTERM # Older macOS reports a variation selector as a spacing character (wcwidth() @@ -1140,8 +1145,17 @@ def test_attributes(self): win.standout() win.standend() + # attron()/attroff()/attrset() reject a bad attribute. + self.assertRaises(OverflowError, win.attron, 1 << 64) + self.assertRaises(OverflowError, win.attroff, -1) + self.assertRaises(OverflowError, win.attrset, 1 << 64) + self.assertRaises(TypeError, win.attron, 'x') + + @requires_curses_window_meth('attr_set') + def test_attr(self): # The attr_*() family works on attr_t attributes paired with a color # pair, unlike the chtype-based attron()/attroff()/attrset(). + win = curses.newwin(5, 15, 5, 2) win.attr_set(curses.A_BOLD | curses.A_UNDERLINE) attrs, pair = win.attr_get() self.assertTrue(attrs & curses.A_BOLD) @@ -1167,13 +1181,9 @@ def test_attributes(self): self.assertRaises(OverflowError, win.attr_set, -1) self.assertRaises(OverflowError, win.attr_on, -1) self.assertRaises(OverflowError, win.attr_set, 1 << 64) - # attron()/attroff()/attrset() reject a bad attribute too. - self.assertRaises(OverflowError, win.attron, 1 << 64) - self.assertRaises(OverflowError, win.attroff, -1) - self.assertRaises(OverflowError, win.attrset, 1 << 64) - self.assertRaises(TypeError, win.attron, 'x') @requires_colors + @requires_curses_window_meth('attr_set') def test_attr_color_pair(self): win = curses.newwin(5, 15, 5, 2) curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) @@ -1383,8 +1393,10 @@ def test_scr_dump(self): stdscr.refresh() self.assertIsNone(curses.scr_restore(dump)) # scr_init() and scr_set() also accept a dump file and return None. + # scr_set() is not available on every curses (e.g. old SVr4). self.assertIsNone(curses.scr_init(dump)) - self.assertIsNone(curses.scr_set(dump)) + if hasattr(curses, 'scr_set'): + self.assertIsNone(curses.scr_set(dump)) # A bytes (path-like) filename is accepted too. curses.scr_dump(os.fsencode(dump)) # Restoring from a missing file is an error. @@ -1846,9 +1858,11 @@ def test_escdelay(self): def test_tabsize(self): tabsize = curses.get_tabsize() self.assertIsInstance(tabsize, int) - curses.set_tabsize(4) - self.assertEqual(curses.get_tabsize(), 4) - curses.set_tabsize(tabsize) + # set_tabsize() is not available on every curses (e.g. old SVr4). + if hasattr(curses, 'set_tabsize'): + curses.set_tabsize(4) + self.assertEqual(curses.get_tabsize(), 4) + curses.set_tabsize(tabsize) @requires_curses_func('getsyx') def test_getsyx(self): diff --git a/Misc/NEWS.d/next/Build/2026-07-19-07-48-43.gh-issue-154055.FemUJU.rst b/Misc/NEWS.d/next/Build/2026-07-19-07-48-43.gh-issue-154055.FemUJU.rst new file mode 100644 index 000000000000000..94fc9672ef7855f --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-07-19-07-48-43.gh-issue-154055.FemUJU.rst @@ -0,0 +1,4 @@ +The :mod:`curses` and :mod:`curses.panel` modules can now be built against a +curses library that lacks the X/Open ``attr_t`` and soft-label attribute +functions, ``scr_set()`` or ``resizeterm()`` -- such as the native SVr4 curses +of illumos and Solaris. These functions are probed and used only when present. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 2253d9ff3d704f3..6df593001b8a1cb 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -2433,6 +2433,7 @@ _curses_window_attrset_impl(PyCursesWindowObject *self, attr_t attr) return curses_window_check_err(self, rtn, "wattrset", "attrset"); } +#ifdef HAVE_CURSES_WATTR_GET /*[clinic input] _curses.window.attr_get @@ -2458,7 +2459,9 @@ _curses_window_attr_get_impl(PyCursesWindowObject *self) } return Py_BuildValue("(ki)", (unsigned long)attrs, (int)pair); } +#endif /* HAVE_CURSES_WATTR_GET */ +#ifdef HAVE_CURSES_WATTR_SET /*[clinic input] _curses.window.attr_set @@ -2482,7 +2485,9 @@ _curses_window_attr_set_impl(PyCursesWindowObject *self, attr_t attr, #endif return curses_window_check_err(self, rtn, "wattr_set", "attr_set"); } +#endif /* HAVE_CURSES_WATTR_SET */ +#ifdef HAVE_CURSES_WATTR_ON /*[clinic input] _curses.window.attr_on @@ -2499,7 +2504,9 @@ _curses_window_attr_on_impl(PyCursesWindowObject *self, attr_t attr) int rtn = wattr_on(self->win, attr, NULL); return curses_window_check_err(self, rtn, "wattr_on", "attr_on"); } +#endif /* HAVE_CURSES_WATTR_ON */ +#ifdef HAVE_CURSES_WATTR_OFF /*[clinic input] _curses.window.attr_off @@ -2516,7 +2523,9 @@ _curses_window_attr_off_impl(PyCursesWindowObject *self, attr_t attr) int rtn = wattr_off(self->win, attr, NULL); return curses_window_check_err(self, rtn, "wattr_off", "attr_off"); } +#endif /* HAVE_CURSES_WATTR_OFF */ +#ifdef HAVE_CURSES_WCOLOR_SET /*[clinic input] _curses.window.color_set @@ -2538,6 +2547,7 @@ _curses_window_color_set_impl(PyCursesWindowObject *self, int pair) #endif return curses_window_check_err(self, rtn, "wcolor_set", "color_set"); } +#endif /* HAVE_CURSES_WCOLOR_SET */ /*[clinic input] _curses.window.getattrs @@ -6083,6 +6093,7 @@ _curses_scr_init(PyObject *module, PyObject *filename) /*[clinic end generated code: output=2e861d381d710419 input=81c45e4702124ef6]*/ ScreenDumpFunctionBody(scr_init) +#ifdef HAVE_CURSES_SCR_SET /*[clinic input] _curses.scr_set @@ -6099,6 +6110,7 @@ static PyObject * _curses_scr_set(PyObject *module, PyObject *filename) /*[clinic end generated code: output=6056fdec12c5935f input=d248c20543cc289b]*/ ScreenDumpFunctionBody(scr_set) +#endif /* HAVE_CURSES_SCR_SET */ #endif /* HAVE_CURSES_SCR_DUMP */ /*[clinic input] @@ -7462,9 +7474,10 @@ _curses_qiflush_impl(PyObject *module, int flag) Py_RETURN_NONE; } -#if defined(HAVE_CURSES_RESIZETERM) || defined(HAVE_CURSES_RESIZE_TERM) /* Internal helper used for updating curses.LINES, curses.COLS, _curses.LINES - * and _curses.COLS. Returns 1 on success and 0 on failure. */ + * and _curses.COLS. Returns 1 on success and 0 on failure. Used + * unconditionally (e.g. by set_term()), so it must not be gated on resizeterm(). + */ static int update_lines_cols(PyObject *private_module) { @@ -7533,8 +7546,6 @@ _curses_update_lines_cols_impl(PyObject *module) Py_RETURN_NONE; } -#endif - /*[clinic input] _curses.raw @@ -8371,6 +8382,7 @@ _curses_slk_attr_impl(PyObject *module) } #endif +#ifdef HAVE_CURSES_SLK_ATTR_ON /*[clinic input] _curses.slk_attr_on @@ -8388,7 +8400,9 @@ _curses_slk_attr_on_impl(PyObject *module, attr_t attr) return curses_check_err(module, slk_attr_on(attr, NULL), "slk_attr_on", NULL); } +#endif /* HAVE_CURSES_SLK_ATTR_ON */ +#ifdef HAVE_CURSES_SLK_ATTR_OFF /*[clinic input] _curses.slk_attr_off @@ -8406,7 +8420,9 @@ _curses_slk_attr_off_impl(PyObject *module, attr_t attr) return curses_check_err(module, slk_attr_off(attr, NULL), "slk_attr_off", NULL); } +#endif /* HAVE_CURSES_SLK_ATTR_OFF */ +#ifdef HAVE_CURSES_SLK_ATTR_SET /*[clinic input] _curses.slk_attr_set @@ -8430,7 +8446,9 @@ _curses_slk_attr_set_impl(PyObject *module, attr_t attr, int pair) #endif return curses_check_err(module, rtn, "slk_attr_set", NULL); } +#endif /* HAVE_CURSES_SLK_ATTR_SET */ +#ifdef HAVE_CURSES_SLK_COLOR /*[clinic input] _curses.slk_color @@ -8447,6 +8465,7 @@ _curses_slk_color_impl(PyObject *module, int pair) PyCursesStatefulInitialised(module); return curses_check_err(module, slk_color((short)pair), "slk_color", NULL); } +#endif /* HAVE_CURSES_SLK_COLOR */ #ifdef HAVE_CURSES_USE_ENV /*[clinic input] diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index da1b97c0ea5ad8b..70d99b4bf351a21 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -534,6 +534,8 @@ _curses_window_attrset(PyObject *self, PyObject *arg) return return_value; } +#if defined(HAVE_CURSES_WATTR_GET) + PyDoc_STRVAR(_curses_window_attr_get__doc__, "attr_get($self, /)\n" "--\n" @@ -552,6 +554,10 @@ _curses_window_attr_get(PyObject *self, PyObject *Py_UNUSED(ignored)) return _curses_window_attr_get_impl((PyCursesWindowObject *)self); } +#endif /* defined(HAVE_CURSES_WATTR_GET) */ + +#if defined(HAVE_CURSES_WATTR_SET) + PyDoc_STRVAR(_curses_window_attr_set__doc__, "attr_set($self, attr, pair=0, /)\n" "--\n" @@ -591,6 +597,10 @@ _curses_window_attr_set(PyObject *self, PyObject *const *args, Py_ssize_t nargs) return return_value; } +#endif /* defined(HAVE_CURSES_WATTR_SET) */ + +#if defined(HAVE_CURSES_WATTR_ON) + PyDoc_STRVAR(_curses_window_attr_on__doc__, "attr_on($self, attr, /)\n" "--\n" @@ -618,6 +628,10 @@ _curses_window_attr_on(PyObject *self, PyObject *arg) return return_value; } +#endif /* defined(HAVE_CURSES_WATTR_ON) */ + +#if defined(HAVE_CURSES_WATTR_OFF) + PyDoc_STRVAR(_curses_window_attr_off__doc__, "attr_off($self, attr, /)\n" "--\n" @@ -645,6 +659,10 @@ _curses_window_attr_off(PyObject *self, PyObject *arg) return return_value; } +#endif /* defined(HAVE_CURSES_WATTR_OFF) */ + +#if defined(HAVE_CURSES_WCOLOR_SET) + PyDoc_STRVAR(_curses_window_color_set__doc__, "color_set($self, pair, /)\n" "--\n" @@ -672,6 +690,8 @@ _curses_window_color_set(PyObject *self, PyObject *arg) return return_value; } +#endif /* defined(HAVE_CURSES_WCOLOR_SET) */ + PyDoc_STRVAR(_curses_window_getattrs__doc__, "getattrs($self, /)\n" "--\n" @@ -3104,7 +3124,7 @@ PyDoc_STRVAR(_curses_scr_init__doc__, #endif /* defined(HAVE_CURSES_SCR_DUMP) */ -#if defined(HAVE_CURSES_SCR_DUMP) +#if defined(HAVE_CURSES_SCR_DUMP) && defined(HAVE_CURSES_SCR_SET) PyDoc_STRVAR(_curses_scr_set__doc__, "scr_set($module, filename, /)\n" @@ -3120,7 +3140,7 @@ PyDoc_STRVAR(_curses_scr_set__doc__, #define _CURSES_SCR_SET_METHODDEF \ {"scr_set", (PyCFunction)_curses_scr_set, METH_O, _curses_scr_set__doc__}, -#endif /* defined(HAVE_CURSES_SCR_DUMP) */ +#endif /* defined(HAVE_CURSES_SCR_DUMP) && defined(HAVE_CURSES_SCR_SET) */ PyDoc_STRVAR(_curses_halfdelay__doc__, "halfdelay($module, tenths, /)\n" @@ -4752,8 +4772,6 @@ _curses_qiflush(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -#if (defined(HAVE_CURSES_RESIZETERM) || defined(HAVE_CURSES_RESIZE_TERM)) - PyDoc_STRVAR(_curses_update_lines_cols__doc__, "update_lines_cols($module, /)\n" "--\n" @@ -4774,8 +4792,6 @@ _curses_update_lines_cols(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_update_lines_cols_impl(module); } -#endif /* (defined(HAVE_CURSES_RESIZETERM) || defined(HAVE_CURSES_RESIZE_TERM)) */ - PyDoc_STRVAR(_curses_raw__doc__, "raw($module, flag=True, /)\n" "--\n" @@ -5755,6 +5771,8 @@ _curses_slk_attr(PyObject *module, PyObject *Py_UNUSED(ignored)) #endif /* (defined(NCURSES_EXT_FUNCS) || defined(PDCURSES)) */ +#if defined(HAVE_CURSES_SLK_ATTR_ON) + PyDoc_STRVAR(_curses_slk_attr_on__doc__, "slk_attr_on($module, attr, /)\n" "--\n" @@ -5782,6 +5800,10 @@ _curses_slk_attr_on(PyObject *module, PyObject *arg) return return_value; } +#endif /* defined(HAVE_CURSES_SLK_ATTR_ON) */ + +#if defined(HAVE_CURSES_SLK_ATTR_OFF) + PyDoc_STRVAR(_curses_slk_attr_off__doc__, "slk_attr_off($module, attr, /)\n" "--\n" @@ -5809,6 +5831,10 @@ _curses_slk_attr_off(PyObject *module, PyObject *arg) return return_value; } +#endif /* defined(HAVE_CURSES_SLK_ATTR_OFF) */ + +#if defined(HAVE_CURSES_SLK_ATTR_SET) + PyDoc_STRVAR(_curses_slk_attr_set__doc__, "slk_attr_set($module, attr, pair=0, /)\n" "--\n" @@ -5847,6 +5873,10 @@ _curses_slk_attr_set(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } +#endif /* defined(HAVE_CURSES_SLK_ATTR_SET) */ + +#if defined(HAVE_CURSES_SLK_COLOR) + PyDoc_STRVAR(_curses_slk_color__doc__, "slk_color($module, pair, /)\n" "--\n" @@ -5874,6 +5904,8 @@ _curses_slk_color(PyObject *module, PyObject *arg) return return_value; } +#endif /* defined(HAVE_CURSES_SLK_COLOR) */ + #if defined(HAVE_CURSES_USE_ENV) PyDoc_STRVAR(_curses_use_env__doc__, @@ -6003,6 +6035,26 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored return _curses_has_extended_color_support_impl(module); } +#ifndef _CURSES_WINDOW_ATTR_GET_METHODDEF + #define _CURSES_WINDOW_ATTR_GET_METHODDEF +#endif /* !defined(_CURSES_WINDOW_ATTR_GET_METHODDEF) */ + +#ifndef _CURSES_WINDOW_ATTR_SET_METHODDEF + #define _CURSES_WINDOW_ATTR_SET_METHODDEF +#endif /* !defined(_CURSES_WINDOW_ATTR_SET_METHODDEF) */ + +#ifndef _CURSES_WINDOW_ATTR_ON_METHODDEF + #define _CURSES_WINDOW_ATTR_ON_METHODDEF +#endif /* !defined(_CURSES_WINDOW_ATTR_ON_METHODDEF) */ + +#ifndef _CURSES_WINDOW_ATTR_OFF_METHODDEF + #define _CURSES_WINDOW_ATTR_OFF_METHODDEF +#endif /* !defined(_CURSES_WINDOW_ATTR_OFF_METHODDEF) */ + +#ifndef _CURSES_WINDOW_COLOR_SET_METHODDEF + #define _CURSES_WINDOW_COLOR_SET_METHODDEF +#endif /* !defined(_CURSES_WINDOW_COLOR_SET_METHODDEF) */ + #ifndef _CURSES_WINDOW_ENCLOSE_METHODDEF #define _CURSES_WINDOW_ENCLOSE_METHODDEF #endif /* !defined(_CURSES_WINDOW_ENCLOSE_METHODDEF) */ @@ -6135,10 +6187,6 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_MOUSEMASK_METHODDEF #endif /* !defined(_CURSES_MOUSEMASK_METHODDEF) */ -#ifndef _CURSES_UPDATE_LINES_COLS_METHODDEF - #define _CURSES_UPDATE_LINES_COLS_METHODDEF -#endif /* !defined(_CURSES_UPDATE_LINES_COLS_METHODDEF) */ - #ifndef _CURSES_RESIZETERM_METHODDEF #define _CURSES_RESIZETERM_METHODDEF #endif /* !defined(_CURSES_RESIZETERM_METHODDEF) */ @@ -6163,6 +6211,22 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_SLK_ATTR_METHODDEF #endif /* !defined(_CURSES_SLK_ATTR_METHODDEF) */ +#ifndef _CURSES_SLK_ATTR_ON_METHODDEF + #define _CURSES_SLK_ATTR_ON_METHODDEF +#endif /* !defined(_CURSES_SLK_ATTR_ON_METHODDEF) */ + +#ifndef _CURSES_SLK_ATTR_OFF_METHODDEF + #define _CURSES_SLK_ATTR_OFF_METHODDEF +#endif /* !defined(_CURSES_SLK_ATTR_OFF_METHODDEF) */ + +#ifndef _CURSES_SLK_ATTR_SET_METHODDEF + #define _CURSES_SLK_ATTR_SET_METHODDEF +#endif /* !defined(_CURSES_SLK_ATTR_SET_METHODDEF) */ + +#ifndef _CURSES_SLK_COLOR_METHODDEF + #define _CURSES_SLK_COLOR_METHODDEF +#endif /* !defined(_CURSES_SLK_COLOR_METHODDEF) */ + #ifndef _CURSES_USE_ENV_METHODDEF #define _CURSES_USE_ENV_METHODDEF #endif /* !defined(_CURSES_USE_ENV_METHODDEF) */ @@ -6174,4 +6238,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=36fcacafc5044720 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=ae1359964feefd64 input=a9049054013a1b77]*/ diff --git a/configure b/configure index 9061cf905119c44..0fc2b0e819a1209 100755 --- a/configure +++ b/configure @@ -32395,6 +32395,546 @@ fi + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function wattr_get" >&5 +printf %s "checking for curses function wattr_get... " >&6; } +if test ${ac_cv_lib_curses_wattr_get+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef wattr_get + void *x=wattr_get + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_wattr_get=yes +else case e in #( + e) ac_cv_lib_curses_wattr_get=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_wattr_get" >&5 +printf "%s\n" "$ac_cv_lib_curses_wattr_get" >&6; } + if test "x$ac_cv_lib_curses_wattr_get" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_WATTR_GET 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function wattr_set" >&5 +printf %s "checking for curses function wattr_set... " >&6; } +if test ${ac_cv_lib_curses_wattr_set+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef wattr_set + void *x=wattr_set + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_wattr_set=yes +else case e in #( + e) ac_cv_lib_curses_wattr_set=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_wattr_set" >&5 +printf "%s\n" "$ac_cv_lib_curses_wattr_set" >&6; } + if test "x$ac_cv_lib_curses_wattr_set" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_WATTR_SET 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function wattr_on" >&5 +printf %s "checking for curses function wattr_on... " >&6; } +if test ${ac_cv_lib_curses_wattr_on+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef wattr_on + void *x=wattr_on + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_wattr_on=yes +else case e in #( + e) ac_cv_lib_curses_wattr_on=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_wattr_on" >&5 +printf "%s\n" "$ac_cv_lib_curses_wattr_on" >&6; } + if test "x$ac_cv_lib_curses_wattr_on" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_WATTR_ON 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function wattr_off" >&5 +printf %s "checking for curses function wattr_off... " >&6; } +if test ${ac_cv_lib_curses_wattr_off+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef wattr_off + void *x=wattr_off + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_wattr_off=yes +else case e in #( + e) ac_cv_lib_curses_wattr_off=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_wattr_off" >&5 +printf "%s\n" "$ac_cv_lib_curses_wattr_off" >&6; } + if test "x$ac_cv_lib_curses_wattr_off" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_WATTR_OFF 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function wcolor_set" >&5 +printf %s "checking for curses function wcolor_set... " >&6; } +if test ${ac_cv_lib_curses_wcolor_set+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef wcolor_set + void *x=wcolor_set + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_wcolor_set=yes +else case e in #( + e) ac_cv_lib_curses_wcolor_set=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_wcolor_set" >&5 +printf "%s\n" "$ac_cv_lib_curses_wcolor_set" >&6; } + if test "x$ac_cv_lib_curses_wcolor_set" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_WCOLOR_SET 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function slk_attr_on" >&5 +printf %s "checking for curses function slk_attr_on... " >&6; } +if test ${ac_cv_lib_curses_slk_attr_on+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef slk_attr_on + void *x=slk_attr_on + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_slk_attr_on=yes +else case e in #( + e) ac_cv_lib_curses_slk_attr_on=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_slk_attr_on" >&5 +printf "%s\n" "$ac_cv_lib_curses_slk_attr_on" >&6; } + if test "x$ac_cv_lib_curses_slk_attr_on" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_SLK_ATTR_ON 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function slk_attr_off" >&5 +printf %s "checking for curses function slk_attr_off... " >&6; } +if test ${ac_cv_lib_curses_slk_attr_off+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef slk_attr_off + void *x=slk_attr_off + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_slk_attr_off=yes +else case e in #( + e) ac_cv_lib_curses_slk_attr_off=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_slk_attr_off" >&5 +printf "%s\n" "$ac_cv_lib_curses_slk_attr_off" >&6; } + if test "x$ac_cv_lib_curses_slk_attr_off" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_SLK_ATTR_OFF 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function slk_attr_set" >&5 +printf %s "checking for curses function slk_attr_set... " >&6; } +if test ${ac_cv_lib_curses_slk_attr_set+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef slk_attr_set + void *x=slk_attr_set + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_slk_attr_set=yes +else case e in #( + e) ac_cv_lib_curses_slk_attr_set=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_slk_attr_set" >&5 +printf "%s\n" "$ac_cv_lib_curses_slk_attr_set" >&6; } + if test "x$ac_cv_lib_curses_slk_attr_set" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_SLK_ATTR_SET 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function slk_color" >&5 +printf %s "checking for curses function slk_color... " >&6; } +if test ${ac_cv_lib_curses_slk_color+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef slk_color + void *x=slk_color + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_slk_color=yes +else case e in #( + e) ac_cv_lib_curses_slk_color=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_slk_color" >&5 +printf "%s\n" "$ac_cv_lib_curses_slk_color" >&6; } + if test "x$ac_cv_lib_curses_slk_color" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_SLK_COLOR 1" >>confdefs.h + +fi + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses variable ESCDELAY" >&5 printf %s "checking for curses variable ESCDELAY... " >&6; } if test ${ac_cv_lib_curses_ESCDELAY+y} @@ -32621,6 +33161,66 @@ fi + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function scr_set" >&5 +printf %s "checking for curses function scr_set... " >&6; } +if test ${ac_cv_lib_curses_scr_set+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ + + #ifndef scr_set + void *x=scr_set + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_scr_set=yes +else case e in #( + e) ac_cv_lib_curses_scr_set=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_scr_set" >&5 +printf "%s\n" "$ac_cv_lib_curses_scr_set" >&6; } + if test "x$ac_cv_lib_curses_scr_set" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_SCR_SET 1" >>confdefs.h + +fi + + + CPPFLAGS=$ac_save_cppflags fi diff --git a/configure.ac b/configure.ac index 97d2091dbf89a70..980ed2af50b13f7 100644 --- a/configure.ac +++ b/configure.ac @@ -7345,6 +7345,17 @@ PY_CHECK_CURSES_FUNC([define_key]) PY_CHECK_CURSES_FUNC([keyok]) PY_CHECK_CURSES_FUNC([set_escdelay]) PY_CHECK_CURSES_FUNC([set_tabsize]) +dnl The X/Open attr_t and soft-label attribute functions are absent on old +dnl SVr4 curses (e.g. illumos). +PY_CHECK_CURSES_FUNC([wattr_get]) +PY_CHECK_CURSES_FUNC([wattr_set]) +PY_CHECK_CURSES_FUNC([wattr_on]) +PY_CHECK_CURSES_FUNC([wattr_off]) +PY_CHECK_CURSES_FUNC([wcolor_set]) +PY_CHECK_CURSES_FUNC([slk_attr_on]) +PY_CHECK_CURSES_FUNC([slk_attr_off]) +PY_CHECK_CURSES_FUNC([slk_attr_set]) +PY_CHECK_CURSES_FUNC([slk_color]) PY_CHECK_CURSES_VAR([ESCDELAY]) PY_CHECK_CURSES_VAR([TABSIZE]) @@ -7360,9 +7371,10 @@ AS_VAR_IF([ac_cv_lib_curses_getmouse], [yes], [AC_DEFINE([HAVE_CURSES_GETMOUSE], [1], [Define if you have the 'getmouse' function with the X/Open signature.])]) -dnl scr_dump and its companions scr_restore/scr_init/scr_set are an inseparable -dnl group; probing scr_dump alone gates the whole family. +dnl scr_dump gates scr_dump/scr_restore/scr_init; scr_set is separate, since +dnl old SVr4 curses (e.g. illumos) has the former but not scr_set. PY_CHECK_CURSES_FUNC([scr_dump]) +PY_CHECK_CURSES_FUNC([scr_set]) CPPFLAGS=$ac_save_cppflags ])dnl have_curses != no ])dnl save env diff --git a/pyconfig.h.in b/pyconfig.h.in index 2f9b2140d9f191a..e259eda7439761e 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -257,12 +257,27 @@ /* Define if you have the 'scr_dump' function. */ #undef HAVE_CURSES_SCR_DUMP +/* Define if you have the 'scr_set' function. */ +#undef HAVE_CURSES_SCR_SET + /* Define if you have the 'set_escdelay' function. */ #undef HAVE_CURSES_SET_ESCDELAY /* Define if you have the 'set_tabsize' function. */ #undef HAVE_CURSES_SET_TABSIZE +/* Define if you have the 'slk_attr_off' function. */ +#undef HAVE_CURSES_SLK_ATTR_OFF + +/* Define if you have the 'slk_attr_on' function. */ +#undef HAVE_CURSES_SLK_ATTR_ON + +/* Define if you have the 'slk_attr_set' function. */ +#undef HAVE_CURSES_SLK_ATTR_SET + +/* Define if you have the 'slk_color' function. */ +#undef HAVE_CURSES_SLK_COLOR + /* Define if you have the 'syncok' function. */ #undef HAVE_CURSES_SYNCOK @@ -284,9 +299,24 @@ /* Define if you have the 'use_window' function. */ #undef HAVE_CURSES_USE_WINDOW +/* Define if you have the 'wattr_get' function. */ +#undef HAVE_CURSES_WATTR_GET + +/* Define if you have the 'wattr_off' function. */ +#undef HAVE_CURSES_WATTR_OFF + +/* Define if you have the 'wattr_on' function. */ +#undef HAVE_CURSES_WATTR_ON + +/* Define if you have the 'wattr_set' function. */ +#undef HAVE_CURSES_WATTR_SET + /* Define if you have the 'wchgat' function. */ #undef HAVE_CURSES_WCHGAT +/* Define if you have the 'wcolor_set' function. */ +#undef HAVE_CURSES_WCOLOR_SET + /* Define to 1 if you have the header file. */ #undef HAVE_DB_H From b8ec956716c183430a93929e6415ceed74089af1 Mon Sep 17 00:00:00 2001 From: Ajob Kustra Date: Sun, 19 Jul 2026 14:29:17 +0200 Subject: [PATCH 05/16] urllib: Add tests for HTTP errors to complete coverage (#154102) * add test for httperror props such as reason and fp, and stringified urlerror test * rm unnecessary 'reason' attr test, change url to filename and add reason and headers attr * separate file pointer test * prevent resource warning, close httperror exception * exc > err --- Lib/test/test_urllib.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 2dd739b77b8e4d1..1e5f79998e7cab2 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -467,6 +467,25 @@ def test_redirect_limit_independent(self): finally: self.unfakehttp() + def test_http_error_attribute_values(self): + hdrs = { + "Authorization": "Bearer foobar", + "Accept": "application/json" + } + err = urllib.error.HTTPError("http://something", 404, "foo", hdrs, None) + self.assertEqual(err.filename, "http://something") + self.assertEqual(err.code, 404) + self.assertEqual(err.msg, "foo") + self.assertEqual(err.reason, "foo") + self.assertEqual(err.hdrs, hdrs) + self.assertEqual(err.headers, hdrs) + err.close() + + def test_http_error_default_fp(self): + err = urllib.error.HTTPError("http://something", 404, "foo", {}, None) + self.assertIsInstance(err.fp, io.BytesIO) + err.close() + def test_empty_socket(self): # urlopen() raises OSError if the underlying socket does not send any # data. (#1680230) @@ -513,6 +532,11 @@ def test_ftp_nonexisting(self): self.assertFalse(e.exception.filename) self.assertTrue(e.exception.reason) + def test_url_error_stringified(self): + reason = 'sixseven' + err = urllib.error.URLError(reason) + self.assertEqual(str(err), f'') + class urlopen_DataTests(unittest.TestCase): """Test urlopen() opening a data URL.""" From 1530b38c82f8f09c3c29b2829cd1d8c1db830761 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sun, 19 Jul 2026 16:10:47 +0300 Subject: [PATCH 06/16] gh-151949: Fix Sphinx reference warnings in `Doc/library/lzma.rst` (GH-153878) The lzma module constants (FORMAT_*, CHECK_*, PRESET_*, FILTER_*, MODE_* and MF_*) were referenced with the :const: role throughout the module documentation but were never defined as reference targets, producing "reference target not found" warnings under nitpicky mode. Document these public constants with .. data:: directives, following the convention used by the signal, socket and ssl modules, so the existing references resolve. The now-redundant inline descriptions of the format and check constants are condensed into linked references. --- Doc/library/lzma.rst | 131 ++++++++++++++++++++++++++++++++++--------- Doc/tools/.nitignore | 1 - 2 files changed, 105 insertions(+), 27 deletions(-) diff --git a/Doc/library/lzma.rst b/Doc/library/lzma.rst index cd72174d54f6e62..580829cfbb46e07 100644 --- a/Doc/library/lzma.rst +++ b/Doc/library/lzma.rst @@ -152,35 +152,14 @@ Compressing and decompressing data in memory :func:`compress`. The *format* argument specifies what container format should be used. - Possible values are: - - * :const:`FORMAT_XZ`: The ``.xz`` container format. - This is the default format. - - * :const:`FORMAT_ALONE`: The legacy ``.lzma`` container format. - This format is more limited than ``.xz`` -- it does not support integrity - checks or multiple filters. - - * :const:`FORMAT_RAW`: A raw data stream, not using any container format. - This format specifier does not support integrity checks, and requires that - you always specify a custom filter chain (for both compression and - decompression). Additionally, data compressed in this manner cannot be - decompressed using :const:`FORMAT_AUTO` (see :class:`LZMADecompressor`). + Possible values are :const:`FORMAT_XZ` (the default), + :const:`FORMAT_ALONE` and :const:`FORMAT_RAW`. The *check* argument specifies the type of integrity check to include in the compressed data. This check is used when decompressing, to ensure that the - data has not been corrupted. Possible values are: - - * :const:`CHECK_NONE`: No integrity check. - This is the default (and the only acceptable value) for - :const:`FORMAT_ALONE` and :const:`FORMAT_RAW`. - - * :const:`CHECK_CRC32`: 32-bit Cyclic Redundancy Check. - - * :const:`CHECK_CRC64`: 64-bit Cyclic Redundancy Check. - This is the default for :const:`FORMAT_XZ`. - - * :const:`CHECK_SHA256`: 256-bit Secure Hash Algorithm. + data has not been corrupted. Possible values are :const:`CHECK_NONE`, + :const:`CHECK_CRC32`, :const:`CHECK_CRC64` (the default for + :const:`FORMAT_XZ`) and :const:`CHECK_SHA256`. If the specified check is not supported, an :class:`LZMAError` is raised. @@ -412,6 +391,106 @@ These filters support one option, ``start_offset``. This specifies the address that should be mapped to the beginning of the input data. The default is 0. +Constants +--------- + +The following module-level constants are provided for use as the *format*, +*check*, *preset* and *filters* arguments of the classes and functions above. + +Container formats: + +.. data:: FORMAT_XZ + + The ``.xz`` container format. + +.. data:: FORMAT_ALONE + + The legacy ``.lzma`` container format. This format is more limited than + ``.xz`` -- it does not support integrity checks or multiple filters. + +.. data:: FORMAT_RAW + + A raw data stream, not using any container format. This format specifier + does not support integrity checks, and requires that you always specify a + custom filter chain (for both compression and decompression). Additionally, + data compressed in this manner cannot be decompressed using + :const:`FORMAT_AUTO`. + +.. data:: FORMAT_AUTO + + Used for decompression only. The container format is detected + automatically, so that both ``.xz`` and ``.lzma`` files can be decompressed. + +Integrity checks: + +.. data:: CHECK_NONE + + No integrity check. This is the default (and the only acceptable value) for + :const:`FORMAT_ALONE` and :const:`FORMAT_RAW`. + +.. data:: CHECK_CRC32 + + A 32-bit Cyclic Redundancy Check. + +.. data:: CHECK_CRC64 + + A 64-bit Cyclic Redundancy Check. This is the default for + :const:`FORMAT_XZ`. + +.. data:: CHECK_SHA256 + + A 256-bit Secure Hash Algorithm. + +.. data:: CHECK_UNKNOWN + + The integrity check used by a stream could not yet be determined. This may + be the value of the :attr:`LZMADecompressor.check` attribute until enough of + the input has been decoded. + +.. data:: CHECK_ID_MAX + + The largest supported integrity-check ID. + +Compression presets: + +.. data:: PRESET_DEFAULT + + The default compression preset, equivalent to preset level ``6``. + +.. data:: PRESET_EXTREME + + A flag that may be bitwise OR-ed with a preset level (``0`` to ``9``) to + select a slower but more thorough variant of that preset. + +Filter IDs and options: + +.. data:: FILTER_LZMA1 + FILTER_LZMA2 + + The LZMA1 and LZMA2 compression filters. :const:`FILTER_LZMA1` is for use + with :const:`FORMAT_ALONE`, while :const:`FILTER_LZMA2` is for use with + :const:`FORMAT_XZ` and :const:`FORMAT_RAW`. + +.. data:: FILTER_DELTA + + The delta filter. + +.. data:: MODE_FAST + MODE_NORMAL + + Compression modes that may be used as the ``mode`` option of a filter + specifier (see :ref:`filter-chain-specs`). + +.. data:: MF_HC3 + MF_HC4 + MF_BT2 + MF_BT3 + MF_BT4 + + Match finders that may be used as the ``mf`` option of a filter specifier + (see :ref:`filter-chain-specs`). + + Examples -------- diff --git a/Doc/tools/.nitignore b/Doc/tools/.nitignore index c5d474895966119..ab592cfa5a1bbbd 100644 --- a/Doc/tools/.nitignore +++ b/Doc/tools/.nitignore @@ -12,7 +12,6 @@ Doc/library/email.parser.rst Doc/library/importlib.rst Doc/library/logging.config.rst Doc/library/logging.handlers.rst -Doc/library/lzma.rst Doc/library/mmap.rst Doc/library/multiprocessing.rst Doc/library/optparse.rst From ee1e48ebd8194cd5984714021c4df85dbb93c6df Mon Sep 17 00:00:00 2001 From: David Date: Sun, 19 Jul 2026 15:19:00 +0200 Subject: [PATCH 07/16] improve json test coverage (GH-154123) Add test that covers the except block on line 106 of decoder.py --- Lib/test/test_json/test_decode.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_json/test_decode.py b/Lib/test/test_json/test_decode.py index 6d05ef8f8eb0918..33aeefa92f282ae 100644 --- a/Lib/test/test_json/test_decode.py +++ b/Lib/test/test_json/test_decode.py @@ -149,6 +149,10 @@ def test_negative_index(self): d = self.json.JSONDecoder() self.assertRaises(ValueError, d.raw_decode, 'a'*42, -50000) + def test_unterminated_string(self): + d = self.json.JSONDecoder() + self.assertRaises(self.JSONDecodeError, d.raw_decode, '"\\') + def test_limit_int(self): maxdigits = 5000 with support.adjust_int_max_str_digits(maxdigits): From ec1057ebb6b3d515bff058af2feebf2868aadd42 Mon Sep 17 00:00:00 2001 From: thexai <58434170+thexai@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:19:40 +0200 Subject: [PATCH 08/16] gh-152433: Windows: ``_uuid`` module: improve UWP compatibility (#152793) --- Modules/_uuidmodule.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index c31a7e8fea56084..3edc29a75b32ce9 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -20,6 +20,9 @@ #ifdef MS_WINDOWS #include +#ifndef RPC_S_OK +#define RPC_S_OK 0L +#endif #endif #ifndef MS_WINDOWS From 1604043caf0a00fe4ea9f6e31e389c4cf8b8baab Mon Sep 17 00:00:00 2001 From: "L. Le" Date: Sun, 19 Jul 2026 15:20:56 +0200 Subject: [PATCH 09/16] gh-153932: protect read of en_index during enumerate.reduce (GH-154118) --- Lib/test/libregrtest/tsan.py | 1 + Lib/test/test_enumerate.py | 26 +++++++++++++++++++ ...-07-19-12-33-27.gh-issue-153932.lKw2bo.rst | 2 ++ Objects/enumobject.c | 9 ++++--- 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-12-33-27.gh-issue-153932.lKw2bo.rst diff --git a/Lib/test/libregrtest/tsan.py b/Lib/test/libregrtest/tsan.py index bacfe5e21ba0b7d..c7ec63763bc9d85 100644 --- a/Lib/test/libregrtest/tsan.py +++ b/Lib/test/libregrtest/tsan.py @@ -8,6 +8,7 @@ 'test_ctypes', 'test_concurrent_futures', 'test_enum', + 'test_enumerate', 'test_functools', 'test_httpservers', 'test_imaplib', diff --git a/Lib/test/test_enumerate.py b/Lib/test/test_enumerate.py index 5cb54cff9b76fdd..c8b85fe86921781 100644 --- a/Lib/test/test_enumerate.py +++ b/Lib/test/test_enumerate.py @@ -3,8 +3,11 @@ import sys import pickle import gc +import threading + from test import support +from test.support import threading_helper class G: 'Sequence using __getitem__' @@ -292,5 +295,28 @@ def enum(self, iterable, start=sys.maxsize + 1): (sys.maxsize+3,'c')] +@threading_helper.requires_working_threading() +class TestThreadSafety(EnumerateStartTestCase): + def test_thread_safety_while_iterating(self): + # gh-153932: calling reduce while iterating should pass with TSAN + + en = enumerate(range(10_000)) + stop = threading.Event() + + def advance(): + for _ in en: + pass + stop.set() + + def read(): + while not stop.is_set(): + en.__reduce__() + + threads = [threading.Thread(target=advance), threading.Thread(target=read)] + + with threading_helper.start_threads(threads): + pass + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-12-33-27.gh-issue-153932.lKw2bo.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-12-33-27.gh-issue-153932.lKw2bo.rst new file mode 100644 index 000000000000000..56b96307ed04cd9 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-12-33-27.gh-issue-153932.lKw2bo.rst @@ -0,0 +1,2 @@ +Fix thread safety issue in the ``__reduce__`` method of +:py:class:`enumerate`. diff --git a/Objects/enumobject.c b/Objects/enumobject.c index fc53f1bfee8dde4..68aa594c5540cee 100644 --- a/Objects/enumobject.c +++ b/Objects/enumobject.c @@ -277,10 +277,13 @@ enum_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) enumobject *en = _enumobject_CAST(op); PyObject *result; Py_BEGIN_CRITICAL_SECTION(en); - if (en->en_longindex != NULL) + if (en->en_longindex != NULL) { result = Py_BuildValue("O(OO)", Py_TYPE(en), en->en_sit, en->en_longindex); - else - result = Py_BuildValue("O(On)", Py_TYPE(en), en->en_sit, en->en_index); + } + else { + Py_ssize_t en_index = FT_ATOMIC_LOAD_SSIZE_RELAXED(en->en_index); + result = Py_BuildValue("O(On)", Py_TYPE(en), en->en_sit, en_index); + } Py_END_CRITICAL_SECTION(); return result; } From 69d44f80b55633baabe1d53b6c9a629348a93dca Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:45:17 +0200 Subject: [PATCH 10/16] gh-153906: Show 'Data' in code formatting (#154132) --- Lib/pydoc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 3140723f49b7d0d..72974af26bee64c 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -881,9 +881,10 @@ def docmodule(self, object, name=None, mod=None, *ignored): if data: contents = [] for key, value in data: - contents.append(self.document(value, key)) + contents.append( + f'
{self.document(value, key)}
') result = result + self.bigsection( - 'Data', 'data', '
\n'.join(contents)) + 'Data', 'data', '\n'.join(contents)) if hasattr(object, '__author__'): contents = self.markup(str(object.__author__), self.preformat) result = result + self.bigsection('Author', 'author', contents) From 8d11eb0c31efcfd1d504650a564d9a4b45cba721 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 19 Jul 2026 18:06:48 +0300 Subject: [PATCH 11/16] gh-154144: Fix false positive from the stdbool.h guard in _testcapimodule.c (GH-154145) Co-authored-by: Claude Fable 5 --- .../Tests/2026-07-19-18-20-00.gh-issue-154144.tcbool.rst | 1 + Modules/_testcapimodule.c | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2026-07-19-18-20-00.gh-issue-154144.tcbool.rst diff --git a/Misc/NEWS.d/next/Tests/2026-07-19-18-20-00.gh-issue-154144.tcbool.rst b/Misc/NEWS.d/next/Tests/2026-07-19-18-20-00.gh-issue-154144.tcbool.rst new file mode 100644 index 000000000000000..dc1ac8b4035153c --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-07-19-18-20-00.gh-issue-154144.tcbool.rst @@ -0,0 +1 @@ +Fix building the :mod:`!_testcapi` module on NetBSD. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 799390c27053287..fb18a866e628128 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -15,6 +15,10 @@ #include "frameobject.h" // PyFrame_New() #include "marshal.h" // PyMarshal_WriteLongToFile() +#ifdef bool +# error "The public headers should not include , see gh-90904" +#endif + #include // FLT_MAX #include #include // offsetof() @@ -26,10 +30,6 @@ # include // sysctlbyname() #endif -#ifdef bool -# error "The public headers should not include , see gh-48924" -#endif - #include "_testcapi/util.h" From 249d5e0a10839f9891caa85de35a8a9cf5d7a3a3 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 17:12:07 +0200 Subject: [PATCH 12/16] gh-153903: Use `@ctypes.util.wrap_dll_function()` (#154122) --- Lib/platform.py | 12 ++++++--- Lib/test/pythoninfo.py | 17 ++++++------ Lib/test/test_android.py | 13 ++++++--- 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, 95 insertions(+), 57 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index 36489d4fdd98ae2..3141998e02dfe21 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -543,21 +543,25 @@ 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 else: # 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") + + @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 3227b91bd82a863..13a3199b1f1267b 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: @@ -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 31daafbc3d63009..201d701aff11eb2 100644 --- a/Lib/test/test_android.py +++ b/Lib/test/test_android.py @@ -41,13 +41,18 @@ 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 + from ctypes.util import wrap_dll_function + liblog = CDLL("liblog.so") + + @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 40036609a190551..1ff600e30bf4cbd 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 # noqa: F811 - 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 a3728b58335e63b..936332bf94ffe77 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 f1c6283f60d35ed..b21dd8a4dca6609 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 e7779b9363503b4..a1c7c42c109330b 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 eb447081ed8182f825fa01953ec87cff41f3d585 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 19 Jul 2026 18:20:35 +0300 Subject: [PATCH 13/16] gh-139806: Mention pickle error changes in What's New in 3.14 (GH-154020) The gh-122311 changes made pickle.dump() and pickle.dumps() raise PicklingError for some failures that previously raised AttributeError, ImportError, ValueError or UnicodeEncodeError, depending on the implementation. Add an entry about this in the "Porting to Python 3.14" section. Co-authored-by: Claude Fable 5 --- Doc/whatsnew/3.14.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 2fa7267292d9940..64c6888d9947702 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -3312,6 +3312,16 @@ Changes in the Python API This temporary change affects other threads. (Contributed by Serhiy Storchaka in :gh:`69998`.) +* :func:`pickle.dump` and :func:`pickle.dumps` now raise + :exc:`~pickle.PicklingError` for some failures that previously raised + :exc:`AttributeError`, :exc:`ImportError`, :exc:`ValueError`, + :exc:`UnicodeEncodeError` or :exc:`!PicklingError`, + depending on the implementation + (for example, pickling a local object, or an object whose module cannot be imported). + The original exception is chained to the :exc:`!PicklingError`. + Code that caught these exceptions should also catch :exc:`!PicklingError`. + (Contributed by Serhiy Storchaka in :gh:`122311`.) + * :class:`types.UnionType` is now an alias for :class:`typing.Union`, causing changes in some behaviors. See :ref:`above ` for more details. From 5625b187ab528e10dcaa21726a7c95b110d97190 Mon Sep 17 00:00:00 2001 From: soreavis <263610811+soreavis@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:31:59 +0200 Subject: [PATCH 14/16] gh-62534: Document that three-argument type() does not call __prepare__ (GH-154028) The three-argument form of type() skips the metaclass __prepare__ method, which is called by the class statement machinery rather than by the metaclass call itself. Say so in the type() entry and point to types.new_class() for dynamic class creation with the appropriate metaclass, as directed in the issue thread. --- Doc/library/functions.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 8dbebf7048b5835..f45ab397e936938 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -2179,6 +2179,11 @@ are always available. They are listed here in alphabetical order. in the same way that keywords in a class definition (besides *metaclass*) would. + Unlike a :keyword:`class` statement, the three argument form does not + call the metaclass ``__prepare__`` method (see :ref:`prepare`). Use + :func:`types.new_class` to dynamically create a class using the + appropriate metaclass. + See also :ref:`class-customization`. .. versionchanged:: 3.6 From f2521324e6bdf331460abec7c2a88e2d3736b91c Mon Sep 17 00:00:00 2001 From: Tobias Alex-Petersen Date: Sun, 19 Jul 2026 15:58:58 +0000 Subject: [PATCH 15/16] gh-119710: fix asyncio Process.wait() to finish on process exit and not wait for closing of pipes (#151983) Co-authored-by: Kumar Aditya --- Lib/asyncio/base_subprocess.py | 25 +++---- Lib/test/test_asyncio/test_subprocess.py | 71 +++++++++++-------- ...-06-11-10-00-00.gh-issue-119710.Qz7Kp2.rst | 4 ++ 3 files changed, 53 insertions(+), 47 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-11-10-00-00.gh-issue-119710.Qz7Kp2.rst diff --git a/Lib/asyncio/base_subprocess.py b/Lib/asyncio/base_subprocess.py index 224b1883808a412..3fa1ed1afd2a651 100644 --- a/Lib/asyncio/base_subprocess.py +++ b/Lib/asyncio/base_subprocess.py @@ -26,7 +26,6 @@ def __init__(self, loop, protocol, args, shell, self._pending_calls = collections.deque() self._pipes = {} self._finished = False - self._pipes_connected = False if stdin == subprocess.PIPE: self._pipes[0] = None @@ -214,7 +213,6 @@ async def _connect_pipes(self, waiter): else: if waiter is not None and not waiter.cancelled(): waiter.set_result(None) - self._pipes_connected = True def _call(self, cb, *data): if self._pending_calls is not None: @@ -235,6 +233,7 @@ def _process_exited(self, returncode): if self._loop.get_debug(): logger.info('%r exited with return code %r', self, returncode) self._returncode = returncode + if self._proc.returncode is None: # asyncio uses a child watcher: copy the status into the Popen # object. On Python 3.6, it is required to avoid a ResourceWarning. @@ -243,6 +242,13 @@ def _process_exited(self, returncode): self._try_finish() + # gh-119710: Wake up futures waiting for wait() as soon as the process + # exits. + for waiter in self._exit_waiters: + if not waiter.done(): + waiter.set_result(returncode) + self._exit_waiters = None + async def _wait(self): """Wait until the process exit and return the process return code. @@ -258,15 +264,7 @@ def _try_finish(self): assert not self._finished if self._returncode is None: return - if not self._pipes_connected: - # self._pipes_connected can be False if not all pipes were connected - # because either the process failed to start or the self._connect_pipes task - # got cancelled. In this broken state we consider all pipes disconnected and - # to avoid hanging forever in self._wait as otherwise _exit_waiters - # would never be woken up, we wake them up here. - for waiter in self._exit_waiters: - if not waiter.done(): - waiter.set_result(self._returncode) + if all(p is not None and p.disconnected for p in self._pipes.values()): self._finished = True @@ -276,11 +274,6 @@ def _call_connection_lost(self, exc): try: self._protocol.connection_lost(exc) finally: - # wake up futures waiting for wait() - for waiter in self._exit_waiters: - if not waiter.done(): - waiter.set_result(self._returncode) - self._exit_waiters = None self._loop = None self._proc = None self._protocol = None diff --git a/Lib/test/test_asyncio/test_subprocess.py b/Lib/test/test_asyncio/test_subprocess.py index bb1a7d6ff22be8d..2e97fa5820f8245 100644 --- a/Lib/test/test_asyncio/test_subprocess.py +++ b/Lib/test/test_asyncio/test_subprocess.py @@ -112,37 +112,6 @@ def test_subprocess_repr(self): ) transport.close() - def test_proc_exited_no_invalid_state_error_on_exit_waiters(self): - # gh-145541: when _connect_pipes hasn't completed (so - # _pipes_connected is False) and the process exits, _try_finish() - # sets the result on exit waiters. Then _call_connection_lost() must - # not call set_result() again on the same waiters. - self.loop.set_exception_handler( - lambda loop, context: self.fail( - f"unexpected exception: {context}") - ) - waiter = self.loop.create_future() - transport, protocol = self.create_transport(waiter) - - # Simulate a waiter registered via _wait() before the process exits. - exit_waiter = self.loop.create_future() - transport._exit_waiters.append(exit_waiter) - - # _connect_pipes hasn't completed, so _pipes_connected is False. - self.assertFalse(transport._pipes_connected) - - # Simulate process exit. _try_finish() will set the result on - # exit_waiter because _pipes_connected is False, and then schedule - # _call_connection_lost() because _pipes is empty (vacuously all - # disconnected). _call_connection_lost() must skip exit_waiter - # because it's already done. - transport._process_exited(6) - self.loop.run_until_complete(waiter) - - self.assertEqual(exit_waiter.result(), 6) - - transport.close() - class SubprocessMixin: @@ -436,6 +405,46 @@ async def len_message(message): self.assertEqual(output.rstrip(), b'3') self.assertEqual(exitcode, 0) + def test_wait_even_if_pipe_is_open(self): + # gh-119710: Process.wait() must return once the process exits even + # if its stdout pipe is inherited by a grandchild that keeps it open, + # so the pipe never reaches EOF. Otherwise wait() hangs forever + # despite the returncode being known. + + async def run(): + # The grandchild inherits the child's stdin and stdout pipes and + # keeps both open after the child is killed. It writes "ready" + # so we know it has started, and exits once its stdin hits EOF. + code = textwrap.dedent("""\ + import subprocess, sys + subprocess.run([sys.executable, "-c", + "import sys; sys.stdout.write('ready');" + " sys.stdout.flush(); sys.stdin.read()"]) + """) + + proc = await asyncio.create_subprocess_exec( + sys.executable, "-c", code, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + try: + wait_proc = asyncio.create_task(proc.wait()) + # Wait until the grandchild holds the inherited pipes; this + # also lets the wait() task register its waiter. + await proc.stdout.readexactly(5) + proc.kill() + returncode = await asyncio.wait_for( + wait_proc, timeout=support.SHORT_TIMEOUT) + if sys.platform == 'win32': + self.assertIsInstance(returncode, int) + else: + self.assertEqual(-signal.SIGKILL, returncode) + finally: + proc.stdin.close() # let the grandchild exit + await proc.stdout.read() + + self.loop.run_until_complete(run()) + def test_empty_input(self): async def empty_input(): diff --git a/Misc/NEWS.d/next/Library/2026-06-11-10-00-00.gh-issue-119710.Qz7Kp2.rst b/Misc/NEWS.d/next/Library/2026-06-11-10-00-00.gh-issue-119710.Qz7Kp2.rst new file mode 100644 index 000000000000000..45f99d72ea29a2f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-11-10-00-00.gh-issue-119710.Qz7Kp2.rst @@ -0,0 +1,4 @@ +Fix :mod:`asyncio` subprocess :meth:`~asyncio.subprocess.Process.wait` +hanging when the process has exited but one of its pipes is kept open by an +inherited child process (so the pipe never reaches EOF). ``wait()`` now +returns as soon as the process exits, regardless of the pipes' state. From da5713c489c63622c0fdd25343a7ea7293a66e68 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 18:24:12 +0200 Subject: [PATCH 16/16] gh-104533: Fix ctypes test_pointer_proto_missing_argtypes_error() (#154169) Save/restore Py_GetVersion.argtypes, since Py_GetVersion is used in another test. --- Lib/test/test_ctypes/test_pointers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_ctypes/test_pointers.py b/Lib/test/test_ctypes/test_pointers.py index 771cc8fbe0ec934..78480af34474a54 100644 --- a/Lib/test/test_ctypes/test_pointers.py +++ b/Lib/test/test_ctypes/test_pointers.py @@ -11,6 +11,7 @@ c_long, c_ulong, c_longlong, c_ulonglong, c_float, c_double) from ctypes import _pointer_type_cache, _pointer_type_cache_fallback +from test import support from test.support import import_helper from weakref import WeakSet _ctypes_test = import_helper.import_module("_ctypes_test") @@ -409,10 +410,9 @@ class BadType(ctypes._Pointer): pass func = ctypes.pythonapi.Py_GetVersion - func.argtypes = (BadType,) - - with self.assertRaises(ctypes.ArgumentError): - func(object()) + with support.swap_attr(func, 'argtypes', (BadType,)): + with self.assertRaises(ctypes.ArgumentError): + func(object()) class PointerTypeCacheTestCase(unittest.TestCase): # dummy tests to check warnings and base behavior