https://github.com/python/cpython/commit/b9d9c3f99c7b1847659ea55ff1d37530a3d5740a
commit: b9d9c3f99c7b1847659ea55ff1d37530a3d5740a
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-21T07:20:21Z
summary:

gh-155499: Fix curses window.border() and window.box() with 0 (GH-155871)

The integer 0 asks for the default character, but it was rejected when
passed together with a string character.  Choose the drawing function by
what the arguments need: only a complexchar cannot be drawn as a byte
character, and a string character is narrowed when one is needed.

Document the defaults as 0 rather than as the ACS_* codes, which are not
defined before initscr().

* Remove the NEWS entry

The regression is not in any release: wide characters in border() and box()
are new in 3.16.

files:
M Doc/library/curses.rst
M Lib/test/test_curses.py
M Modules/_cursesmodule.c
M Modules/clinic/_cursesmodule.c.h

diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst
index e5face72d1e5c9..c1afdb71c89e88 100644
--- a/Doc/library/curses.rst
+++ b/Doc/library/curses.rst
@@ -1585,10 +1585,14 @@ Borders and lines
    The wide default value is used when the border is drawn from string
    characters or :class:`complexchar` cells.
 
+   If any parameter is a byte character or an integer other than ``0``, the
+   border is drawn from byte characters, and every string character must be
+   encodable as a single byte.
+
    .. versionchanged:: next
       Wide and combining characters, and :class:`complexchar` cells, are now
       accepted.  A single call cannot mix
-      them with integer or byte characters.
+      :class:`complexchar` cells with integer or byte characters.
 
 .. method:: window.box([vertch, horch])
 
@@ -1598,7 +1602,7 @@ Borders and lines
    .. versionchanged:: next
       Wide and combining characters, and :class:`complexchar` cells, are now
       accepted.  A single call cannot mix
-      them with integer or byte characters.
+      :class:`complexchar` cells with integer or byte characters.
 
 .. method:: window.hline(ch, n[, attr])
             window.hline(y, x, ch, n[, attr])
diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index 6a7a8e320438f8..ac0110c671455c 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -484,8 +484,76 @@ def test_wide_characters(self):
         if self._encodable(vline + hline):
             stdscr.border(vline, vline, hline, hline)
             stdscr.box(vline, hline)
