Skip to content

Commit 104c397

Browse files
gh-154055: Gate optional curses functions absent on old SVr4 curses (GH-154057)
Build hygiene so the curses modules build against a limited curses (e.g. the native SVr4 curses of illumos/Solaris), matching how other optional functions are already gated: * Probe and #ifdef-gate the X/Open attr_t functions (window.attr_get/attr_set/ attr_on/attr_off/color_set), the soft-label attribute functions (slk_attr_on/off/set, slk_color) and scr_set(); scr_set is probed separately from the scr_dump family, which SVr4 has without it. * Stop gating update_lines_cols() on resizeterm(): it only reads LINES/COLS and is used unconditionally (e.g. by set_term()), so a build without resizeterm() failed to link. * On Solaris/illumos define _BOOL (and include <stdbool.h>) so the SVr4 <curses.h> "typedef char bool" does not clash with C's bool. test.test_curses: skip or guard the tests that use the now-optional functions, split test_attributes so its chtype-based part still runs, and treat the native curses of NetBSD and illumos/Solaris as having broken newterm() (they crash on repeated newterm()/delscreen(), like ncurses before 6.5). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ee50ed3 commit 104c397

8 files changed

Lines changed: 781 additions & 29 deletions

File tree

Include/py_curses.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@
4343
# define PDC_NCMOUSE
4444
#endif
4545

46+
/* On Solaris/illumos, the SVr4 <curses.h> does "typedef char bool;", which
47+
clashes with C's bool from <stdbool.h>. Define _BOOL to suppress it, and
48+
include <stdbool.h> for the bool the header then needs. ncurses ignores
49+
_BOOL. */
50+
#if defined(__sun) && !defined(_BOOL)
51+
# include <stdbool.h>
52+
# define _BOOL
53+
#endif
54+
4655
#if defined(HAVE_NCURSESW_NCURSES_H)
4756
# include <ncursesw/ncurses.h>
4857
#elif defined(HAVE_NCURSESW_CURSES_H)

Lib/test/test_curses.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,15 @@ def wrapped(self, *args, **kwargs):
8484
term = os.environ.get('TERM')
8585
SHORT_MAX = 0x7fff
8686

87-
# ncurses before 6.5 can crash on repeated newterm(). Fall back to initscr()
88-
# and skip the tests that need several screens.
87+
# ncurses before 6.5, and the native curses of NetBSD and illumos/Solaris,
88+
# crash on repeated newterm()/delscreen(); fall back to initscr() and skip the
89+
# multi-screen tests. The native ones are keyed off the platform so a fixed
90+
# version can be excluded later.
8991
_ncurses_version = getattr(curses, 'ncurses_version', None)
90-
BROKEN_NEWTERM = _ncurses_version is not None and _ncurses_version < (6, 5)
92+
if _ncurses_version is not None:
93+
BROKEN_NEWTERM = _ncurses_version < (6, 5)
94+
else:
95+
BROKEN_NEWTERM = sys.platform.startswith(('netbsd', 'sunos'))
9196
USE_NEWTERM = hasattr(curses, 'newterm') and not BROKEN_NEWTERM
9297

9398
# Older macOS reports a variation selector as a spacing character (wcwidth()
@@ -1140,8 +1145,17 @@ def test_attributes(self):
11401145
win.standout()
11411146
win.standend()
11421147

1148+
# attron()/attroff()/attrset() reject a bad attribute.
1149+
self.assertRaises(OverflowError, win.attron, 1 << 64)
1150+
self.assertRaises(OverflowError, win.attroff, -1)
1151+
self.assertRaises(OverflowError, win.attrset, 1 << 64)
1152+
self.assertRaises(TypeError, win.attron, 'x')
1153+
1154+
@requires_curses_window_meth('attr_set')
1155+
def test_attr(self):
11431156
# The attr_*() family works on attr_t attributes paired with a color
11441157
# pair, unlike the chtype-based attron()/attroff()/attrset().
1158+
win = curses.newwin(5, 15, 5, 2)
11451159
win.attr_set(curses.A_BOLD | curses.A_UNDERLINE)
11461160
attrs, pair = win.attr_get()
11471161
self.assertTrue(attrs & curses.A_BOLD)
@@ -1167,13 +1181,9 @@ def test_attributes(self):
11671181
self.assertRaises(OverflowError, win.attr_set, -1)
11681182
self.assertRaises(OverflowError, win.attr_on, -1)
11691183
self.assertRaises(OverflowError, win.attr_set, 1 << 64)
1170-
# attron()/attroff()/attrset() reject a bad attribute too.
1171-
self.assertRaises(OverflowError, win.attron, 1 << 64)
1172-
self.assertRaises(OverflowError, win.attroff, -1)
1173-
self.assertRaises(OverflowError, win.attrset, 1 << 64)
1174-
self.assertRaises(TypeError, win.attron, 'x')
11751184

