]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-152233: Add curses complexstr type and wide-character cell-array methods (GH-152262)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 26 Jun 2026 11:37:30 +0000 (14:37 +0300)
committerGitHub <noreply@github.com>
Fri, 26 Jun 2026 11:37:30 +0000 (14:37 +0300)
Add the immutable curses.complexstr type, an array of styled wide-character
cells -- the string counterpart of complexchar.  It is constructible from an
iterable of cells (each a complexchar or a str) or from a string split into
cells, with optional attr and pair applied to every cell.  It is an immutable
sequence (indexing yields a complexchar, slicing and concatenation yield a
complexstr), is hashable, and str() returns its cells' text.

Add the window method in_wchstr(), the wide-character counterpart of instr()
and in_wstr() that keeps each cell's attributes and color pair instead of
stripping them; it returns a complexstr.

The methods addstr(), addnstr(), insstr() and insnstr() now also accept a
complexstr, so a run read with in_wchstr() can be written back unchanged.  The
cells carry their own rendition, so combining one with an explicit attr raises
TypeError.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Doc/library/curses.rst
Doc/whatsnew/3.16.rst
Lib/test/test_curses.py
Misc/NEWS.d/next/Library/2026-06-26-11-20-00.gh-issue-152233.Kp7mQ2.rst [new file with mode: 0644]
Modules/_cursesmodule.c
Modules/clinic/_cursesmodule.c.h

index 945be3b51998c3460176969d2da54e4743e2f2d8..69af3b94d72da10f052357b2262c9c02a6cb7d7e 100644 (file)
@@ -952,6 +952,9 @@ Window objects
    ``(y, x)`` with attributes
    *attr*, overwriting anything previously on the display.
 
+   .. versionchanged:: next
+      *str* may now also be a :class:`complexstr`; see :meth:`addstr`.
+
 
 .. method:: window.addstr(str[, attr])
             window.addstr(y, x, str[, attr])
@@ -959,6 +962,11 @@ Window objects
    Paint the character string *str* at ``(y, x)`` with attributes
    *attr*, overwriting anything previously on the display.
 
+   *str* may also be a :class:`complexstr`, in which case each cell carries its
+   own attributes and color pair, so *attr* must not be given.  A
+   :class:`complexstr` obtained from :meth:`in_wchstr` is written back
+   unchanged.
+
    .. note::
 
       * Writing outside the window, subwindow, or pad raises :exc:`curses.error`.
@@ -971,6 +979,9 @@ Window objects
         not calling :meth:`!addstr` with a *str* that has embedded newlines;
         instead, call :meth:`!addstr` separately for each line.
 
+   .. versionchanged:: next
+      *str* may now also be a :class:`complexstr`, as described above.
+
 
 .. method:: window.attroff(attr)
 
@@ -1428,6 +1439,9 @@ Window objects
    cursor are shifted right, with the rightmost characters on the line being lost.
    The cursor position does not change (after moving to *y*, *x*, if specified).
 
+   .. versionchanged:: next
+      *str* may now also be a :class:`complexstr`; see :meth:`insstr`.
+
 
 .. method:: window.insstr(str[, attr])
             window.insstr(y, x, str[, attr])
@@ -1437,6 +1451,12 @@ Window objects
    shifted right, with the rightmost characters on the line being lost.  The cursor
    position does not change (after moving to *y*, *x*, if specified).
 
+   *str* may also be a :class:`complexstr`, in which case each cell carries its
+   own attributes and color pair, so *attr* must not be given.
+
+   .. versionchanged:: next
+      *str* may now also be a :class:`complexstr`, as described above.
+
 
 .. method:: window.instr([n])
             window.instr(y, x[, n])
@@ -1465,6 +1485,25 @@ Window objects
    .. versionadded:: next
 
 
