https://github.com/python/cpython/commit/b953155e0d64b143546dd47554983fb73cfa6d1a commit: b953155e0d64b143546dd47554983fb73cfa6d1a branch: main author: Pranav Choudhary <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-10-18T09:09:25Z summary:
gh-155976: Refresh curses LINES, COLS, COLORS and COLOR_PAIRS on screen switch (GH-155994) set_term() and screen.use() now update curses.LINES, curses.COLS, curses.COLORS and curses.COLOR_PAIRS to the values of the current screen. LINES and COLS are taken from the size of its standard window, since ncurses sets its own LINES and COLS only when a screen is created. Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> files: A Misc/NEWS.d/next/Library/2026-10-18-08-30-10.gh-issue-155976.feSum4_j.rst M Lib/test/test_curses.py M Modules/_cursesmodule.c diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 5db44ebedd2ee0a..7fce9ab8646821a 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -14,6 +14,7 @@ check_disallow_instantiation, MISSING_C_DOCSTRINGS, gc_collect, SHORT_TIMEOUT) from test.support.import_helper import import_module +from test.support import os_helper # Optionally test curses module. This currently requires that the # 'curses' resource be given on the regrtest command line using the -u @@ -129,6 +130,9 @@ def _broken_variation_selector_width(): BROKEN_VARIATION_SELECTOR_WIDTH = _broken_variation_selector_width() +class CustomError(Exception): + pass + # newterm() is used when available (it reports errors instead of exiting), but # initscr() is still the fallback, and an unusable $TERM has no terminal to # drive either way. @@ -3353,6 +3357,50 @@ def test_set_term(self): self.assertIs(curses.set_term(a), b) # returns the previous one self.assertIs(curses.set_term(b), a) + def make_screens_of_different_terms(self): + # Two screens of different sizes and numbers of colors. Return them + # with the values of LINES, COLS, COLORS and COLOR_PAIRS for each. + screens = [] + with os_helper.EnvironmentVarGuard() as env: + for term, lines, cols in [('xterm', 25, 80), + ('xterm-256color', 30, 100)]: + env['LINES'] = str(lines) + env['COLUMNS'] = str(cols) + s = self.make_pty() + try: + screen = curses.newterm(term, s, s) + except curses.error: + self.skipTest(f'no {term} terminfo entry') + curses.start_color() + self.assertEqual(screen.stdscr.getmaxyx(), (lines, cols)) + screens.append((screen, self.get_screen_values())) + self.assertNotEqual(screens[0][1], screens[1][1]) + return screens + + def get_screen_values(self): + return curses.LINES, curses.COLS, curses.COLORS, curses.COLOR_PAIRS + + def test_set_term_refreshes_lines_cols_colors(self): + (a, avalues), (b, bvalues) = self.make_screens_of_different_terms() + self.assertIs(curses.set_term(a), b) + self.assertEqual(self.get_screen_values(), avalues) + self.assertIs(curses.set_term(b), a) + self.assertEqual(self.get_screen_values(), bvalues) + + @unittest.skipUnless(hasattr(curses.screen, 'use'), + 'requires curses.screen.use()') + def test_use_screen_refreshes_lines_cols_colors(self): + (a, avalues), (b, bvalues) = self.make_screens_of_different_terms() + self.assertEqual(a.use(lambda scr: self.get_screen_values()), avalues) + self.assertEqual(self.get_screen_values(), bvalues) + + def raise_in_use(scr): + self.assertEqual(self.get_screen_values(), avalues) + raise CustomError + with self.assertRaises(CustomError): + a.use(raise_in_use) + self.assertEqual(self.get_screen_values(), bvalues) + def test_window_keeps_screen_alive(self): # The standard window keeps its screen alive; dropping every other # reference and collecting must not invalidate the window. diff --git a/Misc/NEWS.d/next/Library/2026-10-18-08-30-10.gh-issue-155976.feSum4_j.rst b/Misc/NEWS.d/next/Library/2026-10-18-08-30-10.gh-issue-155976.feSum4_j.rst new file mode 100644 index 000000000000000..64aa76725de723e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-18-08-30-10.gh-issue-155976.feSum4_j.rst @@ -0,0 +1,3 @@ +:func:`curses.set_term` and :meth:`curses.screen.use` now update +:data:`curses.LINES`, :data:`curses.COLS`, :data:`curses.COLORS` +and :data:`curses.COLOR_PAIRS` for the current screen. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 401b777be23caa6..b2b410cfe5f5cc7 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -209,6 +209,8 @@ static int curses_initscr_called = FALSE; /* Tells whether start_color() has been called to initialise color usage. */ static int curses_start_color_called = FALSE; +static int update_lines_cols(PyObject *private_module); + /* Encoding of the initial screen, used by module-level functions that have no window object to take it from (e.g. unctrl(), ungetch()). This is a private copy: the window object that initscr() returns may be deallocated @@ -5480,7 +5482,13 @@ static PyGetSetDef PyCursesScreen_getsets[] = { static int curses_use_screen_cb(SCREEN *Py_UNUSED(sp), void *data) { - curses_use_call((curses_use_data *)data); + curses_use_data *u = (curses_use_data *)data; + PyObject *module = PyType_GetModule(Py_TYPE(u->obj)); + if (module == NULL || !update_lines_cols(module)) { + u->result = NULL; + return 0; + } + curses_use_call(u); return 0; } @@ -5502,12 +5510,27 @@ PyCursesScreen_use(PyObject *self, PyObject *args, PyObject *kwargs) if (curses_use_parse(args, &func, &extra) < 0) { return NULL; } + PyObject *module = PyType_GetModule(Py_TYPE(self)); + if (module == NULL) { + Py_DECREF(extra); + return NULL; + } curses_use_data data = {self, func, extra, kwargs, NULL}; PyObject *prev = state->topscreen; state->topscreen = Py_NewRef(self); use_screen(so->screen, curses_use_screen_cb, &data); Py_SETREF(state->topscreen, prev); Py_DECREF(extra); + if (data.result == NULL) { + PyObject *exc = PyErr_GetRaisedException(); + (void)update_lines_cols(module); + _PyErr_ChainExceptions1(exc); + } + else { + if (!update_lines_cols(module)) { + Py_CLEAR(data.result); + } + } return data.result; } #endif /* HAVE_CURSES_USE_SCREEN */ @@ -7073,8 +7096,6 @@ _curses_setupterm_impl(PyObject *module, const char *term, int fd) Py_RETURN_NONE; } -static int update_lines_cols(PyObject *private_module); /* defined below */ - /* Return a file descriptor for obj, or, if obj is NULL or None, for the sys.<stdname> stream. Returns -1 with an exception set on error. */ static int @@ -7930,7 +7951,11 @@ update_lines_cols(PyObject *private_module) goto error; } - o = PyLong_FromLong(LINES); + int lines = LINES; + if (stdscr != NULL) { + lines = getmaxy(stdscr); + } + o = PyLong_FromLong(lines); if (o == NULL) { goto error; } @@ -7942,7 +7967,11 @@ update_lines_cols(PyObject *private_module) } Py_DECREF(o); - o = PyLong_FromLong(COLS); + int cols = COLS; + if (stdscr != NULL) { + cols = getmaxx(stdscr); + } + o = PyLong_FromLong(cols); if (o == NULL) { goto error; } @@ -7953,6 +7982,32 @@ update_lines_cols(PyObject *private_module) goto error; } Py_DECREF(o); + + if (curses_start_color_called) { + o = PyLong_FromUnsignedLongLong((unsigned long long)COLORS); + if (o == NULL) { + goto error; + } + if (PyDict_SetItemString(exposed_module_dict, "COLORS", o) < 0) { + goto error; + } + if (PyDict_SetItemString(private_module_dict, "COLORS", o) < 0) { + goto error; + } + Py_DECREF(o); + + o = PyLong_FromUnsignedLongLong((unsigned long long)COLOR_PAIRS); + if (o == NULL) { + goto error; + } + if (PyDict_SetItemString(exposed_module_dict, "COLOR_PAIRS", o) < 0) { + goto error; + } + if (PyDict_SetItemString(private_module_dict, "COLOR_PAIRS", o) < 0) { + goto error; + } + Py_DECREF(o); + } Py_DECREF(exposed_module); return 1; _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
