Skip to content

Commit b9d9c3f

Browse files
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.
1 parent 53760b3 commit b9d9c3f

4 files changed

Lines changed: 166 additions & 59 deletions

File tree

Doc/library/curses.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,10 +1585,14 @@ Borders and lines
15851585
The wide default value is used when the border is drawn from string
15861586
characters or :class:`complexchar` cells.
15871587

1588+
If any parameter is a byte character or an integer other than ``0``, the
1589+
border is drawn from byte characters, and every string character must be
1590+
encodable as a single byte.
1591+
15881592
.. versionchanged:: next
15891593
Wide and combining characters, and :class:`complexchar` cells, are now
15901594
accepted. A single call cannot mix
1591-
them with integer or byte characters.
1595+
:class:`complexchar` cells with integer or byte characters.
15921596

15931597
.. method:: window.box([vertch, horch])
15941598

@@ -1598,7 +1602,7 @@ Borders and lines
15981602
.. versionchanged:: next
15991603
Wide and combining characters, and :class:`complexchar` cells, are now
16001604
accepted. A single call cannot mix
1601-
them with integer or byte characters.
1605+
:class:`complexchar` cells with integer or byte characters.
16021606

16031607
.. method:: window.hline(ch, n[, attr])
16041608
window.hline(y, x, ch, n[, attr])

Lib/test/test_curses.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,76 @@ def test_wide_characters(self):
484484
if self._encodable(vline + hline):
485485
stdscr.border(vline, vline, hline, hline)
486486
stdscr.box(vline, hline)
487-
# border() and box() cannot mix integer and wide-string characters.
488-
self.assertRaises(TypeError, stdscr.box, vline, ord('-'))
487+
# border() and box() cannot mix a complexchar with an integer
488+
# character; a wide string character is narrowed instead, which only
489+
# works if it is a single byte.
490+
self.assertRaises(TypeError, stdscr.box,
491+
curses.complexchar(vline), ord('-'))
492+
493+
@requires_wide_build
494+
def test_border_default_characters(self):
495+
# 0 requests the default character, as an omitted argument does,
496+
# even in a border drawn with wide characters.
497+
win = curses.newwin(5, 10, 5, 2)
498+
maxy, maxx = win.getmaxyx()
499+
corners = [(0, 0), (0, maxx-1), (maxy-1, 0), (maxy-1, maxx-1)]
500+
win.border('|', '|', '-', '-', 0, 0, 0, 0)
501+
with_zeros = [win.in_wch(y, x) for y, x in corners]
502+
win.erase()
503+
win.border('|', '|', '-', '-')
504+
self.assertEqual([win.in_wch(y, x) for y, x in corners], with_zeros)
505+
win.border(0, '|', 0, '-', 0, 0, 0, 0)
506+
vline = curses.complexchar('|')
507+
hline = curses.complexchar('-')
508+
win.border(vline, vline, hline, hline, 0, 0, 0, 0)
509+
# box() takes 0 for either side, and draws the same default
510+
# characters as an omitted border() argument.
511+
win.erase()
512+
win.border('|', '|')
513+
default_corner = win.in_wch(0, 0)
514+
default_hline = win.in_wch(0, 1)
515+
win.erase()
516+
win.border(0, 0, '-', '-')
517+
default_vline = win.in_wch(1, 0)
518+
win.erase()
519+
win.box('|', 0)
520+
self.assertEqual(win.in_wch(0, 0), default_corner)
521+
self.assertEqual(win.in_wch(0, 1), default_hline)
522+
win.erase()
523+
win.box(0, '-')
524+
self.assertEqual(win.in_wch(1, 0), default_vline)
525+
win.box(vline, 0)
526+
527+
@requires_wide_build
528+
def test_border_mixed_characters(self):
529+
# Integer and bytes characters other than 0 are only drawn by the
530+
# narrow function, which draws string characters as single bytes.
531+
win = curses.newwin(5, 10, 5, 2)
532+
win.border('|', '|', '-', '-', 65, 66, 67, 68)
533+
self.assertEqual(win.instr(0, 0), b'A--------B')
534+
self.assertEqual(win.instr(1, 0), b'| |')
535+
self.assertEqual(win.instr(4, 0), b'C--------D')
536+
win.border('|', b'!')
537+
self.assertEqual(win.instr(1, 0), b'| !')
538+
# b'\0' is a byte character, not the sentinel, but the narrow function
539+
# draws a zero character as the default one.
540+
win.border('|', b'\0')
541+
# A complexchar cannot be drawn as a byte.
542+
cc = curses.complexchar('|')
543+
self.assertRaises(TypeError, win.border, cc, 65)
544+
self.assertRaises(TypeError, win.border, cc, b'!')
545+
# Neither can a string character that is not a single byte.
546+
vline = '\u2502'
547+
if len(vline.encode(win.encoding, 'replace')) != 1:
548+
self.assertRaises(OverflowError, win.border, vline, 65)
549+
# box() follows the same rules.
550+
win.box('|', 45)
551+
self.assertEqual(win.instr(1, 0), b'| |')
552+
win.box(b'|', '-')
553+
self.assertRaises(TypeError, win.box, cc, 45)
554+
self.assertRaises(TypeError, win.box, cc, b'-')
555+
if len(vline.encode(win.encoding, 'replace')) != 1:
556+
self.assertRaises(OverflowError, win.box, vline, 45)
489557