11761185
@requires_colors
1186+
@requires_curses_window_meth('attr_set')
11771187
def test_attr_color_pair(self):
11781188
win = curses.newwin(5, 15, 5, 2)
11791189
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
@@ -1383,8 +1393,10 @@ def test_scr_dump(self):
13831393
stdscr.refresh()
13841394
self.assertIsNone(curses.scr_restore(dump))
13851395
# scr_init() and scr_set() also accept a dump file and return None.
1396+
# scr_set() is not available on every curses (e.g. old SVr4).
13861397
self.assertIsNone(curses.scr_init(dump))
1387-
self.assertIsNone(curses.scr_set(dump))
1398+
if hasattr(curses, 'scr_set'):
1399+
self.assertIsNone(curses.scr_set(dump))
13881400
# A bytes (path-like) filename is accepted too.
13891401
curses.scr_dump(os.fsencode(dump))
13901402
# Restoring from a missing file is an error.
@@ -1846,9 +1858,11 @@ def test_escdelay(self):
18461858
def test_tabsize(self):
18471859
tabsize = curses.get_tabsize()
18481860
self.assertIsInstance(tabsize, int)
1849-
curses.set_tabsize(4)
1850-
self.assertEqual(curses.get_tabsize(), 4)
1851-
curses.set_tabsize(tabsize)
1861+
# set_tabsize() is not available on every curses (e.g. old SVr4).
1862+
if hasattr(curses, 'set_tabsize'):
1863+
curses.set_tabsize(4)
1864+
self.assertEqual(curses.get_tabsize(), 4)
1865+
curses.set_tabsize(tabsize)
18521866

18531867
@requires_curses_func('getsyx')
18541868
def test_getsyx(self):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The :mod:`curses` and :mod:`curses.panel` modules can now be built against a
2+
curses library that lacks the X/Open ``attr_t`` and soft-label attribute
3+
functions, ``scr_set()`` or ``resizeterm()`` -- such as the native SVr4 curses
4+
of illumos and Solaris. These functions are probed and used only when present.

Modules/_cursesmodule.c

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2433,6 +2433,7 @@ _curses_window_attrset_impl(PyCursesWindowObject *self, attr_t attr)
24332433
return curses_window_check_err(self, rtn, "wattrset", "attrset");
24342434
}
24352435

2436+
#ifdef HAVE_CURSES_WATTR_GET
24362437
/*[clinic input]
24372438
_curses.window.attr_get
24382439
@@ -2458,7 +2459,9 @@ _curses_window_attr_get_impl(PyCursesWindowObject *self)
24582459
}
24592460
return Py_BuildValue("(ki)", (unsigned long)attrs, (int)pair);
24602461
}
2462+
#endif /* HAVE_CURSES_WATTR_GET */
24612463

2464+
#ifdef HAVE_CURSES_WATTR_SET
24622465
/*[clinic input]
24632466
_curses.window.attr_set
24642467
@@ -2482,7 +2485,9 @@ _curses_window_attr_set_impl(PyCursesWindowObject *self, attr_t attr,
24822485
#endif
24832486
return curses_window_check_err(self, rtn, "wattr_set", "attr_set");
24842487
}
2488+
#endif /* HAVE_CURSES_WATTR_SET */
24852489

2490+
#ifdef HAVE_CURSES_WATTR_ON
24862491
/*[clinic input]
24872492
_curses.window.attr_on
24882493
@@ -2499,7 +2504,9 @@ _curses_window_attr_on_impl(PyCursesWindowObject *self, attr_t attr)
24992504
int rtn = wattr_on(self->win, attr, NULL);
25002505
return curses_window_check_err(self, rtn, "wattr_on", "attr_on");
25012506
}
2507+
#endif /* HAVE_CURSES_WATTR_ON */
25022508

2509+
#ifdef HAVE_CURSES_WATTR_OFF
25032510
/*[clinic input]
25042511
_curses.window.attr_off
25052512
@@ -2516,7 +2523,9 @@ _curses_window_attr_off_impl(PyCursesWindowObject *self, attr_t attr)
25162523
int rtn = wattr_off(self->win, attr, NULL);
25172524
return curses_window_check_err(self, rtn, "wattr_off", "attr_off");
25182525
}
2526+
#endif /* HAVE_CURSES_WATTR_OFF */
25192527

