gh-151774: Add curses dynamic color-pair functions (GH-151775)

Add alloc_pair(), find_pair(), free_pair() and reset_color_pairs(),
wrapping the ncurses extended-color dynamic pair management.  They are
available only when built against a wide-character ncurses with
extended-color support.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serhiy Storchaka 2026-06-24 21:24:11 +03:00 committed by GitHub
parent c7faa6936e
commit 3cd4283ba6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 374 additions and 1 deletions

View file

@ -85,6 +85,20 @@ The module :mod:`!curses` defines the following functions:
.. versionadded:: 3.14
.. function:: alloc_pair(fg, bg)
Allocate a color pair for foreground color *fg* and background color *bg*,
and return its number. If a color pair for the same combination of colors
already exists, return its number. Otherwise allocate a new color pair and
return its number.
This function is only available if Python was built against a wide-character
version of the underlying curses library with extended-color support (see
:func:`has_extended_color_support`).
.. versionadded:: next
.. function:: baudrate()
Return the output speed of the terminal in bits per second. On software
@ -226,6 +240,19 @@ The module :mod:`!curses` defines the following functions:
.. versionadded:: next
.. function:: find_pair(fg, bg)
Return the number of a color pair for foreground color *fg* and background
color *bg*, or ``-1`` if no color pair for this combination of colors has
been allocated.
This function is only available if Python was built against a wide-character
version of the underlying curses library with extended-color support (see
:func:`has_extended_color_support`).
.. versionadded:: next
.. function:: flash()
Flash the screen. That is, change it to reverse-video and then change it back
@ -239,6 +266,18 @@ The module :mod:`!curses` defines the following functions:
by the user and has not yet been processed by the program.
.. function:: free_pair(pair_number)
Free the color pair *pair_number*, which must have been allocated by
:func:`alloc_pair`. The pair must not be in use.
This function is only available if Python was built against a wide-character
version of the underlying curses library with extended-color support (see
:func:`has_extended_color_support`).
.. versionadded:: next
.. function:: getmouse()
After :meth:`~window.getch` returns :const:`KEY_MOUSE` to signal a mouse event, this
@ -570,6 +609,18 @@ The module :mod:`!curses` defines the following functions:
presented to curses input functions one by one.
.. function:: reset_color_pairs()
Discard all color-pair definitions, releasing the color pairs allocated by
:func:`init_pair` and :func:`alloc_pair`.
This function is only available if Python was built against a wide-character
version of the underlying curses library with extended-color support (see
:func:`has_extended_color_support`).
.. versionadded:: next
.. function:: reset_prog_mode()
Restore the terminal to "program" mode, as previously saved by

View file

