From 9081e16838d41cd02aee2c6de77e645b7fcdd210 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 20 Jun 2026 11:12:08 +0300 Subject: [PATCH] gh-151774: Add curses dynamic color-pair functions 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) --- Doc/library/curses.rst | 51 ++++++ Doc/whatsnew/3.16.rst | 7 + Lib/test/test_curses.py | 48 +++++ ...-06-20-11-11-22.gh-issue-151774.O6nrvs.rst | 5 + Modules/_cursesmodule.c | 98 +++++++++++ Modules/clinic/_cursesmodule.c.h | 166 +++++++++++++++++- 6 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-20-11-11-22.gh-issue-151774.O6nrvs.rst diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index d7873054d6b915..d4fc0d12a37bb8 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -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 @@ -215,6 +229,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 @@ -228,6 +255,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 @@ -519,6 +558,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 diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 2b6f396bdf16dd..a822d812bd1aac 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -92,6 +92,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 ---- diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 98f1a7c8a0a2c5..4b48fbcf58158f 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1071,6 +1071,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: diff --git a/Misc/NEWS.d/next/Library/2026-06-20-11-11-22.gh-issue-151774.O6nrvs.rst b/Misc/NEWS.d/next/Library/2026-06-20-11-11-22.gh-issue-151774.O6nrvs.rst new file mode 100644 index 00000000000000..668b01ebfb8ad7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-20-11-11-22.gh-issue-151774.O6nrvs.rst @@ -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. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index e60cba3ef87ead..b22150d25417df 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -3825,6 +3825,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 @@ -5328,6 +5422,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 @@ -5344,8 +5439,10 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_ERASECHAR_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 @@ -5382,6 +5479,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 diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index f577368680ef57..002edd7bcfd09e 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -2701,6 +2701,154 @@ _curses_init_pair(PyObject *module, PyObject *const *args, Py_ssize_t nargs) 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" @@ -4453,6 +4601,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_GET_ESCDELAY_METHODDEF #define _CURSES_GET_ESCDELAY_METHODDEF #endif /* !defined(_CURSES_GET_ESCDELAY_METHODDEF) */ @@ -4516,4 +4680,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=7494804bf2c4d1f5 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=cb008c07b319db6e input=a9049054013a1b77]*/