From 6af6165c21ddc6adc6208d69606dbdcdbdb06418 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Wed, 29 Jul 2026 09:07:15 +0300 Subject: [PATCH 1/3] gh-154788: Make curses window.getparent() unconditional (GH-154789) getparent() calls no curses function, it returns the window's stored parent, but it was compiled only when the ncurses extension functions were present. Close the guard after getscrreg(), which does need it. The test moves out of test_state_getters, which is gated on is_scrollok(), so that getparent() is covered on backends without the extensions. --- Lib/test/test_curses.py | 12 +++++++++--- Modules/_cursesmodule.c | 4 +--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 9f7f8535b6d9a8..4e5d5f1da0be95 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -278,6 +278,14 @@ def test_subwindows_references(self): del win2 gc_collect() + def test_getparent(self): + # getparent() calls no curses function, so it works with any backend + # and is not gated like the is_*() getters below. + stdscr = self.stdscr + self.assertIsNone(stdscr.getparent()) + sub = stdscr.subwin(3, 3, 0, 0) + self.assertIs(sub.getparent(), stdscr) + def test_dupwin(self): win = curses.newwin(5, 10, 2, 3) win.addstr(0, 0, 'ABCDE') @@ -1771,13 +1779,11 @@ def test_state_getters(self): stdscr.setscrreg(5, 10) self.assertEqual(stdscr.getscrreg(), (5, 10)) - # is_pad()/is_subwin()/getparent(). + # is_pad()/is_subwin(). self.assertIs(stdscr.is_pad(), False) self.assertIs(stdscr.is_subwin(), False) - self.assertIsNone(stdscr.getparent()) sub = stdscr.subwin(3, 3, 0, 0) self.assertIs(sub.is_subwin(), True) - self.assertIs(sub.getparent(), stdscr) pad = curses.newpad(5, 5) self.assertIs(pad.is_pad(), True) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 01ea3c43cce2e6..a913a7a0babe35 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1936,6 +1936,7 @@ PyCursesWindow_getscrreg(PyObject *op, PyObject *Py_UNUSED(ignored)) } return Py_BuildValue("(ii)", top, bottom); } +#endif /* NCURSES_EXT_FUNCS >= 20110404 || PDCURSES */ static PyObject * PyCursesWindow_getparent(PyObject *op, PyObject *Py_UNUSED(ignored)) @@ -1948,7 +1949,6 @@ PyCursesWindow_getparent(PyObject *op, PyObject *Py_UNUSED(ignored)) } return Py_NewRef((PyObject *)self->orig); } -#endif /* NCURSES_EXT_FUNCS >= 20110404 || PDCURSES */ Window_NoArgNoReturnVoidFunction(wsyncup) Window_NoArgNoReturnVoidFunction(wsyncdown) @@ -5061,11 +5061,9 @@ static PyMethodDef PyCursesWindow_methods[] = { {"getmaxyx", PyCursesWindow_getmaxyx, METH_NOARGS, "getmaxyx($self, /)\n--\n\n" "Return a tuple (y, x) of the window height and width."}, -#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404) || defined(PDCURSES) {"getparent", PyCursesWindow_getparent, METH_NOARGS, "getparent($self, /)\n--\n\n" "Return the parent window, or None if this is not a subwindow."}, -#endif {"getparyx", PyCursesWindow_getparyx, METH_NOARGS, "getparyx($self, /)\n--\n\n" "Return (y, x) relative to the parent window, or (-1, -1) if none."}, From 9c91fd958f3f16e2ea091013847cb024f9da5e48 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Wed, 29 Jul 2026 09:30:37 +0300 Subject: [PATCH 2/3] gh-154781: Fix garbage from curses window.in_wstr() (GH-154782) in_wstr() searched the result for a terminating null, but winnwstr() writes one only if it stored at least one character. Use the number of characters it returns as the length. --- Lib/test/test_curses.py | 6 ++++++ Modules/_cursesmodule.c | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 4e5d5f1da0be95..4ba210c162d0f6 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -522,6 +522,12 @@ def test_in_wstr(self): self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s) self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes) + # Reading no characters gives an empty string, like instr() and + # in_wchstr() do. curses does not terminate the buffer in this case. + stdscr.addstr(0, 0, 'abz') + self.assertEqual(stdscr.in_wstr(0, 0, 0), '') + self.assertEqual(stdscr.in_wstr(0), '') + def test_complexchar(self): # A complexchar is a styled wide-character cell: str() is its text, # and the attr and pair attributes are its rendition. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index a913a7a0babe35..b8680edc6c0bed 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -3956,7 +3956,7 @@ PyCursesWindow_in_wstr(PyObject *op, PyObject *args) PyMem_Free(buf); return Py_GetConstant(Py_CONSTANT_EMPTY_STR); } - PyObject *res = PyUnicode_FromWideChar(buf, -1); + PyObject *res = PyUnicode_FromWideChar(buf, rtn); PyMem_Free(buf); return res; #else From 9ccd5bb81edde823fb5fdbd51287f4a0ddfcd149 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 29 Jul 2026 12:05:11 +0530 Subject: [PATCH 3/3] gh-153531: fix thread safety of setting func.__doc__ and func.__module__ (#154851) --- .../test_free_threading/test_functions.py | 6 ++ Objects/funcobject.c | 90 ++++++++++++------- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/Lib/test/test_free_threading/test_functions.py b/Lib/test/test_free_threading/test_functions.py index 65a90a17bd5b8d..7a59638d27dc9d 100644 --- a/Lib/test/test_free_threading/test_functions.py +++ b/Lib/test/test_free_threading/test_functions.py @@ -58,6 +58,12 @@ def test_annotate(self): def test_type_params(self): self.stress_attribute("__type_params__", lambda: (random_string(),)) + def test_doc(self): + self.stress_attribute("__doc__", random_string) + + def test_module(self): + self.stress_attribute("__module__", random_string) + def test_annotations_and_annotate(self): # The __annotations__ and __annotate__ setters clear each other. def target(): pass diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 49a28e8ad66714..0c1fab7f6d33a8 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -628,9 +628,7 @@ PyFunction_SetAnnotations(PyObject *op, PyObject *annotations) static PyMemberDef func_memberlist[] = { {"__closure__", _Py_T_OBJECT, OFF(func_closure), Py_READONLY}, - {"__doc__", _Py_T_OBJECT, OFF(func_doc), 0}, {"__globals__", _Py_T_OBJECT, OFF(func_globals), Py_READONLY}, - {"__module__", _Py_T_OBJECT, OFF(func_module), 0}, {"__builtins__", _Py_T_OBJECT, OFF(func_builtins), Py_READONLY}, {NULL} /* Sentinel */ }; @@ -762,6 +760,56 @@ func_set_qualname(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) return 0; } +static PyObject * +func_get_doc(PyObject *self, void *Py_UNUSED(ignored)) +{ + PyFunctionObject *op = _PyFunction_CAST(self); + PyObject *doc = op->func_doc; + if (doc == NULL) { + doc = Py_None; + } + return Py_NewRef(doc); +} + +static int +func_set_doc(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) +{ + /* Legal to del f.__doc__ or to set it to any object. */ + PyFunctionObject *op = _PyFunction_CAST(self); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_doc = op->func_doc; + op->func_doc = Py_XNewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_doc); + return 0; +} + +static PyObject * +func_get_module(PyObject *self, void *Py_UNUSED(ignored)) +{ + PyFunctionObject *op = _PyFunction_CAST(self); + PyObject *module = op->func_module; + if (module == NULL) { + module = Py_None; + } + return Py_NewRef(module); +} + +static int +func_set_module(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) +{ + /* Legal to del f.__module__ or to set it to any object. */ + PyFunctionObject *op = _PyFunction_CAST(self); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_module = op->func_module; + op->func_module = Py_XNewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_module); + return 0; +} + static PyObject * func_get_defaults(PyObject *self, void *Py_UNUSED(ignored)) { @@ -891,24 +939,12 @@ function___annotate___set_impl(PyFunctionObject *self, PyObject *value) return -1; } if (Py_IsNone(value)) { - PyInterpreterState *interp = _PyInterpreterState_GET(); - _PyEval_StopTheWorld(interp); - PyObject *old_annotate = self->func_annotate; - self->func_annotate = Py_NewRef(value); - _PyEval_StartTheWorld(interp); - Py_XDECREF(old_annotate); + Py_XSETREF(self->func_annotate, Py_NewRef(value)); return 0; } else if (PyCallable_Check(value)) { - PyInterpreterState *interp = _PyInterpreterState_GET(); - _PyEval_StopTheWorld(interp); - PyObject *old_annotate = self->func_annotate; - self->func_annotate = Py_NewRef(value); - PyObject *old_annotations = self->func_annotations; - self->func_annotations = NULL; - _PyEval_StartTheWorld(interp); - Py_XDECREF(old_annotate); - Py_XDECREF(old_annotations); + Py_XSETREF(self->func_annotate, Py_NewRef(value)); + Py_CLEAR(self->func_annotations); return 0; } else { @@ -961,15 +997,8 @@ function___annotations___set_impl(PyFunctionObject *self, PyObject *value) "__annotations__ must be set to a dict object"); return -1; } - PyInterpreterState *interp = _PyInterpreterState_GET(); - _PyEval_StopTheWorld(interp); - PyObject *old_annotations = self->func_annotations; - self->func_annotations = Py_XNewRef(value); - PyObject *old_annotate = self->func_annotate; - self->func_annotate = NULL; - _PyEval_StartTheWorld(interp); - Py_XDECREF(old_annotations); - Py_XDECREF(old_annotate); + Py_XSETREF(self->func_annotations, Py_XNewRef(value)); + Py_CLEAR(self->func_annotate); return 0; } @@ -1010,12 +1039,7 @@ function___type_params___set_impl(PyFunctionObject *self, PyObject *value) "__type_params__ must be set to a tuple"); return -1; } - PyInterpreterState *interp = _PyInterpreterState_GET(); - _PyEval_StopTheWorld(interp); - PyObject *old_typeparams = self->func_typeparams; - self->func_typeparams = Py_NewRef(value); - _PyEval_StartTheWorld(interp); - Py_XDECREF(old_typeparams); + Py_XSETREF(self->func_typeparams, Py_NewRef(value)); return 0; } @@ -1037,6 +1061,8 @@ static PyGetSetDef func_getsetlist[] = { FUNCTION___ANNOTATIONS___GETSETDEF FUNCTION___ANNOTATE___GETSETDEF {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict}, + {"__doc__", func_get_doc, func_set_doc}, + {"__module__", func_get_module, func_set_module}, {"__name__", func_get_name, func_set_name}, {"__qualname__", func_get_qualname, func_set_qualname}, FUNCTION___TYPE_PARAMS___GETSETDEF