From aad438b0b61206ff67f32aa1ca24792f26f6fff9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 23 Jul 2026 17:48:17 +0300 Subject: [PATCH 1/4] gh-154525: Fix curses getcchar() failure on ncurses older than 6.3 (GH-154526) getcchar() rejected a non-NULL opts argument before ncurses 6.3, so read the color pair through the short slot instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- Modules/_cursesmodule.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 6df593001b8a1cb..f893c2080fc3f33 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -763,7 +763,9 @@ curses_getcchar(const cchar_t *wcval, wchar_t *wstr, attr_t *attrs, int *pair) /* getcchar() is not guaranteed to write the text of an empty cell, so make the output an empty string by default. */ wstr[0] = L'\0'; -#if _NCURSES_EXTENDED_COLOR_FUNCS + /* getcchar()'s opts slot returns the extended color pair, but ncurses + returned ERR for a non-NULL opts until 6.3 (patch 20210116). */ +#if _NCURSES_EXTENDED_COLOR_FUNCS && NCURSES_VERSION_PATCH+0 >= 20210116 int rtn = getcchar(wcval, wstr, attrs, &spair, pair); #else int rtn = getcchar(wcval, wstr, attrs, &spair, NULL); From 22c797005d1eaa2b03bcd9d858c2525a1d1560c4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 23 Jul 2026 18:37:43 +0300 Subject: [PATCH 2/4] gh-154525: Skip ncurses find_pair()/alloc_pair() reuse checks before 6.3 (GH-154543) find_pair() and reuse in alloc_pair() were fixed in ncurses 6.3 (patch 20200411). On earlier versions find_pair() returns -1 and alloc_pair() allocates a fresh pair instead of reusing an equal one, so test_dynamic_color_pairs failed on ncurses 6.1 and 6.2. Co-authored-by: Claude Opus 4.8 (1M context) --- Lib/test/test_curses.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 4ca5dd73f55afee..8aecb1e3a7a42d5 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1997,12 +1997,15 @@ def test_dynamic_color_pairs(self): pair = curses.alloc_pair(fg, bg) self.assertGreater(pair, 0) self.assertEqual(curses.pair_content(pair), (fg, bg)) - # The same combination of colors reuses the same pair. - self.assertEqual(curses.alloc_pair(fg, bg), pair) - self.assertEqual(curses.find_pair(fg, bg), pair) - # Once freed, the pair is no longer found. - self.assertIsNone(curses.free_pair(pair)) - self.assertEqual(curses.find_pair(fg, bg), -1) + if getattr(curses, 'ncurses_version', (6, 3)) >= (6, 3): + # The same combination of colors reuses the same pair. + self.assertEqual(curses.alloc_pair(fg, bg), pair) + self.assertEqual(curses.find_pair(fg, bg), pair) + # Once freed, the pair is no longer found. + self.assertIsNone(curses.free_pair(pair)) + self.assertEqual(curses.find_pair(fg, bg), -1) + else: + self.assertIsNone(curses.free_pair(pair)) # Error paths. for color in self.bad_colors2(): From 0ca0107e4fc842b57ec12a39a51c9ce390d43c4b Mon Sep 17 00:00:00 2001 From: Jason G <41053218+gianghungtien@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:52:42 +0700 Subject: [PATCH 3/4] gh-69573: Check the return value of msvcrt._putch() and _putwch() (GH-154364) --- Doc/library/msvcrt.rst | 12 +++++- Lib/test/test_msvcrt.py | 37 +++++++++++++++++++ ...6-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst | 4 ++ PC/msvcrtmodule.c | 35 ++++++++++++++++-- 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst diff --git a/Doc/library/msvcrt.rst b/Doc/library/msvcrt.rst index 6b49c1a9ccd6e11..f4a1125554a0513 100644 --- a/Doc/library/msvcrt.rst +++ b/Doc/library/msvcrt.rst @@ -135,13 +135,23 @@ Console I/O .. function:: putch(char) - Print the byte string *char* to the console without buffering. + Print the byte string *char* to the console without buffering. Raises + :exc:`OSError` on failure, for example when the process has no console + attached. + + .. versionchanged:: next + Failures are now reported by raising :exc:`OSError` instead of being + silently ignored. .. function:: putwch(unicode_char) Wide char variant of :func:`putch`, accepting a Unicode value. + .. versionchanged:: next + Failures are now reported by raising :exc:`OSError` instead of being + silently ignored. + .. function:: ungetch(char) diff --git a/Lib/test/test_msvcrt.py b/Lib/test/test_msvcrt.py index fef86ce323e54d5..a588b0ff46014a1 100644 --- a/Lib/test/test_msvcrt.py +++ b/Lib/test/test_msvcrt.py @@ -61,6 +61,19 @@ def test_get_osfhandle(self): c_encoded = b'\x57\x5b' # utf-16-le (which windows internally used) encoded char for this CJK char +def has_console(): + # A process created without a console (for example by pythonw.exe, or with + # the DETACHED_PROCESS creation flag) cannot write to the console. + try: + with open('CONOUT$', 'w'): + return True + except OSError: + return False + + +requires_console = unittest.skipUnless(has_console(), 'requires a console') + + class TestConsoleIO(unittest.TestCase): # CREATE_NEW_CONSOLE creates a "popup" window. @requires_resource('gui') @@ -106,12 +119,36 @@ def test_getche(self): def test_getwche(self): self.check_getwch('getwche') + @requires_console def test_putch(self): msvcrt.putch(b'c') + @requires_console def test_putwch(self): msvcrt.putwch(c) + def test_putch_without_console(self): + # gh-69573: putch() and putwch() must report the error instead of + # silently ignoring it when the process has no console attached. + code = dedent(''' + import msvcrt + import sys + + for name, arg in (('putch', b'c'), ('putwch', 'c')): + func = getattr(msvcrt, name) + try: + func(arg) + except OSError: + pass + else: + sys.exit(f'msvcrt.{name}() did not raise OSError') + ''') + # DETACHED_PROCESS: the child process is created without a console. + proc = subprocess.run([sys.executable, '-c', code], + creationflags=subprocess.DETACHED_PROCESS, + capture_output=True, text=True) + self.assertEqual(proc.returncode, 0, proc.stderr) + class TestOther(unittest.TestCase): def test_heap_min(self): diff --git a/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst b/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst new file mode 100644 index 000000000000000..441718032cc0086 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst @@ -0,0 +1,4 @@ +:func:`msvcrt.putch` and :func:`msvcrt.putwch` now check the return value of +the underlying ``_putch()`` and ``_putwch()`` C functions and raise +:exc:`OSError` on failure, for example when the process has no console +attached, instead of silently ignoring the error. diff --git a/PC/msvcrtmodule.c b/PC/msvcrtmodule.c index 8826e7e85a7f5ab..02f16d41b1457b1 100644 --- a/PC/msvcrtmodule.c +++ b/PC/msvcrtmodule.c @@ -317,6 +317,22 @@ msvcrt_getwche_impl(PyObject *module) #endif /* MS_WINDOWS_DESKTOP */ +/* Raise an OSError for a failed _putch()/_putwch() call. + + These functions fail, for example, when the process has no console + attached, but the CRT reports the failure without setting errno (and + without setting the Windows last error either), so fall back to a + generic error message in that case. */ +static PyObject * +set_console_write_error(void) +{ + if (errno != 0) { + return PyErr_SetFromErrno(PyExc_OSError); + } + PyErr_SetString(PyExc_OSError, "write to console failed"); + return NULL; +} + /*[clinic input] msvcrt.putch @@ -330,9 +346,16 @@ static PyObject * msvcrt_putch_impl(PyObject *module, char char_value) /*[clinic end generated code: output=92ec9b81012d8f60 input=ec078dd10cb054d6]*/ { + int res; + _Py_BEGIN_SUPPRESS_IPH - _putch(char_value); + errno = 0; + res = _putch(char_value); _Py_END_SUPPRESS_IPH + + if (res == EOF) { + return set_console_write_error(); + } Py_RETURN_NONE; } @@ -351,11 +374,17 @@ static PyObject * msvcrt_putwch_impl(PyObject *module, int unicode_char) /*[clinic end generated code: output=a3bd1a8951d28eee input=996ccd0bbcbac4c3]*/ { + wint_t res; + _Py_BEGIN_SUPPRESS_IPH - _putwch(unicode_char); + errno = 0; + res = _putwch(unicode_char); _Py_END_SUPPRESS_IPH - Py_RETURN_NONE; + if (res == WEOF) { + return set_console_write_error(); + } + Py_RETURN_NONE; } #endif /* MS_WINDOWS_DESKTOP */ From 2b4e062a277f888c0139ac7196c2b6dcaed795dc Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Thu, 23 Jul 2026 12:57:55 -0400 Subject: [PATCH 4/4] gh-153903: Copy docstrings and other metadata in `ctypes.util.wrap_dll_function` (GH-154495) Co-authored-by: Peter Bierma --- Doc/library/ctypes.rst | 3 +-- Lib/ctypes/util.py | 2 ++ Lib/test/test_ctypes/test_funcptr.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 34c8636abaa925d..a4de7cb09cca9da 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -708,8 +708,7 @@ Specifying function pointers using type annotations @wrap_dll_function(dll_to_wrap) def function_ptr_name(arg_name: ctypes_type, ...) -> ctypes_type: - # There should be no body - pass + """Optional docstring. There should be no function body.""" The body of the decorated function is ignored, and any parameters that are missing type annotations are skipped. The names of the parameters are ignored diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index a73422598b2cd96..1f2b9cf0e3ce8dd 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -590,6 +590,8 @@ def decorator(func): ptr.restype = restype ptr.argtypes = tuple(annotations.values()) + functools.update_wrapper(ptr, func, updated=()) + return ptr return decorator diff --git a/Lib/test/test_ctypes/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py index 86699ad81098275..28ff34f9048454d 100644 --- a/Lib/test/test_ctypes/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -134,12 +134,14 @@ def test_abstract(self): def test_wrap_dll_function(self): @wrap_dll_function(ctypes.pythonapi) def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object) -> ctypes.py_object: + """Call the PythonAPI function underlying getattr""" pass class Foo: a = "abc" self.assertEqual(PyObject_GetAttr(Foo, "a"), "abc") + self.assertEqual(PyObject_GetAttr.__doc__, "Call the PythonAPI function underlying getattr") with self.assertRaises(AttributeError): @wrap_dll_function(ctypes.pythonapi)