@ -118,6 +118,13 @@ curses
* Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`.
(Contributed by Serhiy Storchaka in :gh:`151744`.)
* Add the :mod:`curses` functions :func:`curses.alloc_pair`,
:func:`curses.find_pair`, :func:`curses.free_pair` and
:func:`curses.reset_color_pairs` for dynamic color-pair management,
available when built against a wide-character ncurses with extended-color
support.
(Contributed by Serhiy Storchaka in :gh:`151774`.)
gzip
----

View file

@ -1209,6 +1209,54 @@ def test_init_pair(self):
self.assertRaises(ValueError, curses.init_pair, 1, color, 0)
self.assertRaises(ValueError, curses.init_pair, 1, 0, color)
@requires_curses_func('alloc_pair')
@requires_colors
def test_dynamic_color_pairs(self):
# alloc_pair()/find_pair()/free_pair() (extended-color extension).
fg = bg = curses.COLORS - 1
pair = curses.alloc_pair(fg, bg)
self.assertGreater(pair, 0)
self.assertEqual(curses.pair_content(pair), (fg, bg))
# The same combination of colors reuses the same pair.
self.assertEqual(curses.alloc_pair(fg, bg), pair)
self.assertEqual(curses.find_pair(fg, bg), pair)
# Once freed, the pair is no longer found.
self.assertIsNone(curses.free_pair(pair))
self.assertEqual(curses.find_pair(fg, bg), -1)
# Error paths.
for color in self.bad_colors2():
self.assertRaises(ValueError, curses.alloc_pair, color, 0)
self.assertRaises(ValueError, curses.alloc_pair, 0, color)
self.assertRaises(ValueError, curses.find_pair, color, 0)
self.assertRaises(ValueError, curses.find_pair, 0, color)
for pair in self.bad_pairs():
self.assertRaises(ValueError, curses.free_pair, pair)
# Color pair 0 is reserved and cannot be freed.
self.assertRaises(curses.error, curses.free_pair, 0)
# Invalid number or type of arguments.
self.assertRaises(TypeError, curses.alloc_pair)
self.assertRaises(TypeError, curses.alloc_pair, 0)
self.assertRaises(TypeError, curses.alloc_pair, 0, 0, 0)
self.assertRaises(TypeError, curses.alloc_pair, 'red', 0)
self.assertRaises(TypeError, curses.alloc_pair, 0, 'red')
self.assertRaises(TypeError, curses.alloc_pair, fg=0, bg=0)
self.assertRaises(TypeError, curses.find_pair)
self.assertRaises(TypeError, curses.find_pair, 0)
self.assertRaises(TypeError, curses.find_pair, 0, 0, 0)
self.assertRaises(TypeError, curses.find_pair, 'red', 0)
self.assertRaises(TypeError, curses.find_pair, 0, 'red')
self.assertRaises(TypeError, curses.free_pair)
self.assertRaises(TypeError, curses.free_pair, 1, 2)
self.assertRaises(TypeError, curses.free_pair, 'red')
@requires_curses_func('reset_color_pairs')
@requires_colors
def test_reset_color_pairs(self):
self.assertIsNone(curses.reset_color_pairs())
self.assertRaises(TypeError, curses.reset_color_pairs, 0)
@requires_colors
def test_color_attrs(self):
for pair in 0, 1, 255:

View file

@ -0,0 +1,5 @@
Add the :mod:`curses` functions :func:`curses.alloc_pair`,
:func:`curses.find_pair`, :func:`curses.free_pair` and
:func:`curses.reset_color_pairs` for dynamic color-pair management. They are
only available when Python is built against a wide-character version of the
underlying curses library with extended-color support.

View file

@ -4458,6 +4458,100 @@ _curses_init_pair_impl(PyObject *module, int pair_number, int fg, int bg)
Py_RETURN_NONE;
}
#if _NCURSES_EXTENDED_COLOR_FUNCS
/*[clinic input]
_curses.alloc_pair
fg: color_allow_default
Foreground color number.
bg: color_allow_default
Background color number.
/
Allocate a color pair for the given foreground and background colors.
If a color pair for the same colors already exists, return its number.
Otherwise allocate a new color pair and return its number.
[clinic start generated code]*/
static PyObject *
_curses_alloc_pair_impl(PyObject *module, int fg, int bg)
/*[clinic end generated code: output=6eb08cb643d4b5a2 input=b29bafd7b360fa35]*/
{
PyCursesStatefulInitialised(module);
PyCursesStatefulInitialisedColor(module);
int pair = alloc_pair(fg, bg);
if (pair < 0) {
curses_set_error(module, "alloc_pair", NULL);
return NULL;
}
return PyLong_FromLong(pair);
}
/*[clinic input]
_curses.find_pair
fg: color_allow_default
Foreground color number.
bg: color_allow_default
Background color number.
/
Return the number of a color pair for the given colors, or -1.
Return -1 if no color pair for this combination of foreground and
background colors has been allocated.
[clinic start generated code]*/
static PyObject *
_curses_find_pair_impl(PyObject *module, int fg, int bg)
/*[clinic end generated code: output=376026c2a3ac4a9b input=930feac14892c251]*/
{
PyCursesStatefulInitialised(module);
PyCursesStatefulInitialisedColor(module);
return PyLong_FromLong(find_pair(fg, bg));
}
/*[clinic input]
_curses.free_pair
pair: pair
The number of the color pair to free.
/
Free a color pair allocated by alloc_pair().
[clinic start generated code]*/
static PyObject *
_curses_free_pair_impl(PyObject *module, int pair)
/*[clinic end generated code: output=61be0fb2e4bb4e4a input=d24df62feb4161c6]*/
{
PyCursesStatefulInitialised(module);
PyCursesStatefulInitialisedColor(module);
return curses_check_err(module, free_pair(pair), "free_pair", NULL);
}
/*[clinic input]
_curses.reset_color_pairs
Discard all color-pair definitions.
[clinic start generated code]*/
static PyObject *
_curses_reset_color_pairs_impl(PyObject *module)
/*[clinic end generated code: output=117e68c6614e1d06 input=57c1cf7e5447e1ac]*/
{
PyCursesStatefulInitialised(module);
PyCursesStatefulInitialisedColor(module);
reset_color_pairs();
Py_RETURN_NONE;
}
#endif /* _NCURSES_EXTENDED_COLOR_FUNCS */
/* Refresh the private copy of the screen encoding from a freshly created
stdscr window object. Returns 0 on success, -1 with an exception set. */
static int
@ -6241,6 +6335,7 @@ _curses_has_extended_color_support_impl(PyObject *module)
/* List of functions defined in the module */
static PyMethodDef cursesmodule_methods[] = {
_CURSES_ALLOC_PAIR_METHODDEF
_CURSES_BAUDRATE_METHODDEF
_CURSES_BEEP_METHODDEF
_CURSES_CAN_CHANGE_COLOR_METHODDEF
@ -6258,8 +6353,10 @@ static PyMethodDef cursesmodule_methods[] = {
_CURSES_ERASEWCHAR_METHODDEF
_CURSES_FILTER_METHODDEF
_CURSES_NOFILTER_METHODDEF
_CURSES_FIND_PAIR_METHODDEF
_CURSES_FLASH_METHODDEF
_CURSES_FLUSHINP_METHODDEF
_CURSES_FREE_PAIR_METHODDEF
_CURSES_GETMOUSE_METHODDEF
_CURSES_UNGETMOUSE_METHODDEF
_CURSES_GETSYX_METHODDEF
@ -6301,6 +6398,7 @@ static PyMethodDef cursesmodule_methods[] = {
_CURSES_PUTP_METHODDEF
_CURSES_QIFLUSH_METHODDEF
_CURSES_RAW_METHODDEF
_CURSES_RESET_COLOR_PAIRS_METHODDEF
_CURSES_RESET_PROG_MODE_METHODDEF
_CURSES_RESET_SHELL_MODE_METHODDEF
_CURSES_RESETTY_METHODDEF

View file

@ -2723,6 +2723,154 @@ exit:
return return_value;
}
#if (_NCURSES_EXTENDED_COLOR_FUNCS)
PyDoc_STRVAR(_curses_alloc_pair__doc__,
"alloc_pair($module, fg, bg, /)\n"
"--\n"
"\n"
"Allocate a color pair for the given foreground and background colors.\n"
"\n"
" fg\n"
" Foreground color number.\n"
" bg\n"
" Background color number.\n"
"\n"
"If a color pair for the same colors already exists, return its number.\n"
"Otherwise allocate a new color pair and return its number.");
#define _CURSES_ALLOC_PAIR_METHODDEF \
{"alloc_pair", _PyCFunction_CAST(_curses_alloc_pair), METH_FASTCALL, _curses_alloc_pair__doc__},
static PyObject *
_curses_alloc_pair_impl(PyObject *module, int fg, int bg);
static PyObject *
_curses_alloc_pair(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *return_value = NULL;
int fg;
int bg;
if (!_PyArg_CheckPositional("alloc_pair", nargs, 2, 2)) {
goto exit;
}
if (!color_allow_default_converter(args[0], &fg)) {
goto exit;
}
if (!color_allow_default_converter(args[1], &bg)) {
goto exit;
}
return_value = _curses_alloc_pair_impl(module, fg, bg);
exit:
return return_value;
}
#endif /* (_NCURSES_EXTENDED_COLOR_FUNCS) */
#if (_NCURSES_EXTENDED_COLOR_FUNCS)
PyDoc_STRVAR(_curses_find_pair__doc__,
"find_pair($module, fg, bg, /)\n"
"--\n"
"\n"
"Return the number of a color pair for the given colors, or -1.\n"
"\n"
" fg\n"
" Foreground color number.\n"
" bg\n"
" Background color number.\n"
"\n"
"Return -1 if no color pair for this combination of foreground and\n"
"background colors has been allocated.");
#define _CURSES_FIND_PAIR_METHODDEF \
{"find_pair", _PyCFunction_CAST(_curses_find_pair), METH_FASTCALL, _curses_find_pair__doc__},
static PyObject *
_curses_find_pair_impl(PyObject *module, int fg, int bg);
static PyObject *
_curses_find_pair(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *return_value = NULL;
int fg;
int bg;
if (!_PyArg_CheckPositional("find_pair", nargs, 2, 2)) {
goto exit;
}
if (!color_allow_default_converter(args[0], &fg)) {
goto exit;
}
if (!color_allow_default_converter(args[1], &bg)) {
goto exit;
}
return_value = _curses_find_pair_impl(module, fg, bg);
exit:
return return_value;
}
#endif /* (_NCURSES_EXTENDED_COLOR_FUNCS) */
#if (_NCURSES_EXTENDED_COLOR_FUNCS)
PyDoc_STRVAR(_curses_free_pair__doc__,
"free_pair($module, pair, /)\n"
"--\n"
"\n"
"Free a color pair allocated by alloc_pair().\n"
"\n"
" pair\n"
" The number of the color pair to free.");
#define _CURSES_FREE_PAIR_METHODDEF \
{"free_pair", (PyCFunction)_curses_free_pair, METH_O, _curses_free_pair__doc__},
static PyObject *
_curses_free_pair_impl(PyObject *module, int pair);
static PyObject *
_curses_free_pair(PyObject *module, PyObject *arg)
{
PyObject *return_value = NULL;
int pair;
if (!pair_converter(arg, &pair)) {
goto exit;
}
return_value = _curses_free_pair_impl(module, pair);
exit:
return return_value;
}
#endif /* (_NCURSES_EXTENDED_COLOR_FUNCS) */
#if (_NCURSES_EXTENDED_COLOR_FUNCS)
PyDoc_STRVAR(_curses_reset_color_pairs__doc__,
"reset_color_pairs($module, /)\n"
"--\n"
"\n"
"Discard all color-pair definitions.");
#define _CURSES_RESET_COLOR_PAIRS_METHODDEF \
{"reset_color_pairs", (PyCFunction)_curses_reset_color_pairs, METH_NOARGS, _curses_reset_color_pairs__doc__},
static PyObject *
_curses_reset_color_pairs_impl(PyObject *module);
static PyObject *
_curses_reset_color_pairs(PyObject *module, PyObject *Py_UNUSED(ignored))
{
return _curses_reset_color_pairs_impl(module);
}
#endif /* (_NCURSES_EXTENDED_COLOR_FUNCS) */
PyDoc_STRVAR(_curses_initscr__doc__,
"initscr($module, /)\n"
"--\n"
@ -4623,6 +4771,22 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored
#define _CURSES_HAS_KEY_METHODDEF
#endif /* !defined(_CURSES_HAS_KEY_METHODDEF) */
#ifndef _CURSES_ALLOC_PAIR_METHODDEF
#define _CURSES_ALLOC_PAIR_METHODDEF
#endif /* !defined(_CURSES_ALLOC_PAIR_METHODDEF) */
#ifndef _CURSES_FIND_PAIR_METHODDEF
#define _CURSES_FIND_PAIR_METHODDEF
#endif /* !defined(_CURSES_FIND_PAIR_METHODDEF) */
#ifndef _CURSES_FREE_PAIR_METHODDEF
#define _CURSES_FREE_PAIR_METHODDEF
#endif /* !defined(_CURSES_FREE_PAIR_METHODDEF) */
#ifndef _CURSES_RESET_COLOR_PAIRS_METHODDEF
#define _CURSES_RESET_COLOR_PAIRS_METHODDEF
#endif /* !defined(_CURSES_RESET_COLOR_PAIRS_METHODDEF) */
#ifndef _CURSES_NEW_PRESCR_METHODDEF
#define _CURSES_NEW_PRESCR_METHODDEF
#endif /* !defined(_CURSES_NEW_PRESCR_METHODDEF) */
@ -4698,4 +4862,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=8188ebf7404d028a input=a9049054013a1b77]*/
/*[clinic end generated code: output=35a3d93708112587 input=a9049054013a1b77]*/