+.. method:: window.in_wchstr([n])
+            window.in_wchstr(y, x[, n])
+
+   Return a :class:`complexstr` of the styled cells extracted from the window
+   starting at the current cursor position, or at *y*, *x* if specified, and
+   stopping at the end of the line.  This is the variant of :meth:`instr` and
+   :meth:`in_wstr` that *keeps* each cell's attributes and color pair (those
+   methods strip the rendition).  If *n* is specified, at most *n* cells are
+   returned.  The maximum value for *n* is 2047.
+
+   The result can be written back unchanged with :meth:`addstr` (a read and a
+   re-write is a round-trip that preserves every cell's rendition).
+
+   This method is only available if Python was built against a wide-character
+   version of the underlying curses library.
+
+   .. versionadded:: next
+
+
 .. method:: window.is_cleared()
 
    Return the current value set by :meth:`clearok`.
@@ -1913,6 +1952,43 @@ Complex character objects
    .. versionadded:: next
 
 
+.. class:: complexstr(cells[, attr[, pair]])
+
+   A *complex character string* (or *complexstr*) is an immutable sequence of
+   styled wide-character cells -- the string counterpart of
+   :class:`complexchar` (as :class:`str` is to a single character).
+
+   If *cells* is a string, it is split into character cells (each a spacing
+   character optionally followed by combining characters), and *attr* (a
+   combination of the :ref:`WA_* attributes <curses-wa-constants>`) and *pair*
+   (a color pair number), if given, are applied to every cell.
+
+   Otherwise *cells* is an iterable whose items are themselves cells, each a
+   :class:`complexchar` or a string; each item then carries its own rendition,
+   and *attr* and *pair* must be omitted.
+
+   It is returned by :meth:`window.in_wchstr`, and accepted by
+   :meth:`window.addstr`, :meth:`~window.addnstr`, :meth:`~window.insstr` and
+   :meth:`~window.insnstr`, so a run read from a window can be written back
+   unchanged.
+
+   It behaves like an immutable sequence: ``len(s)`` is the number of cells,
+   ``s[i]`` is the *i*-th cell as a :class:`complexchar`, slicing and
+   concatenation produce new :class:`!complexstr` instances, and iterating
+   yields the cells.  :func:`str` returns the cells' text joined together, and
+   two complex character strings are equal when their cells all match.  It is
+   hashable.
+
+   To build or edit a run of cells, use an ordinary :class:`list` of
+   :class:`complexchar` (or strings); a :class:`!complexstr` is the immutable
+   form returned by a read.
+
+   This type is only available if Python was built against a wide-character
+   version of the underlying curses library.
+
+   .. versionadded:: next
+
+
 Constants
 ---------
 
index c0b6a650b7b887f7fee80d214774e622b81df0ab..80f13e4d759dd3088548b97d326790bf9aa98b14 100644 (file)
@@ -148,6 +148,14 @@ curses
   :class:`~curses.complexchar`.
   (Contributed by Serhiy Storchaka in :gh:`152233`.)
 
+* Add the :class:`curses.complexstr` type, an immutable run of styled cells
+  (the string counterpart of :class:`~curses.complexchar`), and the window
+  method :meth:`~curses.window.in_wchstr` that returns one.  The string-cell
+  methods :meth:`~curses.window.addstr`, :meth:`~curses.window.addnstr`,
+  :meth:`~curses.window.insstr` and :meth:`~curses.window.insnstr` now also
+  accept a :class:`~curses.complexstr`.
+  (Contributed by Serhiy Storchaka in :gh:`152233`.)
+
 gzip
 ----
 
index bc3cc5cb3835177d23355e6327f848c47d850e16..27a64532b21fd85227325ab8612dbde172cdf52e 100644 (file)
@@ -469,6 +469,145 @@ class TestCurses(unittest.TestCase):
         self.assertEqual(str(cc), ' ')
         self.assertTrue(cc.attr & curses.A_BOLD)
 
+    @requires_curses_func('complexstr')
+    def test_complexstr(self):
+        # A complexstr is an immutable run of styled wide-character cells: the
+        # string counterpart of complexchar (as str is to a single character).
+        cc = curses.complexchar
+        B = curses.A_BOLD
+        # Built from an iterable whose items are complexchar or str cells.
+        s = curses.complexstr([cc('A', B), 'b', cc('c')])
+        self.assertEqual(len(s), 3)
+        self.assertEqual(str(s), 'Abc')
+        # Indexing yields a complexchar carrying the cell's rendition.
+        self.assertIsInstance(s[0], curses.complexchar)
+        self.assertEqual(str(s[0]), 'A')
+        self.assertTrue(s[0].attr & B)
+        self.assertEqual(s[-1], cc('c'))
+        self.assertRaises(IndexError, lambda: s[3])
+        # Iteration walks the cells.
+        self.assertEqual([str(c) for c in s], ['A', 'b', 'c'])
+        # Slicing and concatenation produce new complexstr instances.
+        self.assertIsInstance(s[1:], curses.complexstr)
+        self.assertEqual(str(s[1:]), 'bc')
+        self.assertEqual(str(s[::-1]), 'cbA')
+        self.assertEqual(str(s + curses.complexstr(['Z'])), 'AbcZ')
+        # The empty complexstr.
+        self.assertEqual(len(curses.complexstr([])), 0)
+        self.assertEqual(str(curses.complexstr('')), '')
+        # Equality and hashing compare the cells (text, attributes, pair).
+        self.assertEqual(s, curses.complexstr([cc('A', B), 'b', cc('c')]))
+        self.assertEqual(hash(s),
+                         hash(curses.complexstr([cc('A', B), 'b', cc('c')])))
+        self.assertNotEqual(s, curses.complexstr([cc('A'), 'b', cc('c')]))
+        self.assertNotEqual(s, curses.complexstr([cc('A', B), 'b']))
+        # A spacing character optionally followed by combining characters.
+        if self._encodable('é'):
+            self.assertEqual(str(curses.complexstr(['é', 'x'])),
+                             'éx')
+        # cells is positional-only.
+        self.assertRaises(TypeError, lambda: curses.complexstr(cells=['x']))
+        # Invalid arguments.
+        self.assertRaises(TypeError, curses.complexstr, 5)
+        self.assertRaises(TypeError, curses.complexstr, [65])
+        self.assertRaises(ValueError, curses.complexstr, ['ab'])
+
+        # A string is split into character cells, grouping each base character
+        # with the combining characters that follow it (not one cell per code
+        # point), unlike a generic sequence whose items are each one cell.
+        self.assertEqual(len(curses.complexstr('abc')), 3)
+        self.assertEqual(str(curses.complexstr('abc')), 'abc')
+        self.assertEqual(len(curses.complexstr('')), 0)
+        base = 'é'  # 'e' + combining acute: two code points, one cell
+        if self._encodable(base):
+            self.assertEqual(len(curses.complexstr(base)), 1)
+            self.assertEqual(curses.complexstr(base)[0], cc(base))
+            self.assertEqual(len(curses.complexstr('a' + base + 'b')), 3)
+            # A combining character cannot begin a cell: one that leads the
+            # string, or overflows a base's combining slots, has no base.
+            self.assertRaises(ValueError, curses.complexstr, '\u0301')
+            self.assertRaises(ValueError, curses.complexstr, 'e' + '\u0301' * 10)
+            # A control character may stand alone but not carry combining marks.
+            self.assertRaises(ValueError, curses.complexstr, '\n\u0301')
+        # attr and pair apply to every cell of a string; pair is optional.
+        styled = curses.complexstr('hi', B, 0)
+        self.assertTrue(all(styled[i].attr & B for i in range(len(styled))))
+        self.assertEqual(curses.complexstr('x', B)[0], cc('x', B))
+        self.assertEqual(curses.complexstr('x', B, 0)[0], cc('x', B, 0))
+        # attr and pair may also be passed by keyword.
+        self.assertEqual(curses.complexstr('x', attr=B)[0], cc('x', B))
+        self.assertEqual(curses.complexstr('x', attr=B, pair=0)[0], cc('x', B, 0))
+        self.assertEqual(curses.complexstr('x', pair=0)[0], cc('x', 0, 0))
+        # cells is positional-only.
+        self.assertRaises(TypeError, lambda: curses.complexstr(cells='x'))
+        self.assertRaises(ValueError, curses.complexstr, 'a', 0, -1)
+        self.assertRaises(ValueError, lambda: curses.complexstr('a', pair=-1))
+        # For a non-string, giving attr/pair at all is an error (the cells
+        # carry their own rendition) -- even attr=0.
+        self.assertRaises(TypeError, curses.complexstr, [cc('A')], B)
+        self.assertRaises(TypeError, curses.complexstr, [cc('A')], 0)
+        self.assertRaises(TypeError, curses.complexstr, ['A'], 0, 0)
+        self.assertRaises(TypeError,
+                          lambda: curses.complexstr([cc('A')], attr=B))
+        self.assertRaises(TypeError,
+                          lambda: curses.complexstr(['A'], pair=0))
+
+    @requires_curses_window_meth('in_wchstr')
+    def test_in_wchstr(self):
+        # in_wchstr() returns a complexstr -- the styled-cell counterpart of
+        # instr() (bytes) and in_wstr() (str), which both strip the rendition.
+        stdscr = self.stdscr
+        cc = curses.complexchar
+        B = curses.A_BOLD
+        s = curses.complexstr([cc('A', B), cc('b'), cc('C', B)])
+        stdscr.addstr(0, 0, s)
+        r = stdscr.in_wchstr(0, 0, 3)
+        self.assertIsInstance(r, curses.complexstr)
+        # A read followed by a re-write is an exact round-trip.
+        self.assertEqual(r, s)
+        self.assertEqual(str(r), 'AbC')
+        self.assertTrue(r[0].attr & B)
+        self.assertFalse(r[1].attr & B)
+        # The count is optional and reads to the end of the line by default.
+        stdscr.move(0, 0)
+        self.assertEqual(str(stdscr.in_wchstr())[:3], 'AbC')
+
+    @requires_curses_window_meth('in_wchstr')
+    def test_complexstr_in_write_methods(self):
+        # addstr/addnstr/insstr/insnstr also accept a complexstr, written via
+        # the wide-character functions; a plain str keeps its current meaning.
+        stdscr = self.stdscr
+        cc = curses.complexchar
+        B = curses.A_BOLD
+        s = curses.complexstr([cc('A', B), cc('b'), cc('C', B)])
+        # addstr with a complexstr round-trips.
+        stdscr.addstr(0, 0, s)
+        self.assertEqual(stdscr.in_wchstr(0, 0, 3), s)
+        # addnstr writes at most n cells.
+        stdscr.addstr(2, 0, '....')
+        stdscr.addnstr(2, 0, s, 2)
+        self.assertEqual(str(stdscr.in_wchstr(2, 0, 4)), 'Ab..')
+        # insstr inserts the cells in order.
+        stdscr.move(3, 0)
+        stdscr.addstr('END')
+        stdscr.insstr(3, 0, curses.complexstr([cc('P'), cc('Q')]))
+        self.assertEqual(str(stdscr.in_wchstr(3, 0, 5)), 'PQEND')
+        # insnstr inserts at most n cells.
+        stdscr.move(4, 0)
+        stdscr.addstr('END')
+        stdscr.insnstr(4, 0, curses.complexstr(['1', '2', '3']), 2)
+        self.assertEqual(str(stdscr.in_wchstr(4, 0, 5)), '12END')
+        # An empty run is accepted (and still honours the move).
+        stdscr.addstr(5, 0, curses.complexstr([]))
+        stdscr.insstr(5, 0, curses.complexstr([]))
+        # Cells carry their own rendition, so an explicit attr is rejected.
+        self.assertRaises(TypeError, stdscr.addstr, s, B)
+        self.assertRaises(TypeError, stdscr.addnstr, s, 2, B)
+        self.assertRaises(TypeError, stdscr.insstr, s, B)
+        self.assertRaises(TypeError, stdscr.insnstr, s, 2, B)
+        # A bare sequence of cells is not accepted; build a complexstr first.
+        self.assertRaises(TypeError, stdscr.addstr, [cc('A'), 'b'])
+        self.assertRaises(TypeError, stdscr.insstr, [cc('A'), 'b'])
 
     def test_output_character(self):
         stdscr = self.stdscr
diff --git a/Misc/NEWS.d/next/Library/2026-06-26-11-20-00.gh-issue-152233.Kp7mQ2.rst b/Misc/NEWS.d/next/Library/2026-06-26-11-20-00.gh-issue-152233.Kp7mQ2.rst
new file mode 100644 (file)
index 0000000..da9cf22
--- /dev/null
@@ -0,0 +1,6 @@
+Add the :class:`curses.complexstr` type, an immutable string of styled
+wide-character cells (the counterpart of :class:`curses.complexchar`), and the
+:mod:`curses` window method :meth:`~curses.window.in_wchstr` that returns one.
+The string-cell methods :meth:`~curses.window.addstr`,
+:meth:`~curses.window.addnstr`, :meth:`~curses.window.insstr` and
+:meth:`~curses.window.insnstr` now also accept a :class:`~curses.complexstr`.
index 96e1fdc3bfe5a5d4d3cabe559130191a4db0315d..1274f291e15f98d442f54dba19c2af06e372d7f3 100644 (file)
@@ -109,6 +109,7 @@ static const char PyCursesVersion[] = "2.2";
 #include "pycore_long.h"        // _PyLong_GetZero()
 #include "pycore_structseq.h"   // _PyStructSequence_NewType()
 #include "pycore_fileutils.h"   // _Py_dup(), _Py_set_inheritable()
+#include "pycore_tuple.h"       // _PyTuple_HASH_XXPRIME_1
 
 #ifdef __hpux
 #define STRICT_SYSV_CURSES
@@ -167,6 +168,7 @@ typedef struct {
     PyTypeObject *screen_type;      // _curses.screen
 #ifdef HAVE_NCURSESW
     PyTypeObject *complexchar_type; // _curses.complexchar
+    PyTypeObject *complexstr_type;  // _curses.complexstr
 #endif
     PyObject *topscreen;            // owned ref to the current screen object,
                                     // or NULL for the initscr() screen
@@ -202,8 +204,9 @@ module _curses
 class _curses.window "PyCursesWindowObject *" "clinic_state()->window_type"
 class _curses.screen "PyCursesScreenObject *" "clinic_state()->screen_type"
 class _curses.complexchar "PyCursesComplexCharObject *" "clinic_state()->complexchar_type"
+class _curses.complexstr "PyCursesComplexStrObject *" "get_cursesmodule_state_by_cls(type)->complexstr_type"
 [clinic start generated code]*/
-/*[clinic end generated code: output=da39a3ee5e6b4b0d input=211a02287a60aed0]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=e9439fe0a704a26e]*/
 
 /* Indicate whether the module has already been loaded or not. */
 static int curses_module_loaded = 0;
@@ -669,6 +672,17 @@ typedef struct {
 
 #define _PyCursesComplexCharObject_CAST(op)  ((PyCursesComplexCharObject *)(op))
 
+/* An immutable packed array of cchar_t cells -- the "complex character
+   string" counterpart of complexchar (as str is to a single character).
+   It owns the contiguous buffer that win_wchnstr() fills directly, so a read
+   and a re-write is a zero-copy round-trip. */
+typedef struct {
+    PyObject_VAR_HEAD
+    cchar_t cells[1];   // ob_size cells, stored inline (variable-size object)
+} PyCursesComplexStrObject;
+
+#define _PyCursesComplexStrObject_CAST(op)  ((PyCursesComplexStrObject *)(op))
+
 /* Build a single character cell from obj.
 
    Return 1 and store a chtype in *pch for an int or bytes, 2 and store a
@@ -1117,6 +1131,430 @@ static PyGetSetDef complexchar_getsets[] = {
     {NULL}
 };
 
+/* -------------------------------------------------------------*/
+/* Complex character strings (immutable arrays of styled cells)  */
+/* -------------------------------------------------------------*/
+
+/* Pack a single Python cell -- a complexchar (used as is) or a str (a spacing
+   character optionally followed by combining characters, with no attributes
+   and color pair 0) -- into *out.  Return 0 on success, -1 with an exception
+   set otherwise. */
+static int
+curses_pack_cell(cursesmodule_state *state, PyObject *item, cchar_t *out)
+{
+    if (Py_IS_TYPE(item, state->complexchar_type)) {
+        *out = _PyCursesComplexCharObject_CAST(item)->cval;
+        return 0;
+    }
+    if (PyUnicode_Check(item)) {
+        wchar_t wstr[CCHARW_MAX + 1];
+        if (PyCurses_ConvertToWideCell(item, wstr) < 0) {
+            return -1;
+        }
+        if (curses_setcchar(out, wstr, A_NORMAL, 0) == ERR) {
+            PyErr_SetString(state->error, "setcchar() returned ERR");
+            return -1;
+        }
+        return 0;
+    }
+    PyErr_Format(PyExc_TypeError,
+                 "complexstr cell must be a complexchar or a str, not %T",
+                 item);
+    return -1;
+}
+
+/* Wrap a buffer of len cells in a new complexstr, copying them in.  tp_alloc
+   sizes the variable-size object for len cells and sets ob_size. */
+static PyObject *
+PyCursesComplexStr_New(cursesmodule_state *state, const cchar_t *cells,
+                       Py_ssize_t len)
+{
+    PyTypeObject *type = state->complexstr_type;
+    PyObject *res = type->tp_alloc(type, len);
+    if (res != NULL && len > 0) {
+        memcpy(_PyCursesComplexStrObject_CAST(res)->cells, cells,
+               (size_t)len * sizeof(cchar_t));
+    }
+    return res;
+}
+
+/* Build a complexstr from a string, grouping each base character with its
+   trailing combining characters into one cell (so "é" is one cell, not two).
+   A string needs this separate path because a generic sequence is packed one
+   cell per item, which would not keep combining marks with their base. */
+static PyObject *
+complexstr_from_string(cursesmodule_state *state, PyObject *str,
+                       attr_t attr, int pair)
+{
+    Py_ssize_t n;
+    wchar_t *wbuf = PyUnicode_AsWideCharString(str, &n);
+    if (wbuf == NULL) {
+        return NULL;
+    }
+    cchar_t *cells = n > 0 ? PyMem_New(cchar_t, n) : NULL;
+    if (n > 0 && cells == NULL) {
+        PyMem_Free(wbuf);
+        return PyErr_NoMemory();
+    }
+    Py_ssize_t count = 0;
+    for (Py_ssize_t i = 0; i < n; ) {
+        wchar_t cell[CCHARW_MAX + 1];
+        Py_ssize_t k = 0;
+        cell[k++] = wbuf[i++];
+        while (i < n && k < CCHARW_MAX && wcwidth(wbuf[i]) == 0) {
+            cell[k++] = wbuf[i++];
+        }
+        cell[k] = L'\0';
+        /* A cell's base must be a spacing character.  A combining character
+           (wcwidth 0) has no base to attach to, so it cannot start a cell.  A
+           control character (wcwidth < 0) may stand alone but cannot carry
+           combining marks. */
+        int width = wcwidth(cell[0]);
+        if (width == 0 || (k > 1 && width < 0)) {
+            PyErr_Format(PyExc_ValueError,
+                         "a character cell must be a single spacing character "
+                         "optionally followed by up to %d combining characters",
+                         (int)(CCHARW_MAX - 1));
+            PyMem_Free(cells);
+            PyMem_Free(wbuf);
+            return NULL;
+        }
+        if (curses_setcchar(&cells[count], cell, attr, pair) == ERR) {
+            if (!PyErr_Occurred()) {
+                PyErr_SetString(state->error, "setcchar() returned ERR");
+            }
+            PyMem_Free(cells);
+            PyMem_Free(wbuf);
+            return NULL;
+        }
+        count++;
+    }
+    PyObject *res = PyCursesComplexStr_New(state, cells, count);
+    PyMem_Free(cells);
+    PyMem_Free(wbuf);
+    return res;
+}
+
+/*[clinic input]
+@classmethod
+_curses.complexstr.__new__ as complexstr_new
+
+    cells: object
+        An iterable of cells, each a complexchar or a str.
+    /
+    attr: object = NULL
+        Attributes applied to every cell (only with a string).
+    pair: object = NULL
+        Color pair applied to every cell (only with a string).
+
+An immutable string of styled wide-character cells.
+
+It is the counterpart of complexchar for a run of cells, and the type
+returned by window.in_wchstr().  Each cell is a complexchar or a str (a
+spacing character optionally followed by combining characters).
+
+When cells is a string it is split into character cells, and attr and
+pair (if given) style every cell.  Otherwise each item carries its own
+rendition, and attr and pair must be omitted.
+[clinic start generated code]*/
+
+static PyObject *
+complexstr_new_impl(PyTypeObject *type, PyObject *cells, PyObject *attr,
+                    PyObject *pair)
+/*[clinic end generated code: output=ef8a53143d35a32a input=9b75aee973cc6565]*/
+{
+    cursesmodule_state *state = get_cursesmodule_state_by_cls(type);
+    /* A string is split into cells (grouping combining characters), not
+       iterated as one cell per code point; attr/pair then style every cell. */
+    if (PyUnicode_Check(cells)) {
+        attr_t cattr = A_NORMAL;
+        int cpair = 0;
+        if (attr != NULL && !attr_converter(attr, &cattr)) {
+            return NULL;
+        }
+        if (pair != NULL) {
+            cpair = PyLong_AsInt(pair);
+            if (cpair == -1 && PyErr_Occurred()) {
+                return NULL;
+            }
+            if (cpair < 0) {
+                PyErr_SetString(PyExc_ValueError, "color pair is less than 0");
+                return NULL;
+            }
+        }
+        return complexstr_from_string(state, cells, cattr, cpair);
+    }
+    /* For any other sequence each item carries its own rendition, so attr and
+       pair cannot be given. */
+    if (attr != NULL || pair != NULL) {
+        PyErr_SetString(PyExc_TypeError,
+                        "attr and pair can only be given with a string");
+        return NULL;
+    }
+    PyObject *seq = PySequence_Fast(cells,
+                                    "complexstr() argument must be an iterable");
+    if (seq == NULL) {
+        return NULL;
+    }
+    Py_ssize_t n = PySequence_Fast_GET_SIZE(seq);
+    PyObject *res = type->tp_alloc(type, n);
+    if (res == NULL) {
+        Py_DECREF(seq);
+        return NULL;
+    }
+    cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells;
+    for (Py_ssize_t i = 0; i < n; i++) {
+        PyObject *item = PySequence_Fast_GET_ITEM(seq, i);  // borrowed
+        if (curses_pack_cell(state, item, &out[i]) < 0) {
+            Py_DECREF(res);
+            Py_DECREF(seq);
+            return NULL;
+        }
+    }
+    Py_DECREF(seq);
+    return res;
+}
+
+static void
+complexstr_dealloc(PyObject *self)
+{
+    PyTypeObject *tp = Py_TYPE(self);
+    tp->tp_free(self);
+    Py_DECREF(tp);
+}
+
+static Py_ssize_t
+complexstr_length(PyObject *self)
+{
+    return Py_SIZE(self);
+}
+
+/* Wrap cell i (no bounds check) in a new complexchar. */
+static PyObject *
+complexstr_getcell(PyObject *self, Py_ssize_t i)
+{
+    cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self));
+    cchar_t *cells = _PyCursesComplexStrObject_CAST(self)->cells;
+    return PyCursesComplexChar_New(state, &cells[i]);
+}
+
+static PyObject *
+complexstr_item(PyObject *self, Py_ssize_t i)
+{
+    if (i < 0 || i >= Py_SIZE(self)) {
+        PyErr_SetString(PyExc_IndexError, "complexstr index out of range");
+        return NULL;
+    }
+    return complexstr_getcell(self, i);
+}
+
+static PyObject *
+complexstr_subscript(PyObject *self, PyObject *key)
+{
+    PyCursesComplexStrObject *s = _PyCursesComplexStrObject_CAST(self);
+    if (PyIndex_Check(key)) {
+        Py_ssize_t i = PyNumber_AsSsize_t(key, PyExc_IndexError);
+        if (i == -1 && PyErr_Occurred()) {
+            return NULL;
+        }
+        if (i < 0) {
+            i += Py_SIZE(s);
+        }
+        return complexstr_item(self, i);
+    }
+    if (PySlice_Check(key)) {
+        Py_ssize_t start, stop, step, slicelen;
+        if (PySlice_GetIndicesEx(key, Py_SIZE(s), &start, &stop, &step,
+                                 &slicelen) < 0)
+        {
+            return NULL;
+        }
+        cursesmodule_state *state =
+            get_cursesmodule_state_by_cls(Py_TYPE(self));
+        PyTypeObject *type = state->complexstr_type;
+        PyObject *res = type->tp_alloc(type, slicelen);
+        if (res == NULL) {
+            return NULL;
+        }
+        cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells;
+        for (Py_ssize_t i = 0, idx = start; i < slicelen; i++, idx += step) {
+            out[i] = s->cells[idx];
+        }
+        return res;
+    }
+    PyErr_Format(PyExc_TypeError,
+                 "complexstr indices must be integers or slices, not %T",
+                 key);
+    return NULL;
+}
+
+static PyObject *
+complexstr_concat(PyObject *a, PyObject *b)
+{
+    cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(a));
+    if (!Py_IS_TYPE(b, state->complexstr_type)) {
+        Py_RETURN_NOTIMPLEMENTED;
+    }
+    PyCursesComplexStrObject *sa = _PyCursesComplexStrObject_CAST(a);
+    PyCursesComplexStrObject *sb = _PyCursesComplexStrObject_CAST(b);
+    PyTypeObject *type = state->complexstr_type;
+    PyObject *res = type->tp_alloc(type, Py_SIZE(sa) + Py_SIZE(sb));
+    if (res == NULL) {
+        return NULL;
+    }
+    cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells;
+    if (Py_SIZE(sa)) {
+        memcpy(out, sa->cells, (size_t)Py_SIZE(sa) * sizeof(cchar_t));
+    }
+    if (Py_SIZE(sb)) {
+        memcpy(out + Py_SIZE(sa), sb->cells, (size_t)Py_SIZE(sb) * sizeof(cchar_t));
+    }
+    return res;
+}
+
+static PyObject *
+complexstr_str(PyObject *self)
+{
+    PyCursesComplexStrObject *s = _PyCursesComplexStrObject_CAST(self);
+    if (Py_SIZE(s) == 0) {
+        return Py_GetConstant(Py_CONSTANT_EMPTY_STR);
+    }
+    wchar_t *buf = PyMem_New(wchar_t, Py_SIZE(s) * CCHARW_MAX + 1);
+    if (buf == NULL) {
+        return PyErr_NoMemory();
+    }
+    Py_ssize_t pos = 0;
+    for (Py_ssize_t i = 0; i < Py_SIZE(s); i++) {
+        attr_t attrs;
+        int pair;
+        /* getcchar() writes the cell's text (and a terminator) at buf + pos;
+           the next cell overwrites the terminator. */
+        if (curses_getcchar(&s->cells[i], buf + pos, &attrs, &pair) == ERR) {
+            cursesmodule_state *state =
+                get_cursesmodule_state_by_cls(Py_TYPE(self));
+            PyErr_SetString(state->error, "getcchar() returned ERR");
+            PyMem_Free(buf);
+            return NULL;
+        }
+        pos += wcslen(buf + pos);
+    }
+    PyObject *res = PyUnicode_FromWideChar(buf, pos);
+    PyMem_Free(buf);
+    return res;
+}
+
+static PyObject *
+complexstr_repr(PyObject *self)
+{
+    PyObject *list = PySequence_List(self);
+    if (list == NULL) {
+        return NULL;
+    }
+    PyObject *res = PyUnicode_FromFormat("%T(%R)", self, list);
+    Py_DECREF(list);
+    return res;
+}
+
+static Py_hash_t
+complexstr_hash(PyObject *self)
+{
+    PyCursesComplexStrObject *s = _PyCursesComplexStrObject_CAST(self);
+    cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self));
+    /* Combine the per-cell hashes like a tuple. */
+    Py_uhash_t acc = _PyTuple_HASH_XXPRIME_5;
+    for (Py_ssize_t i = 0; i < Py_SIZE(s); i++) {
+        Py_hash_t lane = curses_cchar_hash(state, &s->cells[i]);
+        if (lane == -1) {
+            return -1;
+        }
+        acc += (Py_uhash_t)lane * _PyTuple_HASH_XXPRIME_2;
+        acc = _PyTuple_HASH_XXROTATE(acc);
+        acc *= _PyTuple_HASH_XXPRIME_1;
+    }
+    acc += Py_SIZE(s) ^ (_PyTuple_HASH_XXPRIME_5 ^ 3527539);
+    if (acc == (Py_uhash_t)-1) {
+        acc = 1546275796;
+    }
+    return (Py_hash_t)acc;
+}
+
+static PyObject *
+complexstr_richcompare(PyObject *self, PyObject *other, int op)
+{
+    if ((op != Py_EQ && op != Py_NE) || !Py_IS_TYPE(other, Py_TYPE(self))) {
+        Py_RETURN_NOTIMPLEMENTED;
+    }
+    PyCursesComplexStrObject *a = _PyCursesComplexStrObject_CAST(self);
+    PyCursesComplexStrObject *b = _PyCursesComplexStrObject_CAST(other);
+    int equal = (Py_SIZE(a) == Py_SIZE(b));
+    for (Py_ssize_t i = 0; equal && i < Py_SIZE(a); i++) {
+        wchar_t wa[CCHARW_MAX + 1], wb[CCHARW_MAX + 1];
+        attr_t aa, ab;
+        int pa, pb;
+        if (curses_getcchar(&a->cells[i], wa, &aa, &pa) == ERR ||
+            curses_getcchar(&b->cells[i], wb, &ab, &pb) == ERR)
+        {
+            cursesmodule_state *state =
+                get_cursesmodule_state_by_cls(Py_TYPE(self));
+            PyErr_SetString(state->error, "getcchar() returned ERR");
+            return NULL;
+        }
+        equal = (aa == ab && pa == pb && wcscmp(wa, wb) == 0);
+    }
+    return PyBool_FromLong(equal == (op == Py_EQ));
+}
+
+/* Write (insert=0) or insert (insert=1) a complexstr's cells, using its buffer
+   directly, at the current or given position.  n_limit < 0 means the whole run.
+   Returns None or NULL with an exception. */
+static PyObject *
+curses_window_put_cells(PyCursesWindowObject *self, PyObject *obj,
+                        int use_xy, int y, int x, int n_limit, int insert,
+                        const char *funcname)
+{
+    const cchar_t *cells = _PyCursesComplexStrObject_CAST(obj)->cells;
+    Py_ssize_t count = Py_SIZE(obj);
+
+    if (n_limit >= 0 && count > n_limit) {
+        count = n_limit;
+    }
+
+    if (count == 0) {
+        /* Nothing to write; just honour the optional move, like an empty
+           string would. */
+        int rtn = use_xy ? wmove(self->win, y, x) : OK;
+        return curses_window_check_err(self, rtn, "wmove", funcname);
+    }
+
+    int rtn;
+    const char *cfuncname;
+    if (insert) {
+        /* There is no batch cchar_t insert; insert the cells right-to-left at
+           the position so they end up in order. */
+        if (use_xy && wmove(self->win, y, x) == ERR) {
+            curses_window_set_error(self, "wmove", funcname);
+            return NULL;
+        }
+        rtn = OK;
+        cfuncname = "wins_wch";
+        for (Py_ssize_t i = count - 1; i >= 0; i--) {
+            rtn = wins_wch(self->win, &cells[i]);
+            if (rtn == ERR) {
+                break;
+            }
+        }
+    }
+    else if (use_xy) {
+        rtn = mvwadd_wchnstr(self->win, y, x, cells, (int)count);
+        cfuncname = "mvwadd_wchnstr";
+    }
+    else {
+        rtn = wadd_wchnstr(self->win, cells, (int)count);
+        cfuncname = "wadd_wchnstr";
+    }
+    return curses_window_check_err(self, rtn, cfuncname, funcname);
+}
+
 #endif
 
 /*****************************************************************************
@@ -1523,6 +1961,18 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1,
     const char *funcname;
 
 #ifdef HAVE_NCURSESW
+    {
+        cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+        if (Py_IS_TYPE(str, state->complexstr_type)) {
+            if (use_attr) {
+                PyErr_SetString(PyExc_TypeError, "addstr(): attr cannot be "
+                                "specified together with a complexstr");
+                return NULL;
+            }
+            return curses_window_put_cells(self, str, use_xy, y, x,
+                                           -1, 0, "addstr");
+        }
+    }
     strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr);
 #else
     strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL);
@@ -1626,6 +2076,18 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1,
     const char *funcname;
 
 #ifdef HAVE_NCURSESW
+    {
+        cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+        if (Py_IS_TYPE(str, state->complexstr_type)) {
+            if (use_attr) {
+                PyErr_SetString(PyExc_TypeError, "addnstr(): attr cannot be "
+                                "specified together with a complexstr");
+                return NULL;
+            }
+            return curses_window_put_cells(self, str, use_xy, y, x,
+                                           n, 0, "addnstr");
+        }
+    }
     strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr);
 #else
     strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL);
@@ -3026,6 +3488,73 @@ PyCursesWindow_in_wstr(PyObject *op, PyObject *args)
     PyMem_Free(buf);
     return res;
 }
+
+PyDoc_STRVAR(_curses_window_in_wchstr__doc__,
+"in_wchstr([y, x,] n=2047)\n"
+"Return a complexstr of the styled cells extracted from the window.\n"
+"\n"
+"  y\n"
+"    Y-coordinate.\n"
+"  x\n"
+"    X-coordinate.\n"
+"  n\n"
+"    Maximal number of cells.\n"
+"\n"
+"This is the wide-character variant of instr() and in_wstr() that keeps\n"
+"each cell's attributes and color pair; it returns a complexstr.");
+
+static PyObject *
+PyCursesWindow_in_wchstr(PyObject *op, PyObject *args)
+{
+    PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op);
+    int rtn, use_xy = 0, y = 0, x = 0;
+    unsigned int max_buf_size = 2048;
+    unsigned int n = max_buf_size - 1;
+
+    if (!curses_clinic_parse_optional_xy_n(args, &y, &x, &n, &use_xy,
+                                           "_curses.window.in_wchstr"))
+    {
+        return NULL;
+    }
+
+    n = Py_MIN(n, max_buf_size - 1);
+    cchar_t *buf = PyMem_New(cchar_t, n + 1);
+    if (buf == NULL) {
+        return PyErr_NoMemory();
+    }
+
+    if (use_xy) {
+        rtn = mvwin_wchnstr(self->win, y, x, buf, n);
+    }
+    else {
+        rtn = win_wchnstr(self->win, buf, n);
+    }
+
+    cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+    if (rtn == ERR) {
+        PyMem_Free(buf);
+        return PyCursesComplexStr_New(state, NULL, 0);
+    }
+
+    /* win_wchnstr() stores at most n cells and zero-terminates the array at
+       the actual count; every real cell holds at least a space, so the first
+       empty cell marks the end of the run. */
+    Py_ssize_t count = 0;
+    while (count < (Py_ssize_t)n) {
+        wchar_t wstr[CCHARW_MAX + 1];
+        attr_t attrs;
+        int pair;
+        if (curses_getcchar(&buf[count], wstr, &attrs, &pair) == ERR
+            || wstr[0] == L'\0')
+        {
+            break;
+        }
+        count++;
+    }
+    PyObject *res = PyCursesComplexStr_New(state, buf, count);
+    PyMem_Free(buf);
+    return res;
+}
 #endif /* HAVE_NCURSESW */
 
 /*[clinic input]
@@ -3073,6 +3602,18 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1,
     const char *funcname;
 
 #ifdef HAVE_NCURSESW
+    {
+        cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+        if (Py_IS_TYPE(str, state->complexstr_type)) {
+            if (use_attr) {
+                PyErr_SetString(PyExc_TypeError, "insstr(): attr cannot be "
+                                "specified together with a complexstr");
+                return NULL;
+            }
+            return curses_window_put_cells(self, str, use_xy, y, x,
+                                           -1, 1, "insstr");
+        }
+    }
     strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr);
 #else
     strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL);
@@ -3174,6 +3715,18 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1,
     const char *funcname;
 
 #ifdef HAVE_NCURSESW
+    {
+        cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+        if (Py_IS_TYPE(str, state->complexstr_type)) {
+            if (use_attr) {
+                PyErr_SetString(PyExc_TypeError, "insnstr(): attr cannot be "
+                                "specified together with a complexstr");
+                return NULL;
+            }
+            return curses_window_put_cells(self, str, use_xy, y, x,
+                                           n, 1, "insnstr");
+        }
+    }
     strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr);
 #else
     strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL);
@@ -3817,6 +4370,32 @@ static PyType_Spec PyCursesComplexChar_Type_spec = {
         | Py_TPFLAGS_HEAPTYPE,
     .slots = PyCursesComplexChar_Type_slots
 };
+
+static PyType_Slot PyCursesComplexStr_Type_slots[] = {
+    {Py_tp_doc, (void *)complexstr_new__doc__},
+    {Py_tp_new, complexstr_new},
+    {Py_tp_dealloc, complexstr_dealloc},
+    {Py_tp_repr, complexstr_repr},
+    {Py_tp_str, complexstr_str},
+    {Py_tp_richcompare, complexstr_richcompare},
+    {Py_tp_hash, complexstr_hash},
+    {Py_sq_length, complexstr_length},
+    {Py_sq_concat, complexstr_concat},
+    {Py_sq_item, complexstr_item},
+    {Py_mp_length, complexstr_length},
+    {Py_mp_subscript, complexstr_subscript},
+    {0, NULL}
+};
+
+static PyType_Spec PyCursesComplexStr_Type_spec = {
+    .name = "_curses.complexstr",
+    .basicsize = offsetof(PyCursesComplexStrObject, cells),
+    .itemsize = sizeof(cchar_t),
+    .flags = Py_TPFLAGS_DEFAULT
+        | Py_TPFLAGS_IMMUTABLETYPE
+        | Py_TPFLAGS_HEAPTYPE,
+    .slots = PyCursesComplexStr_Type_slots
+};
 #endif
 #undef clinic_state
 
@@ -4021,6 +4600,10 @@ static PyMethodDef PyCursesWindow_methods[] = {
         "in_wstr", PyCursesWindow_in_wstr, METH_VARARGS,
         _curses_window_in_wstr__doc__
     },
+    {
+        "in_wchstr", PyCursesWindow_in_wchstr, METH_VARARGS,
+        _curses_window_in_wchstr__doc__
+    },
 #endif
     _CURSES_WINDOW_IS_LINETOUCHED_METHODDEF
     {"is_wintouched", PyCursesWindow_is_wintouched, METH_NOARGS,
@@ -7238,6 +7821,7 @@ cursesmodule_traverse(PyObject *mod, visitproc visit, void *arg)
     Py_VISIT(state->screen_type);
 #ifdef HAVE_NCURSESW
     Py_VISIT(state->complexchar_type);
+    Py_VISIT(state->complexstr_type);
 #endif
     Py_VISIT(state->topscreen);
     return 0;
@@ -7252,6 +7836,7 @@ cursesmodule_clear(PyObject *mod)
     Py_CLEAR(state->screen_type);
 #ifdef HAVE_NCURSESW
     Py_CLEAR(state->complexchar_type);
+    Py_CLEAR(state->complexstr_type);
 #endif
     Py_CLEAR(state->topscreen);
     return 0;
@@ -7303,6 +7888,14 @@ cursesmodule_exec(PyObject *module)
     if (PyModule_AddType(module, state->complexchar_type) < 0) {
         return -1;
     }
+    state->complexstr_type = (PyTypeObject *)PyType_FromModuleAndSpec(
+        module, &PyCursesComplexStr_Type_spec, NULL);
+    if (state->complexstr_type == NULL) {
+        return -1;
+    }
+    if (PyModule_AddType(module, state->complexstr_type) < 0) {
+        return -1;
+    }
 #endif
 
     /* Add some symbolic constants to the module */
