]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-151774: Add curses dynamic color-pair functions (GH-151775)
authorSerhiy Storchaka <storchaka@gmail.com>
Wed, 24 Jun 2026 18:24:11 +0000 (21:24 +0300)
committerGitHub <noreply@github.com>
Wed, 24 Jun 2026 18:24:11 +0000 (21:24 +0300)
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>
Doc/library/curses.rst
Doc/whatsnew/3.16.rst
Lib/test/test_curses.py
Misc/NEWS.d/next/Library/2026-06-20-11-11-22.gh-issue-151774.O6nrvs.rst [new file with mode: 0644]
Modules/_cursesmodule.c
Modules/clinic/_cursesmodule.c.h

index 758dca976c228dad05708537e18c5ac3c4a63a12..9c8bfb69de1b2995612a894ecd50ea9e86fa7cea 100644 (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
index 7f35c0e3559a9efd9e95f77415ac620a631682cc..7baa9e004afcddcde4e7302fe2500db394aa0326 100644 (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
 ----
 
index fe53ce49ab63b02fcb81376b6d67cb81b47e2dfe..81118deb15cde2604dd31222fadd7cc96444cae2 100644 (file)
@@ -1209,6 +1209,54 @@ class TestCurses(unittest.TestCase):
             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 (file)
index 0000000..668b01e
--- /dev/null
@@ -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.
index fc71efff22d931046346a524a0ff21c84e0a6c6a..973cfa14d61a2df3b960d88d017642418b159ecd 100644 (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
index 46c6ebedcbbd9eec226bd6f19f1e24e8a0fa78e1..b8f67ab389826b57c48d9edb5e117ef75fc21832 100644 (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]*/