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]

Reply via email to