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]