index 4a87ec3e215e995ccfacd35487f70c1ccb641200..fc48af636346c27da712facd4939aa7387ea3a30 100644 (file)
@@ -105,6 +105,97 @@ exit:
 
 #endif /* defined(HAVE_NCURSESW) */
 
+#if defined(HAVE_NCURSESW)
+
+PyDoc_STRVAR(complexstr_new__doc__,
+"complexstr(cells, /, attr=<unrepresentable>, pair=<unrepresentable>)\n"
+"--\n"
+"\n"
+"An immutable string of styled wide-character cells.\n"
+"\n"
+"  cells\n"
+"    An iterable of cells, each a complexchar or a str.\n"
+"  attr\n"
+"    Attributes applied to every cell (only with a string).\n"
+"  pair\n"
+"    Color pair applied to every cell (only with a string).\n"
+"\n"
+"It is the counterpart of complexchar for a run of cells, and the type\n"
+"returned by window.in_wchstr().  Each cell is a complexchar or a str (a\n"
+"spacing character optionally followed by combining characters).\n"
+"\n"
+"When cells is a string it is split into character cells, and attr and\n"
+"pair (if given) style every cell.  Otherwise each item carries its own\n"
+"rendition, and attr and pair must be omitted.");
+
+static PyObject *
+complexstr_new_impl(PyTypeObject *type, PyObject *cells, PyObject *attr,
+                    PyObject *pair);
+
+static PyObject *
+complexstr_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 2
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { &_Py_ID(attr), &_Py_ID(pair), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"", "attr", "pair", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "complexstr",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[3];
+    PyObject * const *fastargs;
+    Py_ssize_t nargs = PyTuple_GET_SIZE(args);
+    Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1;
+    PyObject *cells;
+    PyObject *attr = NULL;
+    PyObject *pair = NULL;
+
+    fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser,
+            /*minpos*/ 1, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!fastargs) {
+        goto exit;
+    }
+    cells = fastargs[0];
+    if (!noptargs) {
+        goto skip_optional_pos;
+    }
+    if (fastargs[1]) {
+        attr = fastargs[1];
+        if (!--noptargs) {
+            goto skip_optional_pos;
+        }
+    }
+    pair = fastargs[2];
+skip_optional_pos:
+    return_value = complexstr_new_impl(type, cells, attr, pair);
+
+exit:
+    return return_value;
+}
+
+#endif /* defined(HAVE_NCURSESW) */
+
 PyDoc_STRVAR(_curses_window_addch__doc__,
 "addch([y, x,] ch, [attr])\n"
 "Paint the character.\n"
@@ -5309,4 +5400,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored
 #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF
     #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF
 #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */
-/*[clinic end generated code: output=081cc398989ca202 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=7940d7d4775b58fd input=a9049054013a1b77]*/