Skip to content

Commit 453ee06

Browse files
gh-154055: Gate optional curses functions absent on old SVr4 curses
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 4b60b46 commit 453ee06

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()
@@ -1143,8 +1148,17 @@ def test_attributes(self):
11431148
win.standout()
11441149
win.standend()
11451150

1151+
# attron()/attroff()/attrset() reject a bad attribute.
1152+
self.assertRaises(OverflowError, win.attron, 1 << 64)
1153+
self.assertRaises(OverflowError, win.attroff, -1)
1154+
self.assertRaises(OverflowError, win.attrset, 1 << 64)
1155+
self.assertRaises(TypeError, win.attron, 'x')
1156+
1157+
@requires_curses_window_meth('attr_set')
1158+
def test_attr(self):
11461159
# The attr_*() family works on attr_t attributes paired with a color
11471160
# pair, unlike the chtype-based attron()/attroff()/attrset().
1161+
win = curses.newwin(5, 15, 5, 2)
11481162
win.attr_set(curses.A_BOLD | curses.A_UNDERLINE)
11491163
attrs, pair = win.attr_get()
11501164
self.assertTrue(attrs & curses.A_BOLD)
@@ -1170,13 +1184,9 @@ def test_attributes(self):
11701184
self.assertRaises(OverflowError, win.attr_set, -1)
11711185
self.assertRaises(OverflowError, win.attr_on, -1)
11721186
self.assertRaises(OverflowError, win.attr_set, 1 << 64)
1173-
# attron()/attroff()/attrset() reject a bad attribute too.
1174-
self.assertRaises(OverflowError, win.attron, 1 << 64)
1175-
self.assertRaises(OverflowError, win.attroff, -1)
1176-
self.assertRaises(OverflowError, win.attrset, 1 << 64)
1177-
self.assertRaises(TypeError, win.attron, 'x')
11781187

11791188
@requires_colors
1189+
@requires_curses_window_meth('attr_set')
11801190
def test_attr_color_pair(self):
11811191
win = curses.newwin(5, 15, 5, 2)
11821192
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
@@ -1386,8 +1396,10 @@ def test_scr_dump(self):
13861396
stdscr.refresh()
13871397
self.assertIsNone(curses.scr_restore(dump))
13881398
# scr_init() and scr_set() also accept a dump file and return None.
1399+
# scr_set() is not available on every curses (e.g. old SVr4).
13891400
self.assertIsNone(curses.scr_init(dump))
1390-
self.assertIsNone(curses.scr_set(dump))
1401+
if hasattr(curses, 'scr_set'):
1402+
self.assertIsNone(curses.scr_set(dump))
13911403
# A bytes (path-like) filename is accepted too.
13921404
curses.scr_dump(os.fsencode(dump))
13931405
# Restoring from a missing file is an error.
@@ -1849,9 +1861,11 @@ def test_escdelay(self):
18491861
def test_tabsize(self):
18501862
tabsize = curses.get_tabsize()
18511863
self.assertIsInstance(tabsize, int)
1852-
curses.set_tabsize(4)
1853-
self.assertEqual(curses.get_tabsize(), 4)
1854-
curses.set_tabsize(tabsize)
1864+
# set_tabsize() is not available on every curses (e.g. old SVr4).
1865+
if hasattr(curses, 'set_tabsize'):
1866+
curses.set_tabsize(4)
1867+
self.assertEqual(curses.get_tabsize(), 4)
1868+
curses.set_tabsize(tabsize)
18551869

18561870
@requires_curses_func('getsyx')
18571871
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
@@ -6065,6 +6075,7 @@ _curses_scr_init(PyObject *module, PyObject *filename)
60656075
/*[clinic end generated code: output=2e861d381d710419 input=81c45e4702124ef6]*/
60666076
ScreenDumpFunctionBody(scr_init)
60676077

