diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 35f7fa62140bd49..7e85e08c4291a06 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -62,7 +62,7 @@ def _build_graph_for_future( coro = coro.cr_await elif hasattr(coro, 'ag_await'): # A native async generator or duck-type compatible iterator - st.append(FrameCallGraphEntry(coro.cr_frame)) + st.append(FrameCallGraphEntry(coro.ag_frame)) coro = coro.ag_await else: break diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 3dde7535284b255..928b618fe5c55b7 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -1,5 +1,6 @@ import asyncio import io +import sys import unittest from unittest import mock @@ -147,6 +148,31 @@ async def main(): 'async generator CallStackTestBase.test_stack_async_gen..gen()', stack_for_gen_nested_call[1]) + def test_ag_frame_used_for_async_generator(self): + # Regression test for gh-148736: the ag_await branch of + # _build_graph_for_future must read ag_frame, not cr_frame. + from asyncio.graph import _build_graph_for_future + + sentinel_frame = sys._getframe() + + class FakeAsyncGen: + ag_await = None + ag_frame = sentinel_frame + + class FakeCoro: + cr_frame = sentinel_frame + cr_await = FakeAsyncGen() + + loop = asyncio.new_event_loop() + try: + fut = loop.create_future() + fut.get_coro = lambda: FakeCoro() + result = _build_graph_for_future(fut) + finally: + loop.close() + + self.assertEqual(len(result.call_stack), 2) + async def test_stack_gather(self): stack_for_deep = None diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 17837c0219f88eb..ad5893e6754f68c 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -974,7 +974,7 @@ def test_read_from_window(self): with self.subTest(ch=ch): stdscr.addstr(2, 0, ch) self.assertEqual(stdscr.instr(2, 0, 1), b) - self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT, b[0]) + self.assertEqual(stdscr.inch(2, 0), b[0]) def test_coordinate_errors(self): # Addressing a cell outside the window raises curses.error. diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index a234d6b37ff3797..7bb50f7b8aa47e2 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -1787,6 +1787,23 @@ def testGetaddrinfo(self): except socket.gaierror: pass + @unittest.skipUnless(hasattr(socket, 'AI_NUMERICSERV'), + 'needs socket.AI_NUMERICSERV') + @support.thread_unsafe('setlocale is not thread-safe') + @support.run_with_locales('LC_ALL', + 'uk_UA.KOI8-U', 'uk_UA', 'ja_JP.eucJP', 'ja_JP.SJIS', 'ja_JP', + 'ko_KR.eucKR', 'zh_CN.GB18030', 'el_GR.ISO8859-7', + 'de_DE.ISO8859-1', 'ja_JP.UTF-8', + '') + def test_getaddrinfo_localized_error(self): + # gh-93251: the localized gai_strerror() message could fail to be + # decoded as UTF-8, so UnicodeDecodeError was raised + # instead of gaierror. + with self.assertRaises(socket.gaierror) as cm: + socket.getaddrinfo("localhost", "http", + flags=socket.AI_NUMERICSERV) + str(cm.exception) + @unittest.skipIf(_testcapi is None, "requires _testcapi") def test_getaddrinfo_int_port_overflow(self): # gh-74895: Test that getaddrinfo does not raise OverflowError on port. diff --git a/Misc/NEWS.d/next/Library/2026-04-18-12-00-00.gh-issue-148736.ag-frame.rst b/Misc/NEWS.d/next/Library/2026-04-18-12-00-00.gh-issue-148736.ag-frame.rst new file mode 100644 index 000000000000000..e9701dfaa6a48d5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-18-12-00-00.gh-issue-148736.ag-frame.rst @@ -0,0 +1,3 @@ +Fix a latent :exc:`AttributeError` in :mod:`asyncio` call-graph capture when +walking an async generator's ``ag_await`` chain: the walker referenced +``cr_frame`` instead of ``ag_frame``. diff --git a/Misc/NEWS.d/next/Library/2026-07-25-13-30-00.gh-issue-93251.gAiErr.rst b/Misc/NEWS.d/next/Library/2026-07-25-13-30-00.gh-issue-93251.gAiErr.rst new file mode 100644 index 000000000000000..3f313030802f875 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-25-13-30-00.gh-issue-93251.gAiErr.rst @@ -0,0 +1,4 @@ +Fix :exc:`UnicodeDecodeError` in :mod:`socket` functions +(such as :func:`~socket.getaddrinfo` and :func:`~socket.gethostbyaddr`) +when the localized error message of the C library is not UTF-8: +decode it from the locale encoding. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index f893c2080fc3f33..b2d745332317a36 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -776,30 +776,6 @@ curses_getcchar(const cchar_t *wcval, wchar_t *wstr, attr_t *attrs, int *pair) return rtn; } -/* winch() returns the low 8 bits of the character's code point with no locale - conversion, unlike instr(), so recover the locale byte from the wide cell - when the character maps to exactly one byte, keeping the attribute and color - bits in RTN. A character with no single-byte form is left to winch(). */ -static chtype -curses_cell_locale_byte(chtype rtn, const cchar_t *cell) -{ - wchar_t wstr[CCHARW_MAX + 1]; - attr_t attrs; - int pair; - if (curses_getcchar(cell, wstr, &attrs, &pair) == ERR - || wstr[0] == L'\0' || wstr[1] != L'\0') - { - return rtn; - } - /* wctob() mirrors ncurses' own _nc_to_char(): the single-byte form, or EOF - when the character has none in this locale. */ - int byte = wctob(wstr[0]); - if (byte != EOF) { - rtn = (rtn & ~(chtype)A_CHARTEXT) | (unsigned char)byte; - } - return rtn; -} - /* Hash one cell by value (text, attributes, pair) -- consistent with the equality comparison, not the raw cchar_t whose padding and unused text tail it ignores. Zero the key first so those bytes are deterministic, then @@ -3650,7 +3626,40 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, { chtype rtn; const char *funcname; - +#ifdef HAVE_NCURSESW + /* ncursesw's winch() returns the character's whole code point instead of + its locale byte, overflowing the chtype's 8-bit character field into the + color and attribute bits; read the wide cell and rebuild it instead. */ + cchar_t cell = {0}; + int rc; + if (!group_right_1) { + rc = win_wch(self->win, &cell); + funcname = "win_wch"; + } + else { + rc = mvwin_wch(self->win, y, x, &cell); + funcname = "mvwin_wch"; + } + if (rc == ERR) { + curses_window_set_error(self, funcname, "inch"); + return NULL; + } + wchar_t wstr[CCHARW_MAX + 1]; + attr_t attrs; + int pair; + if (curses_getcchar(&cell, wstr, &attrs, &pair) == ERR) { + curses_window_set_error(self, "getcchar", "inch"); + return NULL; + } + int byte = 0; + if (wstr[0] != L'\0' && wstr[1] == L'\0') { + byte = wctob(wstr[0]); + if (byte == EOF) { + byte = 0; + } + } + rtn = (chtype)byte | (attrs & ~(attr_t)A_COLOR) | COLOR_PAIR(pair); +#else if (!group_right_1) { rtn = winch(self->win); funcname = "winch"; @@ -3663,13 +3672,6 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, curses_window_set_error(self, funcname, "inch"); return NULL; } -#ifdef HAVE_NCURSESW - curses_cell_t cell = {0}; - if ((group_right_1 ? mvwin_wch(self->win, y, x, &cell) - : win_wch(self->win, &cell)) != ERR) - { - rtn = curses_cell_locale_byte(rtn, &cell); - } #endif return PyLong_FromUnsignedLong(rtn); } diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index 084c2dbcff066e5..73ae1c942daba48 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -745,6 +745,16 @@ set_error(void) } +#if defined(HAVE_HSTRERROR) || defined(HAVE_GAI_STRERROR) +/* Decode a locale-encoded error message from the C library. + It can be localized and use a non-UTF-8 encoding. */ +static PyObject * +decode_error_message(const char *str) +{ + return PyUnicode_DecodeLocale(str, "surrogateescape"); +} +#endif + #if defined(HAVE_GETHOSTBYNAME_R) || defined (HAVE_GETHOSTBYNAME) || defined (HAVE_GETHOSTBYADDR) static PyObject * set_herror(socket_state *state, int h_error) @@ -752,7 +762,7 @@ set_herror(socket_state *state, int h_error) PyObject *v; #ifdef HAVE_HSTRERROR - v = Py_BuildValue("(is)", h_error, hstrerror(h_error)); + v = Py_BuildValue("(iN)", h_error, decode_error_message(hstrerror(h_error))); #else v = Py_BuildValue("(is)", h_error, "host not found"); #endif @@ -779,7 +789,7 @@ set_gaierror(socket_state *state, int error) #endif #ifdef HAVE_GAI_STRERROR - v = Py_BuildValue("(is)", error, gai_strerror(error)); + v = Py_BuildValue("(iN)", error, decode_error_message(gai_strerror(error))); #else v = Py_BuildValue("(is)", error, "getaddrinfo failed"); #endif