2528+
#ifdef HAVE_CURSES_WCOLOR_SET
25202529
/*[clinic input]
25212530
_curses.window.color_set
25222531
@@ -2538,6 +2547,7 @@ _curses_window_color_set_impl(PyCursesWindowObject *self, int pair)
25382547
#endif
25392548
return curses_window_check_err(self, rtn, "wcolor_set", "color_set");
25402549
}
2550+
#endif /* HAVE_CURSES_WCOLOR_SET */
25412551

25422552
/*[clinic input]
25432553
_curses.window.getattrs
@@ -6083,6 +6093,7 @@ _curses_scr_init(PyObject *module, PyObject *filename)
60836093
/*[clinic end generated code: output=2e861d381d710419 input=81c45e4702124ef6]*/
60846094
ScreenDumpFunctionBody(scr_init)
60856095

6096+
#ifdef HAVE_CURSES_SCR_SET
60866097
/*[clinic input]
60876098
_curses.scr_set
60886099
@@ -6099,6 +6110,7 @@ static PyObject *
60996110
_curses_scr_set(PyObject *module, PyObject *filename)
61006111
/*[clinic end generated code: output=6056fdec12c5935f input=d248c20543cc289b]*/
61016112
ScreenDumpFunctionBody(scr_set)
6113+
#endif /* HAVE_CURSES_SCR_SET */
61026114
#endif /* HAVE_CURSES_SCR_DUMP */
61036115

61046116
/*[clinic input]
@@ -7462,9 +7474,10 @@ _curses_qiflush_impl(PyObject *module, int flag)
74627474
Py_RETURN_NONE;
74637475
}
74647476

7465-
#if defined(HAVE_CURSES_RESIZETERM) || defined(HAVE_CURSES_RESIZE_TERM)
74667477
/* Internal helper used for updating curses.LINES, curses.COLS, _curses.LINES
7467-
* and _curses.COLS. Returns 1 on success and 0 on failure. */
7478+
* and _curses.COLS. Returns 1 on success and 0 on failure. Used
7479+
* unconditionally (e.g. by set_term()), so it must not be gated on resizeterm().
7480+
*/
74687481
static int
74697482
update_lines_cols(PyObject *private_module)
74707483
{
@@ -7533,8 +7546,6 @@ _curses_update_lines_cols_impl(PyObject *module)
75337546
Py_RETURN_NONE;
75347547
}
75357548

7536-
#endif
7537-
75387549
/*[clinic input]
75397550
_curses.raw
75407551
@@ -8371,6 +8382,7 @@ _curses_slk_attr_impl(PyObject *module)
83718382
}
83728383
#endif
83738384

8385+
#ifdef HAVE_CURSES_SLK_ATTR_ON
83748386
/*[clinic input]
83758387
_curses.slk_attr_on
83768388
@@ -8388,7 +8400,9 @@ _curses_slk_attr_on_impl(PyObject *module, attr_t attr)
83888400
return curses_check_err(module, slk_attr_on(attr, NULL),
83898401
"slk_attr_on", NULL);
83908402
}
8403+
#endif /* HAVE_CURSES_SLK_ATTR_ON */
83918404

8405+
#ifdef HAVE_CURSES_SLK_ATTR_OFF
83928406
/*[clinic input]
83938407
_curses.slk_attr_off
83948408
@@ -8406,7 +8420,9 @@ _curses_slk_attr_off_impl(PyObject *module, attr_t attr)
84068420
return curses_check_err(module, slk_attr_off(attr, NULL),
84078421
"slk_attr_off", NULL);
84088422
}
8423+
#endif /* HAVE_CURSES_SLK_ATTR_OFF */
84098424

8425+
#ifdef HAVE_CURSES_SLK_ATTR_SET
84108426
/*[clinic input]
84118427
_curses.slk_attr_set
84128428
@@ -8430,7 +8446,9 @@ _curses_slk_attr_set_impl(PyObject *module, attr_t attr, int pair)
84308446
#endif
84318447
return curses_check_err(module, rtn, "slk_attr_set", NULL);
84328448
}
8449+
#endif /* HAVE_CURSES_SLK_ATTR_SET */
84338450

8451+
#ifdef HAVE_CURSES_SLK_COLOR
84348452
/*[clinic input]
84358453
_curses.slk_color
84368454
@@ -8447,6 +8465,7 @@ _curses_slk_color_impl(PyObject *module, int pair)
84478465
PyCursesStatefulInitialised(module);
84488466
return curses_check_err(module, slk_color((short)pair), "slk_color", NULL);
84498467
}
8468+
#endif /* HAVE_CURSES_SLK_COLOR */
84508469

84518470
#ifdef HAVE_CURSES_USE_ENV
84528471
/*[clinic input]

0 commit comments

Comments
 (0)