6078+
#ifdef HAVE_CURSES_SCR_SET
60686079
/*[clinic input]
60696080
_curses.scr_set
60706081
@@ -6081,6 +6092,7 @@ static PyObject *
60816092
_curses_scr_set(PyObject *module, PyObject *filename)
60826093
/*[clinic end generated code: output=6056fdec12c5935f input=d248c20543cc289b]*/
60836094
ScreenDumpFunctionBody(scr_set)
6095+
#endif /* HAVE_CURSES_SCR_SET */
60846096
#endif /* HAVE_CURSES_SCR_DUMP */
60856097

60866098
/*[clinic input]
@@ -7444,9 +7456,10 @@ _curses_qiflush_impl(PyObject *module, int flag)
74447456
Py_RETURN_NONE;
74457457
}
74467458

7447-
#if defined(HAVE_CURSES_RESIZETERM) || defined(HAVE_CURSES_RESIZE_TERM)
74487459
/* Internal helper used for updating curses.LINES, curses.COLS, _curses.LINES
7449-
* and _curses.COLS. Returns 1 on success and 0 on failure. */
7460+
* and _curses.COLS. Returns 1 on success and 0 on failure. Used
7461+
* unconditionally (e.g. by set_term()), so it must not be gated on resizeterm().
7462+
*/
74507463
static int
74517464
update_lines_cols(PyObject *private_module)
74527465
{
@@ -7515,8 +7528,6 @@ _curses_update_lines_cols_impl(PyObject *module)
75157528
Py_RETURN_NONE;
75167529
}
75177530

7518-
#endif
7519-
75207531
/*[clinic input]
75217532
_curses.raw
75227533
@@ -8353,6 +8364,7 @@ _curses_slk_attr_impl(PyObject *module)
83538364
}
83548365
#endif
83558366

8367+
#ifdef HAVE_CURSES_SLK_ATTR_ON
83568368
/*[clinic input]
83578369
_curses.slk_attr_on
83588370
@@ -8370,7 +8382,9 @@ _curses_slk_attr_on_impl(PyObject *module, attr_t attr)
83708382
return curses_check_err(module, slk_attr_on(attr, NULL),
83718383
"slk_attr_on", NULL);
83728384
}
8385+
#endif /* HAVE_CURSES_SLK_ATTR_ON */
83738386

8387+
#ifdef HAVE_CURSES_SLK_ATTR_OFF
83748388
/*[clinic input]
83758389
_curses.slk_attr_off
83768390
@@ -8388,7 +8402,9 @@ _curses_slk_attr_off_impl(PyObject *module, attr_t attr)
83888402
return curses_check_err(module, slk_attr_off(attr, NULL),
83898403
"slk_attr_off", NULL);
83908404
}
8405+
#endif /* HAVE_CURSES_SLK_ATTR_OFF */
83918406

8407+
#ifdef HAVE_CURSES_SLK_ATTR_SET
83928408
/*[clinic input]
83938409
_curses.slk_attr_set
83948410
@@ -8412,7 +8428,9 @@ _curses_slk_attr_set_impl(PyObject *module, attr_t attr, int pair)
84128428
#endif
84138429
return curses_check_err(module, rtn, "slk_attr_set", NULL);
84148430
}
8431+
#endif /* HAVE_CURSES_SLK_ATTR_SET */
84158432

8433+
#ifdef HAVE_CURSES_SLK_COLOR
84168434
/*[clinic input]
84178435
_curses.slk_color
84188436
@@ -8429,6 +8447,7 @@ _curses_slk_color_impl(PyObject *module, int pair)
84298447
PyCursesStatefulInitialised(module);
84308448
return curses_check_err(module, slk_color((short)pair), "slk_color", NULL);
84318449
}
8450+
#endif /* HAVE_CURSES_SLK_COLOR */
84328451

84338452
#ifdef HAVE_CURSES_USE_ENV
84348453
/*[clinic input]

0 commit comments

Comments
 (0)