490558
@requires_wide_build
491559
def test_wacs_constants(self):

Modules/_cursesmodule.c

Lines changed: 88 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2699,21 +2699,21 @@ _curses_window_bkgdset_impl(PyCursesWindowObject *self, PyObject *ch,
26992699
/*[clinic input]
27002700
_curses.window.border
27012701
2702-
ls: object(c_default="NULL") = _curses.ACS_VLINE
2702+
ls: object(c_default="NULL") = 0
27032703
Left side.
2704-
rs: object(c_default="NULL") = _curses.ACS_VLINE
2704+
rs: object(c_default="NULL") = 0
27052705
Right side.
2706-
ts: object(c_default="NULL") = _curses.ACS_HLINE
2706+
ts: object(c_default="NULL") = 0
27072707
Top side.
2708-
bs: object(c_default="NULL") = _curses.ACS_HLINE
2708+
bs: object(c_default="NULL") = 0
27092709
Bottom side.
2710-
tl: object(c_default="NULL") = _curses.ACS_ULCORNER
2710+
tl: object(c_default="NULL") = 0
27112711
Upper-left corner.
2712-
tr: object(c_default="NULL") = _curses.ACS_URCORNER
2712+
tr: object(c_default="NULL") = 0
27132713
Upper-right corner.
2714-
bl: object(c_default="NULL") = _curses.ACS_LLCORNER
2714+
bl: object(c_default="NULL") = 0
27152715
Bottom-left corner.
2716-
br: object(c_default="NULL") = _curses.ACS_LRCORNER
2716+
br: object(c_default="NULL") = 0
27172717
Bottom-right corner.
27182718
/
27192719
@@ -2730,7 +2730,7 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls,
27302730
PyObject *rs, PyObject *ts, PyObject *bs,
27312731
PyObject *tl, PyObject *tr, PyObject *bl,
27322732
PyObject *br)
2733-
/*[clinic end generated code: output=670ef38d3d7c2aa3 input=42568c1458221d24]*/
2733+
/*[clinic end generated code: output=670ef38d3d7c2aa3 input=d826ce9d6335479a]*/
27342734
{
27352735
chtype ch[8];
27362736
int i, rtn;
@@ -2743,36 +2743,49 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls,
27432743
#ifdef HAVE_NCURSESW
27442744
cchar_t wch[8];
27452745
const cchar_t *wch_p[8];
2746-
int use_wide = 0;
2747-
int types[8];
2746+
/* Only wborder_set() draws a complexchar and only wborder() an integer
2747+
or bytes character; a string character suits both, and so does the
2748+
integer 0, which asks for the default character. */
2749+
int has_narrow = 0, has_str = 0, has_cchar = 0;
27482750
for (i = 0; i < 8; i++) {
2749-
types[i] = 0;
2751+
wch_p[i] = NULL; /* use the default character */
27502752
if (objs[i] != NULL) {
2751-
types[i] = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0,
2753+
int type = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0,
27522754
"border", &ch[i], &wch[i]);
2753-
if (types[i] == 0) {
2755+
if (type == 0) {
27542756
return NULL;
27552757
}
2756-
if (types[i] == 2) {
2757-
use_wide = 1;
2758+
if (type == 2) {
2759+
wch_p[i] = &wch[i];
2760+
if (PyUnicode_Check(objs[i])) {
2761+
has_str = 1;
2762+
}
2763+
else {
2764+
has_cchar = 1;
2765+
}
2766+
}
2767+
else if (!PyLong_CheckExact(objs[i]) || ch[i] != 0) {
2768+
has_narrow = 1; /* b'\0' is a byte character, not the 0 */
27582769
}
27592770
}
27602771
}
2761-
if (use_wide) {
2772+
if (has_narrow) {
2773+
if (has_cchar) {
2774+
PyErr_SetString(PyExc_TypeError,
2775+
"border() cannot mix complexchar characters "
2776+
"with integer or bytes characters");
2777+
return NULL;
2778+
}
2779+
/* Narrow the string characters. */
27622780
for (i = 0; i < 8; i++) {
2763-
if (objs[i] == NULL) {
2764-
wch_p[i] = NULL; /* use the default character */
2765-
}
2766-
else if (types[i] == 2) {
2767-
wch_p[i] = &wch[i];
2768-
}
2769-
else {
2770-
PyErr_SetString(PyExc_TypeError,
2771-
"border() cannot mix integer or bytes "
2772-
"characters with wide string characters");
2781+
if (objs[i] != NULL && PyUnicode_Check(objs[i]) &&
2782+
!PyCurses_ConvertToChtype(self, objs[i], &ch[i]))
2783+
{
27732784
return NULL;
27742785
}
27752786
}
2787+
}
2788+
else if (has_str || has_cchar) {
27762789
rtn = wborder_set(self->win,
27772790
wch_p[0], wch_p[1], wch_p[2], wch_p[3],
27782791
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,
28152828
PyObject *verch, PyObject *horch)
28162829
/*[clinic end generated code: output=f3fcb038bb287192 input=e11acb7dbf6790b6]*/
28172830
{
2818-
chtype ch1 = 0, ch2 = 0;
2831+
chtype ch[2] = {0, 0};
2832+
PyObject *objs[2] = {verch, horch};
2833+
int i;
28192834
#ifdef HAVE_NCURSESW
2820-
cchar_t wch1, wch2;
2821-
int t1 = 0, t2 = 0;
2835+
cchar_t wch[2];
2836+
const cchar_t *wch_p[2] = {NULL, NULL};
2837+
int has_narrow = 0, has_str = 0, has_cchar = 0;
28222838
if (group_right_1) {
2823-
t1 = PyCurses_ConvertToCell(self, verch, A_NORMAL, 0, "box", &ch1, &wch1);
2824-
if (t1 == 0) {
2825-
return NULL;
2826-
}
2827-
t2 = PyCurses_ConvertToCell(self, horch, A_NORMAL, 0, "box", &ch2, &wch2);
2828-
if (t2 == 0) {
2829-
return NULL;
2839+
for (i = 0; i < 2; i++) {
2840+
int type = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0,
2841+
"box", &ch[i], &wch[i]);
2842+
if (type == 0) {
2843+
return NULL;
2844+
}
2845+
if (type == 2) {
2846+
wch_p[i] = &wch[i];
2847+
if (PyUnicode_Check(objs[i])) {
2848+
has_str = 1;
2849+
}
2850+
else {
2851+
has_cchar = 1;
2852+
}
2853+
}
2854+
else if (!PyLong_CheckExact(objs[i]) || ch[i] != 0) {
2855+
has_narrow = 1; /* b'\0' is a byte character, not the 0 */
2856+
}
28302857
}
28312858
}
2832-
if (t1 == 2 || t2 == 2) {
2833-
if (t1 != 2 || t2 != 2) {
2859+
if (has_narrow) {
2860+
if (has_cchar) {
28342861
PyErr_SetString(PyExc_TypeError,
2835-
"box() cannot mix integer or bytes characters "
2836-
"with wide string characters");
2862+
"box() cannot mix complexchar characters "
2863+
"with integer or bytes characters");
28372864
return NULL;
28382865
}
2839-
int rtn = wborder_set(self->win, &wch1, &wch1, &wch2, &wch2,
2840-
NULL, NULL, NULL, NULL);
2841-
return curses_window_check_err(self, rtn, "wborder_set", "box");
2866+
/* Narrow the string characters. */
2867+
for (i = 0; i < 2; i++) {
2868+
if (PyUnicode_Check(objs[i]) &&
2869+
!PyCurses_ConvertToChtype(self, objs[i], &ch[i]))
2870+
{
2871+
return NULL;
2872+
}
2873+
}
2874+
}
2875+
else if (has_str || has_cchar) {
2876+
int rtn = box_set(self->win, wch_p[0], wch_p[1]);
2877+
return curses_window_check_err(self, rtn, "box_set", "box");
28422878
}
28432879
#else
28442880
if (group_right_1) {
2845-
if (!PyCurses_ConvertToCell(self, verch, A_NORMAL, 0, "box", &ch1)) {
2846-
return NULL;
2847-
}
2848-
if (!PyCurses_ConvertToCell(self, horch, A_NORMAL, 0, "box", &ch2)) {
2849-
return NULL;
2881+
for (i = 0; i < 2; i++) {
2882+
if (!PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0, "box",
2883+
&ch[i]))
2884+
{
2885+
return NULL;
2886+
}
28502887
}
28512888
}
28522889
#endif
2853-
return curses_window_check_err(self, box(self->win, ch1, ch2), "box", NULL);
2890+
return curses_window_check_err(self, box(self->win, ch[0], ch[1]),
2891+
"box", NULL);
28542892
}
28552893

28562894
#if defined(HAVE_NCURSES_H) || defined(MVWDELCH_IS_EXPRESSION)

Modules/clinic/_cursesmodule.c.h

Lines changed: 2 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)