-        # border() and box() cannot mix integer and wide-string characters.
-        self.assertRaises(TypeError, stdscr.box, vline, ord('-'))
+        # border() and box() cannot mix a complexchar with an integer
+        # character; a wide string character is narrowed instead, which only
+        # works if it is a single byte.
+        self.assertRaises(TypeError, stdscr.box,
+                          curses.complexchar(vline), ord('-'))
+
+    @requires_wide_build
+    def test_border_default_characters(self):
+        # 0 requests the default character, as an omitted argument does,
+        # even in a border drawn with wide characters.
+        win = curses.newwin(5, 10, 5, 2)
+        maxy, maxx = win.getmaxyx()
+        corners = [(0, 0), (0, maxx-1), (maxy-1, 0), (maxy-1, maxx-1)]
+        win.border('|', '|', '-', '-', 0, 0, 0, 0)
+        with_zeros = [win.in_wch(y, x) for y, x in corners]
+        win.erase()
+        win.border('|', '|', '-', '-')
+        self.assertEqual([win.in_wch(y, x) for y, x in corners], with_zeros)
+        win.border(0, '|', 0, '-', 0, 0, 0, 0)
+        vline = curses.complexchar('|')
+        hline = curses.complexchar('-')
+        win.border(vline, vline, hline, hline, 0, 0, 0, 0)
+        # box() takes 0 for either side, and draws the same default
+        # characters as an omitted border() argument.
+        win.erase()
+        win.border('|', '|')
+        default_corner = win.in_wch(0, 0)
+        default_hline = win.in_wch(0, 1)
+        win.erase()
+        win.border(0, 0, '-', '-')
+        default_vline = win.in_wch(1, 0)
+        win.erase()
+        win.box('|', 0)
+        self.assertEqual(win.in_wch(0, 0), default_corner)
+        self.assertEqual(win.in_wch(0, 1), default_hline)
+        win.erase()
+        win.box(0, '-')
+        self.assertEqual(win.in_wch(1, 0), default_vline)
+        win.box(vline, 0)
+
+    @requires_wide_build
+    def test_border_mixed_characters(self):
+        # Integer and bytes characters other than 0 are only drawn by the
+        # narrow function, which draws string characters as single bytes.
+        win = curses.newwin(5, 10, 5, 2)
+        win.border('|', '|', '-', '-', 65, 66, 67, 68)
+        self.assertEqual(win.instr(0, 0), b'A--------B')
+        self.assertEqual(win.instr(1, 0), b'|        |')
+        self.assertEqual(win.instr(4, 0), b'C--------D')
+        win.border('|', b'!')
+        self.assertEqual(win.instr(1, 0), b'|        !')
+        # b'\0' is a byte character, not the sentinel, but the narrow function
+        # draws a zero character as the default one.
+        win.border('|', b'\0')
+        # A complexchar cannot be drawn as a byte.
+        cc = curses.complexchar('|')
+        self.assertRaises(TypeError, win.border, cc, 65)
+        self.assertRaises(TypeError, win.border, cc, b'!')
+        # Neither can a string character that is not a single byte.
+        vline = '\u2502'
+        if len(vline.encode(win.encoding, 'replace')) != 1:
+            self.assertRaises(OverflowError, win.border, vline, 65)
+        # box() follows the same rules.
+        win.box('|', 45)
+        self.assertEqual(win.instr(1, 0), b'|        |')
+        win.box(b'|', '-')
+        self.assertRaises(TypeError, win.box, cc, 45)
+        self.assertRaises(TypeError, win.box, cc, b'-')
+        if len(vline.encode(win.encoding, 'replace')) != 1:
+            self.assertRaises(OverflowError, win.box, vline, 45)
 
     @requires_wide_build
     def test_wacs_constants(self):
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index ea399e25213aef..e208b2d3d52acc 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -2699,21 +2699,21 @@ _curses_window_bkgdset_impl(PyCursesWindowObject *self, 
PyObject *ch,
 /*[clinic input]
 _curses.window.border
 
-    ls: object(c_default="NULL") = _curses.ACS_VLINE
+    ls: object(c_default="NULL") = 0
         Left side.
-    rs: object(c_default="NULL") = _curses.ACS_VLINE
+    rs: object(c_default="NULL") = 0
         Right side.
-    ts: object(c_default="NULL") = _curses.ACS_HLINE
+    ts: object(c_default="NULL") = 0
         Top side.
-    bs: object(c_default="NULL") = _curses.ACS_HLINE
+    bs: object(c_default="NULL") = 0
         Bottom side.
-    tl: object(c_default="NULL") = _curses.ACS_ULCORNER
+    tl: object(c_default="NULL") = 0
         Upper-left corner.
-    tr: object(c_default="NULL") = _curses.ACS_URCORNER
+    tr: object(c_default="NULL") = 0
         Upper-right corner.
-    bl: object(c_default="NULL") = _curses.ACS_LLCORNER
+    bl: object(c_default="NULL") = 0
         Bottom-left corner.
-    br: object(c_default="NULL") = _curses.ACS_LRCORNER
+    br: object(c_default="NULL") = 0
         Bottom-right corner.
     /
 
@@ -2730,7 +2730,7 @@ _curses_window_border_impl(PyCursesWindowObject *self, 
PyObject *ls,
                            PyObject *rs, PyObject *ts, PyObject *bs,
                            PyObject *tl, PyObject *tr, PyObject *bl,
                            PyObject *br)
-/*[clinic end generated code: output=670ef38d3d7c2aa3 input=42568c1458221d24]*/
+/*[clinic end generated code: output=670ef38d3d7c2aa3 input=d826ce9d6335479a]*/
 {
     chtype ch[8];
     int i, rtn;
@@ -2743,36 +2743,49 @@ _curses_window_border_impl(PyCursesWindowObject *self, 
PyObject *ls,
 #ifdef HAVE_NCURSESW
     cchar_t wch[8];
     const cchar_t *wch_p[8];
-    int use_wide = 0;
-    int types[8];
+    /* Only wborder_set() draws a complexchar and only wborder() an integer
+       or bytes character; a string character suits both, and so does the
+       integer 0, which asks for the default character. */
+    int has_narrow = 0, has_str = 0, has_cchar = 0;
     for (i = 0; i < 8; i++) {
-        types[i] = 0;
+        wch_p[i] = NULL;  /* use the default character */
         if (objs[i] != NULL) {
-            types[i] = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0,
+            int type = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0,
                                               "border", &ch[i], &wch[i]);
-            if (types[i] == 0) {
+            if (type == 0) {
                 return NULL;
             }
-            if (types[i] == 2) {
-                use_wide = 1;
+            if (type == 2) {
+                wch_p[i] = &wch[i];
+                if (PyUnicode_Check(objs[i])) {
+                    has_str = 1;
+                }
+                else {
+                    has_cchar = 1;
+                }
+            }
+            else if (!PyLong_CheckExact(objs[i]) || ch[i] != 0) {
+                has_narrow = 1;  /* b'\0' is a byte character, not the 0 */
             }
         }
     }
-    if (use_wide) {
+    if (has_narrow) {
+        if (has_cchar) {
+            PyErr_SetString(PyExc_TypeError,
+                            "border() cannot mix complexchar characters "
+                            "with integer or bytes characters");
+            return NULL;
+        }
+        /* Narrow the string characters. */
         for (i = 0; i < 8; i++) {
-            if (objs[i] == NULL) {
-                wch_p[i] = NULL;  /* use the default character */
-            }
-            else if (types[i] == 2) {
-                wch_p[i] = &wch[i];
-            }
-            else {
-                PyErr_SetString(PyExc_TypeError,
-                                "border() cannot mix integer or bytes "
-                                "characters with wide string characters");
+            if (objs[i] != NULL && PyUnicode_Check(objs[i]) &&
+                !PyCurses_ConvertToChtype(self, objs[i], &ch[i]))
+            {
                 return NULL;
             }
         }
+    }
+    else if (has_str || has_cchar) {
         rtn = wborder_set(self->win,
                           wch_p[0], wch_p[1], wch_p[2], wch_p[3],
                           wch_p[4], wch_p[5], wch_p[6], wch_p[7]);
@@ -2815,42 +2828,67 @@ _curses_window_box_impl(PyCursesWindowObject *self, int 
group_right_1,
                         PyObject *verch, PyObject *horch)
 /*[clinic end generated code: output=f3fcb038bb287192 input=e11acb7dbf6790b6]*/
 {
-    chtype ch1 = 0, ch2 = 0;
+    chtype ch[2] = {0, 0};
+    PyObject *objs[2] = {verch, horch};
+    int i;
 #ifdef HAVE_NCURSESW
-    cchar_t wch1, wch2;
-    int t1 = 0, t2 = 0;
+    cchar_t wch[2];
+    const cchar_t *wch_p[2] = {NULL, NULL};
+    int has_narrow = 0, has_str = 0, has_cchar = 0;
     if (group_right_1) {
-        t1 = PyCurses_ConvertToCell(self, verch, A_NORMAL, 0, "box", &ch1, 
&wch1);
-        if (t1 == 0) {
-            return NULL;
-        }
-        t2 = PyCurses_ConvertToCell(self, horch, A_NORMAL, 0, "box", &ch2, 
&wch2);
-        if (t2 == 0) {
-            return NULL;
+        for (i = 0; i < 2; i++) {
+            int type = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0,
+                                              "box", &ch[i], &wch[i]);
+            if (type == 0) {
+                return NULL;
+            }
+            if (type == 2) {
+                wch_p[i] = &wch[i];
+                if (PyUnicode_Check(objs[i])) {
+                    has_str = 1;
+                }
+                else {
+                    has_cchar = 1;
+                }
+            }
+            else if (!PyLong_CheckExact(objs[i]) || ch[i] != 0) {
+                has_narrow = 1;  /* b'\0' is a byte character, not the 0 */
+            }
         }
     }
-    if (t1 == 2 || t2 == 2) {
-        if (t1 != 2 || t2 != 2) {
+    if (has_narrow) {
+        if (has_cchar) {
             PyErr_SetString(PyExc_TypeError,
-                            "box() cannot mix integer or bytes characters "
-                            "with wide string characters");
+                            "box() cannot mix complexchar characters "
+                            "with integer or bytes characters");
             return NULL;
         }
-        int rtn = wborder_set(self->win, &wch1, &wch1, &wch2, &wch2,
-                              NULL, NULL, NULL, NULL);
-        return curses_window_check_err(self, rtn, "wborder_set", "box");
+        /* Narrow the string characters. */
+        for (i = 0; i < 2; i++) {
+            if (PyUnicode_Check(objs[i]) &&
+                !PyCurses_ConvertToChtype(self, objs[i], &ch[i]))
+            {
+                return NULL;
+            }
+        }
+    }
+    else if (has_str || has_cchar) {
+        int rtn = box_set(self->win, wch_p[0], wch_p[1]);
+        return curses_window_check_err(self, rtn, "box_set", "box");
     }
 #else
     if (group_right_1) {
-        if (!PyCurses_ConvertToCell(self, verch, A_NORMAL, 0, "box", &ch1)) {
-            return NULL;
-        }
-        if (!PyCurses_ConvertToCell(self, horch, A_NORMAL, 0, "box", &ch2)) {
-            return NULL;
+        for (i = 0; i < 2; i++) {
+            if (!PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0, "box",
+                                        &ch[i]))
+            {
+                return NULL;
+            }
         }
     }
 #endif
-    return curses_window_check_err(self, box(self->win, ch1, ch2), "box", 
NULL);
+    return curses_window_check_err(self, box(self->win, ch[0], ch[1]),
+                                   "box", NULL);
 }
 
 #if defined(HAVE_NCURSES_H) || defined(MVWDELCH_IS_EXPRESSION)
diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h
index b4cb294e3bb61a..d2f30178b1c33c 100644
--- a/Modules/clinic/_cursesmodule.c.h
+++ b/Modules/clinic/_cursesmodule.c.h
@@ -757,10 +757,7 @@ _curses_window_bkgdset(PyObject *self, PyObject *args)
 }
 
 PyDoc_STRVAR(_curses_window_border__doc__,
-"border($self, ls=_curses.ACS_VLINE, rs=_curses.ACS_VLINE,\n"
-"       ts=_curses.ACS_HLINE, bs=_curses.ACS_HLINE,\n"
-"       tl=_curses.ACS_ULCORNER, tr=_curses.ACS_URCORNER,\n"
-"       bl=_curses.ACS_LLCORNER, br=_curses.ACS_LRCORNER, /)\n"
+"border($self, ls=0, rs=0, ts=0, bs=0, tl=0, tr=0, bl=0, br=0, /)\n"
 "--\n"
 "\n"
 "Draw a border around the edges of the window.\n"
@@ -6585,4 +6582,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=680f621e7c1f101b input=a9049054013a1b77]*/
+/*[clinic end generated code: output=4e98ddbfb69f2c04 input=a9049054013a1b77]*/

_______________________________________________
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