From 5ebd4860d3746eaeb890c947799508e1ed4936fe Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 21 Aug 2026 09:35:55 +0300 Subject: [PATCH 01/11] gh-156099: Fix a crash when deleting SSLContext.keylog_filename (GH-156103) The setter did not check the value for NULL and passed it to Py_fopen(). --- Lib/test/test_ssl.py | 6 ++++++ .../Library/2026-08-20-12-00-00.gh-issue-156099.Kp4vRt.rst | 3 +++ Modules/_ssl/debughelpers.c | 6 ++++++ 3 files changed, 15 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-20-12-00-00.gh-issue-156099.Kp4vRt.rst diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 2bba665d19343e6..14e4620669491f2 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -5518,6 +5518,12 @@ def test_keylog_defaults(self): with self.assertRaises(TypeError): ctx.keylog_filename = 1 + ctx.keylog_filename = os_helper.TESTFN + with self.assertRaisesRegex(AttributeError, 'cannot be deleted'): + del ctx.keylog_filename + # a failed deletion does not change the value + self.assertEqual(ctx.keylog_filename, os_helper.TESTFN) + def test_keylog_filename(self): self.addCleanup(os_helper.unlink, os_helper.TESTFN) client_context, server_context, hostname = testing_context() diff --git a/Misc/NEWS.d/next/Library/2026-08-20-12-00-00.gh-issue-156099.Kp4vRt.rst b/Misc/NEWS.d/next/Library/2026-08-20-12-00-00.gh-issue-156099.Kp4vRt.rst new file mode 100644 index 000000000000000..1092a5a53785635 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-20-12-00-00.gh-issue-156099.Kp4vRt.rst @@ -0,0 +1,3 @@ +Fix a crash when deleting the ``keylog_filename`` attribute of +:class:`ssl.SSLContext`. +It now raises :exc:`AttributeError`. diff --git a/Modules/_ssl/debughelpers.c b/Modules/_ssl/debughelpers.c index fb9043994f08f9e..b2d552f97e5b0e1 100644 --- a/Modules/_ssl/debughelpers.c +++ b/Modules/_ssl/debughelpers.c @@ -182,6 +182,12 @@ static int _PySSLContext_set_keylog_filename(PyObject *op, PyObject *arg, void *Py_UNUSED(closure)) { + if (arg == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'keylog_filename' of '%.100s' objects " + "cannot be deleted", Py_TYPE(op)->tp_name); + return -1; + } #if defined(MS_WINDOWS_APP) && !defined(MS_WINDOWS_DESKTOP) PyErr_SetString(PyExc_NotImplementedError, "set_keylog_filename: unavailable on UWP build"); From 0f9fb00af8af3f08388d50d09121b9ce460c38a7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 21 Aug 2026 09:39:12 +0300 Subject: [PATCH 02/11] gh-156138: Keep the color pair when a curses write restores the rendition (GH-156139) addstr(), addnstr(), insstr() and insnstr() saved the window rendition with getattrs() and put it back with wattrset(), whose A_COLOR field holds only pairs 0 to 255, so a window using a larger pair lost it. Save and restore the pair with wattr_get() and wattr_set() where they exist. --- Lib/test/test_curses.py | 24 +++++++++++ Modules/_cursesmodule.c | 90 +++++++++++++++++++++++++++++++++-------- 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 6ab951ad2786ea8..6518dadebae83ff 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -910,6 +910,30 @@ def test_output_string_attr_restored(self): func(0, 0, *args, curses.A_BOLD) self.assertEqual(win.getattrs(), curses.A_UNDERLINE) + @requires_colors + @requires_curses_window_meth('color_set') + @requires_curses_window_meth('attr_get') + def test_output_string_pair_restored(self): + # The rendition put back after a write includes the color pair, also + # when it is larger than the A_COLOR field of a chtype holds. + pairs = [7] + if curses.has_extended_color_support() and curses.COLOR_PAIRS > 300: + pairs.append(300) + win = curses.newwin(2, 10, 0, 0) + for pair in pairs: + curses.init_pair(pair, curses.COLOR_RED, curses.COLOR_BLACK) + for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)), + (win.insstr, ('x',)), (win.insnstr, ('x', 1))]: + with self.subTest(func.__qualname__, pair=pair): + win.color_set(pair) + func(0, 0, *args, curses.A_BOLD) + self.assertEqual(win.attr_get()[1], pair) + win.color_set(pair) + # y=100 is outside the window, so the write fails. + self.assertRaises(curses.error, func, 100, 0, *args, + curses.A_BOLD) + self.assertEqual(win.attr_get()[1], pair) + def test_add_string_behavior(self): # addstr() advances the cursor past the written text; addnstr() # writes at most n characters. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 0bab30184a357ad..866ed4bafe6b9cf 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -2161,6 +2161,56 @@ curses_wattrset(PyCursesWindowObject *self, attr_t attr, const char *funcname) return 0; } +/* Read the rendition a write with an *attr* argument has to put back. The + color pair is read apart from the attributes because the A_COLOR field of a + chtype holds only pairs 0 to 255, while a window can use a larger one. */ +static int +curses_wattr_save(PyCursesWindowObject *self, attr_t *attrs, int *pair, + const char *funcname) +{ +#if defined(HAVE_CURSES_WATTR_GET) && defined(HAVE_CURSES_WATTR_SET) + int rtn; +#if _NCURSES_EXTENDED_COLOR_FUNCS + short legacy_pair; + rtn = wattr_get(self->win, attrs, &legacy_pair, pair); +#else + short spair; + rtn = wattr_get(self->win, attrs, &spair, NULL); + *pair = spair; +#endif + if (rtn == ERR) { + curses_window_set_error(self, "wattr_get", funcname); + return -1; + } +#else + *attrs = getattrs(self->win); + *pair = 0; +#endif + return 0; +} + +/* Put the rendition back. The name of the curses function used is + _CURSES_WATTR_RESTORE_FUNC, for the caller to name it in an error. */ +#if defined(HAVE_CURSES_WATTR_GET) && defined(HAVE_CURSES_WATTR_SET) +#define _CURSES_WATTR_RESTORE_FUNC "wattr_set" +#else +#define _CURSES_WATTR_RESTORE_FUNC "wattrset" +#endif + +static int +curses_wattr_restore(PyCursesWindowObject *self, attr_t attrs, int pair) +{ +#if defined(HAVE_CURSES_WATTR_GET) && defined(HAVE_CURSES_WATTR_SET) +#if _NCURSES_EXTENDED_COLOR_FUNCS + return wattr_set(self->win, attrs, 0, &pair); +#else + return wattr_set(self->win, attrs, (short)pair, NULL); +#endif +#else + return wattrset(self->win, attrs); +#endif +} + /*[clinic input] _curses.window.addstr @@ -2201,6 +2251,7 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1, wchar_t *wstr = NULL; #endif attr_t attr_old = A_NORMAL; + int pair_old = 0; int use_xy = group_left_1, use_attr = group_right_1; const char *funcname; @@ -2225,8 +2276,9 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1, return NULL; } if (use_attr) { - attr_old = getattrs(self->win); - if (curses_wattrset(self, attr, "addstr") < 0) { + if (curses_wattr_save(self, &attr_old, &pair_old, "addstr") < 0 || + curses_wattrset(self, attr, "addstr") < 0) + { curses_release_wstr(strtype, wstr); Py_XDECREF(bytesobj); return NULL; @@ -2263,10 +2315,10 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1, Py_DECREF(bytesobj); } if (use_attr) { - int attr_rtn = wattrset(self->win, attr_old); + int attr_rtn = curses_wattr_restore(self, attr_old, pair_old); if (rtn != ERR) { rtn = attr_rtn; - funcname = "wattrset"; + funcname = _CURSES_WATTR_RESTORE_FUNC; } } return curses_window_check_err(self, rtn, funcname, "addstr"); @@ -2315,6 +2367,7 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1, wchar_t *wstr = NULL; #endif attr_t attr_old = A_NORMAL; + int pair_old = 0; int use_xy = group_left_1, use_attr = group_right_1; const char *funcname; @@ -2339,8 +2392,9 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1, return NULL; if (use_attr) { - attr_old = getattrs(self->win); - if (curses_wattrset(self, attr, "addnstr") < 0) { + if (curses_wattr_save(self, &attr_old, &pair_old, "addnstr") < 0 || + curses_wattrset(self, attr, "addnstr") < 0) + { curses_release_wstr(strtype, wstr); Py_XDECREF(bytesobj); return NULL; @@ -2373,10 +2427,10 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1, Py_DECREF(bytesobj); } if (use_attr) { - int attr_rtn = wattrset(self->win, attr_old); + int attr_rtn = curses_wattr_restore(self, attr_old, pair_old); if (rtn != ERR) { rtn = attr_rtn; - funcname = "wattrset"; + funcname = _CURSES_WATTR_RESTORE_FUNC; } } return curses_window_check_err(self, rtn, funcname, "addnstr"); @@ -4035,6 +4089,7 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1, wchar_t *wstr = NULL; #endif attr_t attr_old = A_NORMAL; + int pair_old = 0; int use_xy = group_left_1, use_attr = group_right_1; const char *funcname; @@ -4059,8 +4114,9 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1, return NULL; if (use_attr) { - attr_old = getattrs(self->win); - if (curses_wattrset(self, attr, "insstr") < 0) { + if (curses_wattr_save(self, &attr_old, &pair_old, "insstr") < 0 || + curses_wattrset(self, attr, "insstr") < 0) + { curses_release_wstr(strtype, wstr); Py_XDECREF(bytesobj); return NULL; @@ -4093,10 +4149,10 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1, Py_DECREF(bytesobj); } if (use_attr) { - int attr_rtn = wattrset(self->win, attr_old); + int attr_rtn = curses_wattr_restore(self, attr_old, pair_old); if (rtn != ERR) { rtn = attr_rtn; - funcname = "wattrset"; + funcname = _CURSES_WATTR_RESTORE_FUNC; } } return curses_window_check_err(self, rtn, funcname, "insstr"); @@ -4147,6 +4203,7 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1, wchar_t *wstr = NULL; #endif attr_t attr_old = A_NORMAL; + int pair_old = 0; int use_xy = group_left_1, use_attr = group_right_1; const char *funcname; @@ -4171,8 +4228,9 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1, return NULL; if (use_attr) { - attr_old = getattrs(self->win); - if (curses_wattrset(self, attr, "insnstr") < 0) { + if (curses_wattr_save(self, &attr_old, &pair_old, "insnstr") < 0 || + curses_wattrset(self, attr, "insnstr") < 0) + { curses_release_wstr(strtype, wstr); return NULL; } @@ -4204,10 +4262,10 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1, Py_DECREF(bytesobj); } if (use_attr) { - int attr_rtn = wattrset(self->win, attr_old); + int attr_rtn = curses_wattr_restore(self, attr_old, pair_old); if (rtn != ERR) { rtn = attr_rtn; - funcname = "wattrset"; + funcname = _CURSES_WATTR_RESTORE_FUNC; } } return curses_window_check_err(self, rtn, funcname, "insnstr"); From 8e01c9082971c848ee3a2d3946e03919a46a268d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 21 Aug 2026 09:41:01 +0300 Subject: [PATCH 03/11] gh-155863: Add the WACS_* constants to the curses module (GH-155870) They are the counterparts of the ACS_* line-drawing codes as complexchar cells, added to the module by initscr() and newterm() like the ACS_* ones. Besides the codes that mirror the ACS_* ones, ncurses provides the double-line and thick-line families, which have no ACS_* counterpart. --- Doc/library/curses.rst | 404 ++++++++++-------- Doc/whatsnew/3.16.rst | 5 + Lib/curses/__init__.py | 14 +- Lib/test/test_curses.py | 83 ++++ ...-08-15-17-40-12.gh-issue-155863.Kw3Vqp.rst | 4 + Modules/_cursesmodule.c | 173 ++++++++ 6 files changed, 505 insertions(+), 178 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-15-17-40-12.gh-issue-155863.Kw3Vqp.rst diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index 04cf2e17d538fcc..e5face72d1e5c9b 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -1562,25 +1562,28 @@ Borders and lines that parameter. Keyword parameters can *not* be used. The defaults are listed in this table: - +-----------+---------------------+-----------------------+ - | Parameter | Description | Default value | - +===========+=====================+=======================+ - | *ls* | Left side | :const:`ACS_VLINE` | - +-----------+---------------------+-----------------------+ - | *rs* | Right side | :const:`ACS_VLINE` | - +-----------+---------------------+-----------------------+ - | *ts* | Top | :const:`ACS_HLINE` | - +-----------+---------------------+-----------------------+ - | *bs* | Bottom | :const:`ACS_HLINE` | - +-----------+---------------------+-----------------------+ - | *tl* | Upper-left corner | :const:`ACS_ULCORNER` | - +-----------+---------------------+-----------------------+ - | *tr* | Upper-right corner | :const:`ACS_URCORNER` | - +-----------+---------------------+-----------------------+ - | *bl* | Bottom-left corner | :const:`ACS_LLCORNER` | - +-----------+---------------------+-----------------------+ - | *br* | Bottom-right corner | :const:`ACS_LRCORNER` | - +-----------+---------------------+-----------------------+ + +-----------+---------------------+-----------------------+------------------------+ + | Parameter | Description | Default value | Wide default value | + +===========+=====================+=======================+========================+ + | *ls* | Left side | :const:`ACS_VLINE` | :const:`WACS_VLINE` | + +-----------+---------------------+-----------------------+------------------------+ + | *rs* | Right side | :const:`ACS_VLINE` | :const:`WACS_VLINE` | + +-----------+---------------------+-----------------------+------------------------+ + | *ts* | Top | :const:`ACS_HLINE` | :const:`WACS_HLINE` | + +-----------+---------------------+-----------------------+------------------------+ + | *bs* | Bottom | :const:`ACS_HLINE` | :const:`WACS_HLINE` | + +-----------+---------------------+-----------------------+------------------------+ + | *tl* | Upper-left corner | :const:`ACS_ULCORNER` | :const:`WACS_ULCORNER` | + +-----------+---------------------+-----------------------+------------------------+ + | *tr* | Upper-right corner | :const:`ACS_URCORNER` | :const:`WACS_URCORNER` | + +-----------+---------------------+-----------------------+------------------------+ + | *bl* | Bottom-left corner | :const:`ACS_LLCORNER` | :const:`WACS_LLCORNER` | + +-----------+---------------------+-----------------------+------------------------+ + | *br* | Bottom-right corner | :const:`ACS_LRCORNER` | :const:`WACS_LRCORNER` | + +-----------+---------------------+-----------------------+------------------------+ + + The wide default value is used when the border is drawn from string + characters or :class:`complexchar` cells. .. versionchanged:: next Wide and combining characters, and :class:`complexchar` cells, are now @@ -2255,62 +2258,58 @@ Attributes Some constants are available to specify character cell attributes. The exact constants available are system dependent. -+------------------------+-------------------------------+ -| Attribute | Meaning | -+========================+===============================+ -| .. data:: A_ALTCHARSET | Alternate character set mode | -+------------------------+-------------------------------+ -| .. data:: A_BLINK | Blink mode | -+------------------------+-------------------------------+ -| .. data:: A_BOLD | Bold mode | -+------------------------+-------------------------------+ -| .. data:: A_DIM | Dim mode | -+------------------------+-------------------------------+ -| .. data:: A_INVIS | Invisible or blank mode | -+------------------------+-------------------------------+ -| .. data:: A_ITALIC | Italic mode | -+------------------------+-------------------------------+ -| .. data:: A_NORMAL | Normal attribute | -+------------------------+-------------------------------+ -| .. data:: A_PROTECT | Protected mode | -+------------------------+-------------------------------+ -| .. data:: A_REVERSE | Reverse background and | -| | foreground colors | -+------------------------+-------------------------------+ -| .. data:: A_STANDOUT | Standout mode | -+------------------------+-------------------------------+ -| .. data:: A_UNDERLINE | Underline mode | -+------------------------+-------------------------------+ -| .. data:: A_HORIZONTAL | Horizontal highlight | -+------------------------+-------------------------------+ -| .. data:: A_LEFT | Left highlight | -+------------------------+-------------------------------+ -| .. data:: A_LOW | Low highlight | -+------------------------+-------------------------------+ -| .. data:: A_RIGHT | Right highlight | -+------------------------+-------------------------------+ -| .. data:: A_TOP | Top highlight | -+------------------------+-------------------------------+ -| .. data:: A_VERTICAL | Vertical highlight | -+------------------------+-------------------------------+ +.. _curses-wa-constants: + ++------------------------+-------------------------+------------------------------------------+ +| Attribute | Wide attribute | Meaning | ++========================+=========================+==========================================+ +| .. data:: A_ALTCHARSET | .. data:: WA_ALTCHARSET | Alternate character set mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_BLINK | .. data:: WA_BLINK | Blink mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_BOLD | .. data:: WA_BOLD | Bold mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_DIM | .. data:: WA_DIM | Dim mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_INVIS | .. data:: WA_INVIS | Invisible or blank mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_ITALIC | .. data:: WA_ITALIC | Italic mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_NORMAL | .. data:: WA_NORMAL | Normal attribute | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_PROTECT | .. data:: WA_PROTECT | Protected mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_REVERSE | .. data:: WA_REVERSE | Reverse background and foreground colors | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_STANDOUT | .. data:: WA_STANDOUT | Standout mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_UNDERLINE | .. data:: WA_UNDERLINE | Underline mode | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_HORIZONTAL | .. data:: WA_HORIZONTAL | Horizontal highlight | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_LEFT | .. data:: WA_LEFT | Left highlight | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_LOW | .. data:: WA_LOW | Low highlight | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_RIGHT | .. data:: WA_RIGHT | Right highlight | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_TOP | .. data:: WA_TOP | Top highlight | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: A_VERTICAL | .. data:: WA_VERTICAL | Vertical highlight | ++------------------------+-------------------------+------------------------------------------+ .. versionadded:: 3.7 ``A_ITALIC`` was added. -.. _curses-wa-constants: - The :meth:`~window.attr_get`, :meth:`~window.attr_set`, :meth:`~window.attr_on` -and :meth:`~window.attr_off` methods use a parallel set of ``WA_*`` constants. -These have the same meaning as the corresponding ``A_*`` attributes above -(``WA_BOLD`` like :const:`A_BOLD`, and so on), but belong to the ``attr_t`` type -rather than being packed into a character. In ncurses the two sets share the -same values, but other curses implementations may give them different ones, so -use the ``WA_*`` constants with the ``attr_*`` methods. The available names are -``WA_ATTRIBUTES``, ``WA_NORMAL``, ``WA_STANDOUT``, ``WA_UNDERLINE``, -``WA_REVERSE``, ``WA_BLINK``, ``WA_DIM``, ``WA_BOLD``, ``WA_ALTCHARSET``, -``WA_INVIS``, ``WA_PROTECT``, ``WA_HORIZONTAL``, ``WA_LEFT``, ``WA_LOW``, -``WA_RIGHT``, ``WA_TOP``, ``WA_VERTICAL`` and ``WA_ITALIC`` (each available only -where the platform defines it). +and :meth:`~window.attr_off` methods use the parallel set of ``WA_*`` constants +listed above. +Each has the same meaning as the corresponding ``A_*`` attribute +(:const:`WA_BOLD` like :const:`A_BOLD`, and so on), but belongs to the +``attr_t`` type rather than being packed into a character. +In ncurses the two sets share the same values, but other curses implementations +may give them different ones, so use the ``WA_*`` constants with the ``attr_*`` +methods. .. versionadded:: next The ``WA_*`` constants were added. @@ -2318,18 +2317,15 @@ where the platform defines it). Several constants are available to extract corresponding attributes returned by some methods. -+-------------------------+-------------------------------+ -| Bit-mask | Meaning | -+=========================+===============================+ -| .. data:: A_ATTRIBUTES | Bit-mask to extract | -| | attributes | -+-------------------------+-------------------------------+ -| .. data:: A_CHARTEXT | Bit-mask to extract a | -| | character | -+-------------------------+-------------------------------+ -| .. data:: A_COLOR | Bit-mask to extract | -| | color-pair field information | -+-------------------------+-------------------------------+ ++-------------------------+--------------------------+--------------------------------------------------+ +| Bit-mask | Wide bit-mask | Meaning | ++=========================+==========================+==================================================+ +| .. data:: A_ATTRIBUTES | .. data:: WA_ATTRIBUTES | Bit-mask to extract attributes | ++-------------------------+--------------------------+--------------------------------------------------+ +| .. data:: A_CHARTEXT | | Bit-mask to extract a character | ++-------------------------+--------------------------+--------------------------------------------------+ +| .. data:: A_COLOR | | Bit-mask to extract color-pair field information | ++-------------------------+--------------------------+--------------------------------------------------+ Keys ~~~~ @@ -2564,99 +2560,165 @@ inherited from the VT100 terminal, and will generally be available on software emulations such as X terminals. When there is no graphic available, curses falls back on a crude printable ASCII approximation. +Every character has two names. +The ``ACS_*`` code is an integer character, restricted to the 8-bit +alternate character set of the terminal. +The ``WACS_*`` code is the same character as a :class:`complexchar` cell, +which is not restricted to the alternate character set. + .. note:: These are available only after :func:`initscr` has been called. + The ``WACS_*`` codes are only available if Python is built with + wide character support. -+------------------------+------------------------------------------+ -| ACS code | Meaning | -+========================+==========================================+ -| .. data:: ACS_BBSS | alternate name for upper-right corner | -+------------------------+------------------------------------------+ -| .. data:: ACS_BLOCK | solid square block | -+------------------------+------------------------------------------+ -| .. data:: ACS_BOARD | board of squares | -+------------------------+------------------------------------------+ -| .. data:: ACS_BSBS | alternate name for horizontal line | -+------------------------+------------------------------------------+ -| .. data:: ACS_BSSB | alternate name for upper-left corner | -+------------------------+------------------------------------------+ -| .. data:: ACS_BSSS | alternate name for top tee | -+------------------------+------------------------------------------+ -| .. data:: ACS_BTEE | bottom tee | -+------------------------+------------------------------------------+ -| .. data:: ACS_BULLET | bullet | -+------------------------+------------------------------------------+ -| .. data:: ACS_CKBOARD | checker board (stipple) | -+------------------------+------------------------------------------+ -| .. data:: ACS_DARROW | arrow pointing down | -+------------------------+------------------------------------------+ -| .. data:: ACS_DEGREE | degree symbol | -+------------------------+------------------------------------------+ -| .. data:: ACS_DIAMOND | diamond | -+------------------------+------------------------------------------+ -| .. data:: ACS_GEQUAL | greater-than-or-equal-to | -+------------------------+------------------------------------------+ -| .. data:: ACS_HLINE | horizontal line | -+------------------------+------------------------------------------+ -| .. data:: ACS_LANTERN | lantern symbol | -+------------------------+------------------------------------------+ -| .. data:: ACS_LARROW | left arrow | -+------------------------+------------------------------------------+ -| .. data:: ACS_LEQUAL | less-than-or-equal-to | -+------------------------+------------------------------------------+ -| .. data:: ACS_LLCORNER | lower-left corner | -+------------------------+------------------------------------------+ -| .. data:: ACS_LRCORNER | lower-right corner | -+------------------------+------------------------------------------+ -| .. data:: ACS_LTEE | left tee | -+------------------------+------------------------------------------+ -| .. data:: ACS_NEQUAL | not-equal sign | -+------------------------+------------------------------------------+ -| .. data:: ACS_PI | letter pi | -+------------------------+------------------------------------------+ -| .. data:: ACS_PLMINUS | plus-or-minus sign | -+------------------------+------------------------------------------+ -| .. data:: ACS_PLUS | big plus sign | -+------------------------+------------------------------------------+ -| .. data:: ACS_RARROW | right arrow | -+------------------------+------------------------------------------+ -| .. data:: ACS_RTEE | right tee | -+------------------------+------------------------------------------+ -| .. data:: ACS_S1 | scan line 1 | -+------------------------+------------------------------------------+ -| .. data:: ACS_S3 | scan line 3 | -+------------------------+------------------------------------------+ -| .. data:: ACS_S7 | scan line 7 | -+------------------------+------------------------------------------+ -| .. data:: ACS_S9 | scan line 9 | -+------------------------+------------------------------------------+ -| .. data:: ACS_SBBS | alternate name for lower-right corner | -+------------------------+------------------------------------------+ -| .. data:: ACS_SBSB | alternate name for vertical line | -+------------------------+------------------------------------------+ -| .. data:: ACS_SBSS | alternate name for right tee | -+------------------------+------------------------------------------+ -| .. data:: ACS_SSBB | alternate name for lower-left corner | -+------------------------+------------------------------------------+ -| .. data:: ACS_SSBS | alternate name for bottom tee | -+------------------------+------------------------------------------+ -| .. data:: ACS_SSSB | alternate name for left tee | -+------------------------+------------------------------------------+ -| .. data:: ACS_SSSS | alternate name for crossover or big plus | -+------------------------+------------------------------------------+ -| .. data:: ACS_STERLING | pound sterling | -+------------------------+------------------------------------------+ -| .. data:: ACS_TTEE | top tee | -+------------------------+------------------------------------------+ -| .. data:: ACS_UARROW | up arrow | -+------------------------+------------------------------------------+ -| .. data:: ACS_ULCORNER | upper-left corner | -+------------------------+------------------------------------------+ -| .. data:: ACS_URCORNER | upper-right corner | -+------------------------+------------------------------------------+ -| .. data:: ACS_VLINE | vertical line | -+------------------------+------------------------------------------+ +.. versionadded:: next + The ``WACS_*`` codes. + ++------------------------+-------------------------+------------------------------------------+ +| ACS code | WACS code | Meaning | ++========================+=========================+==========================================+ +| .. data:: ACS_BBSS | .. data:: WACS_BBSS | alternate name for upper-right corner | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_BLOCK | .. data:: WACS_BLOCK | solid square block | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_BOARD | .. data:: WACS_BOARD | board of squares | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_BSBS | .. data:: WACS_BSBS | alternate name for horizontal line | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_BSSB | .. data:: WACS_BSSB | alternate name for upper-left corner | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_BSSS | .. data:: WACS_BSSS | alternate name for top tee | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_BTEE | .. data:: WACS_BTEE | bottom tee | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_BULLET | .. data:: WACS_BULLET | bullet | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_CKBOARD | .. data:: WACS_CKBOARD | checker board (stipple) | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_DARROW | .. data:: WACS_DARROW | arrow pointing down | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_DEGREE | .. data:: WACS_DEGREE | degree symbol | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_DIAMOND | .. data:: WACS_DIAMOND | diamond | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_GEQUAL | .. data:: WACS_GEQUAL | greater-than-or-equal-to | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_HLINE | .. data:: WACS_HLINE | horizontal line | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_LANTERN | .. data:: WACS_LANTERN | lantern symbol | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_LARROW | .. data:: WACS_LARROW | left arrow | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_LEQUAL | .. data:: WACS_LEQUAL | less-than-or-equal-to | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_LLCORNER | .. data:: WACS_LLCORNER | lower-left corner | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_LRCORNER | .. data:: WACS_LRCORNER | lower-right corner | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_LTEE | .. data:: WACS_LTEE | left tee | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_NEQUAL | .. data:: WACS_NEQUAL | not-equal sign | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_PI | .. data:: WACS_PI | letter pi | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_PLMINUS | .. data:: WACS_PLMINUS | plus-or-minus sign | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_PLUS | .. data:: WACS_PLUS | big plus sign | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_RARROW | .. data:: WACS_RARROW | right arrow | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_RTEE | .. data:: WACS_RTEE | right tee | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_S1 | .. data:: WACS_S1 | scan line 1 | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_S3 | .. data:: WACS_S3 | scan line 3 | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_S7 | .. data:: WACS_S7 | scan line 7 | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_S9 | .. data:: WACS_S9 | scan line 9 | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_SBBS | .. data:: WACS_SBBS | alternate name for lower-right corner | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_SBSB | .. data:: WACS_SBSB | alternate name for vertical line | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_SBSS | .. data:: WACS_SBSS | alternate name for right tee | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_SSBB | .. data:: WACS_SSBB | alternate name for lower-left corner | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_SSBS | .. data:: WACS_SSBS | alternate name for bottom tee | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_SSSB | .. data:: WACS_SSSB | alternate name for left tee | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_SSSS | .. data:: WACS_SSSS | alternate name for crossover or big plus | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_STERLING | .. data:: WACS_STERLING | pound sterling | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_TTEE | .. data:: WACS_TTEE | top tee | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_UARROW | .. data:: WACS_UARROW | up arrow | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_ULCORNER | .. data:: WACS_ULCORNER | upper-left corner | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_URCORNER | .. data:: WACS_URCORNER | upper-right corner | ++------------------------+-------------------------+------------------------------------------+ +| .. data:: ACS_VLINE | .. data:: WACS_VLINE | vertical line | ++------------------------+-------------------------+------------------------------------------+ + +The following table lists the double-line and thick-line characters. +They have no ``ACS_*`` counterpart, and are not provided by every implementation. +As in the table above, the alternate name spells out the four sides of the +character, clockwise from the top: +``B`` for a blank side, ``S`` for a single line, ``D`` for a double line and +``T`` for a thick line. + ++---------------------------+---------------------+--------------------------------+ +| WACS code | Alternate name | Meaning | ++===========================+=====================+================================+ +| .. data:: WACS_D_BTEE | .. data:: WACS_DDBD | double-line bottom tee | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_HLINE | .. data:: WACS_BDBD | double-line horizontal line | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_LLCORNER | .. data:: WACS_DDBB | double-line lower-left corner | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_LRCORNER | .. data:: WACS_DBBD | double-line lower-right corner | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_LTEE | .. data:: WACS_DDDB | double-line left tee | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_PLUS | .. data:: WACS_DDDD | double-line big plus sign | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_RTEE | .. data:: WACS_DBDD | double-line right tee | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_TTEE | .. data:: WACS_BDDD | double-line top tee | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_ULCORNER | .. data:: WACS_BDDB | double-line upper-left corner | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_URCORNER | .. data:: WACS_BBDD | double-line upper-right corner | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_D_VLINE | .. data:: WACS_DBDB | double-line vertical line | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_BTEE | .. data:: WACS_TTBT | thick-line bottom tee | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_HLINE | .. data:: WACS_BTBT | thick-line horizontal line | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_LLCORNER | .. data:: WACS_TTBB | thick-line lower-left corner | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_LRCORNER | .. data:: WACS_TBBT | thick-line lower-right corner | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_LTEE | .. data:: WACS_TTTB | thick-line left tee | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_PLUS | .. data:: WACS_TTTT | thick-line big plus sign | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_RTEE | .. data:: WACS_TBTT | thick-line right tee | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_TTEE | .. data:: WACS_BTTT | thick-line top tee | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_ULCORNER | .. data:: WACS_BTTB | thick-line upper-left corner | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_URCORNER | .. data:: WACS_BBTT | thick-line upper-right corner | ++---------------------------+---------------------+--------------------------------+ +| .. data:: WACS_T_VLINE | .. data:: WACS_TBTB | thick-line vertical line | ++---------------------------+---------------------+--------------------------------+ Mouse buttons ~~~~~~~~~~~~~ diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 063755e1eadcb53..a1a8415482b97aa 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -216,6 +216,11 @@ curses counterpart of :func:`curses.termattrs`. (Contributed by Serhiy Storchaka in :gh:`152332`.) +* Add the ``WACS_*`` constants to the :mod:`curses` module, the counterparts of + the :ref:`ACS_* ` line-drawing codes as + :class:`curses.complexchar` cells. + (Contributed by Serhiy Storchaka in :gh:`155863`.) + * 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, diff --git a/Lib/curses/__init__.py b/Lib/curses/__init__.py index e150c7f932385eb..1e372ecdc831396 100644 --- a/Lib/curses/__init__.py +++ b/Lib/curses/__init__.py @@ -14,8 +14,8 @@ import os as _os import sys as _sys -# Some constants, most notably the ACS_* ones, are only added to the C -# _curses module's dictionary after initscr() is called. (Some +# Some constants, most notably the ACS_* and WACS_* ones, are only added +# to the C _curses module's dictionary after initscr() is called. (Some # versions of SGI's curses don't define values for those constants # until initscr() has been called.) This wrapper function calls the # underlying C initscr(), and then copies the constants from the @@ -30,13 +30,13 @@ def initscr(): fd=_sys.__stdout__.fileno()) stdscr = _curses.initscr() for key, value in _curses.__dict__.items(): - if key.startswith('ACS_') or key in ('LINES', 'COLS'): + if key.startswith(('ACS_', 'WACS_')) or key in ('LINES', 'COLS'): setattr(curses, key, value) return stdscr -# newterm() is wrapped for the same reason as initscr(): the ACS_* constants -# and LINES/COLS only become available once a terminal is initialized, and are -# then copied to the curses package's dictionary. +# newterm() is wrapped for the same reason as initscr(): the ACS_* and WACS_* +# constants and LINES/COLS only become available once a terminal is +# initialized, and are then copied to the curses package's dictionary. try: newterm @@ -47,7 +47,7 @@ def newterm(type=None, fd=None, infd=None, /): import _curses, curses screen = _curses.newterm(type, fd, infd) for key, value in _curses.__dict__.items(): - if key.startswith('ACS_') or key in ('LINES', 'COLS'): + if key.startswith(('ACS_', 'WACS_')) or key in ('LINES', 'COLS'): setattr(curses, key, value) return screen diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 6518dadebae83ff..6a7a8e320438f85 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -57,6 +57,23 @@ def wrapped(self, *args, **kwargs): return wrapped +# The WACS_* double-line and thick-line character cells, without the common +# prefix, paired with the alternate name spelling out their four sides +# (blank, double or thick, clockwise from the top). +WACS_LINE_ALIASES = [ + ('D_ULCORNER', 'BDDB'), ('D_LLCORNER', 'DDBB'), + ('D_URCORNER', 'BBDD'), ('D_LRCORNER', 'DBBD'), + ('D_LTEE', 'DDDB'), ('D_RTEE', 'DBDD'), + ('D_BTEE', 'DDBD'), ('D_TTEE', 'BDDD'), + ('D_HLINE', 'BDBD'), ('D_VLINE', 'DBDB'), ('D_PLUS', 'DDDD'), + ('T_ULCORNER', 'BTTB'), ('T_LLCORNER', 'TTBB'), + ('T_URCORNER', 'BBTT'), ('T_LRCORNER', 'TBBT'), + ('T_LTEE', 'TTTB'), ('T_RTEE', 'TBTT'), + ('T_BTEE', 'TTBT'), ('T_TTEE', 'BTTT'), + ('T_HLINE', 'BTBT'), ('T_VLINE', 'TBTB'), ('T_PLUS', 'TTTT'), +] + + def requires_colors(test): @functools.wraps(test) def wrapped(self, *args, **kwargs): @@ -470,6 +487,72 @@ def test_wide_characters(self): # border() and box() cannot mix integer and wide-string characters. self.assertRaises(TypeError, stdscr.box, vline, ord('-')) + @requires_wide_build + def test_wacs_constants(self): + # Every ACS_* code has a WACS_* character cell counterpart, plus the + # double-line and thick-line codes, which have no ACS_* counterpart. + acs = {name.removeprefix('ACS_') + for name in dir(curses) if name.startswith('ACS_')} + wacs = {name.removeprefix('WACS_') + for name in dir(curses) if name.startswith('WACS_')} + extra = {name for pair in WACS_LINE_ALIASES for name in pair} + self.assertEqual(wacs - extra, acs) + for name in sorted(wacs): + with self.subTest(name=name): + self.assertIsInstance(getattr(curses, 'WACS_' + name), + curses.complexchar) + # The alternate names refer to the same cells. + self.assertEqual(curses.WACS_BSSB, curses.WACS_ULCORNER) + self.assertEqual(curses.WACS_BSBS, curses.WACS_HLINE) + self.assertEqual(curses.WACS_SBSB, curses.WACS_VLINE) + self.assertEqual(curses.WACS_SSSS, curses.WACS_PLUS) + + @requires_wide_build + def test_wacs_line_constants(self): + # The double-line and thick-line codes are optional, but a supporting + # implementation provides the whole family under both names. + present = [name for name, alias in WACS_LINE_ALIASES + if hasattr(curses, 'WACS_' + name)] + if not present: + self.skipTest('requires double-line and thick-line characters') + self.assertEqual(len(present), len(WACS_LINE_ALIASES)) + for name, alias in WACS_LINE_ALIASES: + with self.subTest(name=name): + cell = getattr(curses, 'WACS_' + name) + self.assertIsInstance(cell, curses.complexchar) + self.assertEqual(getattr(curses, 'WACS_' + alias), cell) + # They are distinct from the single-line characters. + self.assertNotEqual(curses.WACS_D_HLINE, curses.WACS_HLINE) + self.assertNotEqual(curses.WACS_T_HLINE, curses.WACS_HLINE) + self.assertNotEqual(curses.WACS_D_HLINE, curses.WACS_T_HLINE) + stdscr = self.stdscr + stdscr.border(curses.WACS_D_VLINE, curses.WACS_D_VLINE, + curses.WACS_D_HLINE, curses.WACS_D_HLINE, + curses.WACS_D_ULCORNER, curses.WACS_D_URCORNER, + curses.WACS_D_LLCORNER, curses.WACS_D_LRCORNER) + self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_D_ULCORNER) + self.assertEqual(stdscr.in_wch(0, 1), curses.WACS_D_HLINE) + + @requires_wide_build + def test_wacs_in_cell_methods(self): + # A WACS_* cell can be used wherever a character cell is accepted. + stdscr = self.stdscr + stdscr.addch(0, 0, curses.WACS_ULCORNER) + self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_ULCORNER) + stdscr.insch(1, 0, curses.WACS_DIAMOND) + self.assertEqual(stdscr.in_wch(1, 0), curses.WACS_DIAMOND) + stdscr.hline(2, 0, curses.WACS_HLINE, 5) + self.assertEqual(stdscr.in_wch(2, 4), curses.WACS_HLINE) + stdscr.vline(3, 0, curses.WACS_VLINE, 3) + self.assertEqual(stdscr.in_wch(5, 0), curses.WACS_VLINE) + stdscr.border(curses.WACS_VLINE, curses.WACS_VLINE, + curses.WACS_HLINE, curses.WACS_HLINE, + curses.WACS_ULCORNER, curses.WACS_URCORNER, + curses.WACS_LLCORNER, curses.WACS_LRCORNER) + self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_ULCORNER) + stdscr.box(curses.WACS_VLINE, curses.WACS_HLINE) + self.assertEqual(stdscr.in_wch(0, 1), curses.WACS_HLINE) + def test_complexchar_in_cell_methods(self): # Every single-character-cell method also accepts a complexchar, whose # attributes and color pair come from the cell itself. diff --git a/Misc/NEWS.d/next/Library/2026-08-15-17-40-12.gh-issue-155863.Kw3Vqp.rst b/Misc/NEWS.d/next/Library/2026-08-15-17-40-12.gh-issue-155863.Kw3Vqp.rst new file mode 100644 index 000000000000000..1266fbba5cc2325 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-15-17-40-12.gh-issue-155863.Kw3Vqp.rst @@ -0,0 +1,4 @@ +Add the ``WACS_*`` constants to the :mod:`curses` module. They are the +counterparts of the ``ACS_*`` line-drawing codes as :class:`curses.complexchar` +cells, and are added, like the latter, by :func:`curses.initscr` and +:func:`curses.newterm`. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 866ed4bafe6b9cf..ea399e25213aef2 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -6612,6 +6612,179 @@ curses_init_dict(PyObject *module) SetDictInt("ACS_STERLING", (ACS_STERLING)); #endif +#ifdef HAVE_NCURSESW + /* The same graphic symbols as character cells, for the methods that take + a complexchar. Unlike the ACS_* codes, these are not restricted to the + 8-bit alternate character set. */ + cursesmodule_state *state = get_cursesmodule_state(module); +#define SetDictWACS(NAME, VALUE) \ + do { \ + PyObject *value = PyCursesComplexChar_New(state, (VALUE)); \ + if (value == NULL) { \ + return -1; \ + } \ + int rc = PyDict_SetItemString(module_dict, (NAME), value); \ + Py_DECREF(value); \ + if (rc < 0) { \ + return -1; \ + } \ + } while (0) + + SetDictWACS("WACS_ULCORNER", WACS_ULCORNER); + SetDictWACS("WACS_LLCORNER", WACS_LLCORNER); + SetDictWACS("WACS_URCORNER", WACS_URCORNER); + SetDictWACS("WACS_LRCORNER", WACS_LRCORNER); + SetDictWACS("WACS_LTEE", WACS_LTEE); + SetDictWACS("WACS_RTEE", WACS_RTEE); + SetDictWACS("WACS_BTEE", WACS_BTEE); + SetDictWACS("WACS_TTEE", WACS_TTEE); + SetDictWACS("WACS_HLINE", WACS_HLINE); + SetDictWACS("WACS_VLINE", WACS_VLINE); + SetDictWACS("WACS_PLUS", WACS_PLUS); + SetDictWACS("WACS_S1", WACS_S1); + SetDictWACS("WACS_S9", WACS_S9); + SetDictWACS("WACS_DIAMOND", WACS_DIAMOND); + SetDictWACS("WACS_CKBOARD", WACS_CKBOARD); + SetDictWACS("WACS_DEGREE", WACS_DEGREE); + SetDictWACS("WACS_PLMINUS", WACS_PLMINUS); + SetDictWACS("WACS_BULLET", WACS_BULLET); + SetDictWACS("WACS_LARROW", WACS_LARROW); + SetDictWACS("WACS_RARROW", WACS_RARROW); + SetDictWACS("WACS_DARROW", WACS_DARROW); + SetDictWACS("WACS_UARROW", WACS_UARROW); + SetDictWACS("WACS_BOARD", WACS_BOARD); + SetDictWACS("WACS_LANTERN", WACS_LANTERN); + SetDictWACS("WACS_BLOCK", WACS_BLOCK); + + SetDictWACS("WACS_BSSB", WACS_ULCORNER); + SetDictWACS("WACS_SSBB", WACS_LLCORNER); + SetDictWACS("WACS_BBSS", WACS_URCORNER); + SetDictWACS("WACS_SBBS", WACS_LRCORNER); + SetDictWACS("WACS_SBSS", WACS_RTEE); + SetDictWACS("WACS_SSSB", WACS_LTEE); + SetDictWACS("WACS_SSBS", WACS_BTEE); + SetDictWACS("WACS_BSSS", WACS_TTEE); + SetDictWACS("WACS_BSBS", WACS_HLINE); + SetDictWACS("WACS_SBSB", WACS_VLINE); + SetDictWACS("WACS_SSSS", WACS_PLUS); + + /* The following are never available with strict SYSV curses */ +#ifdef WACS_S3 + SetDictWACS("WACS_S3", WACS_S3); +#endif +#ifdef WACS_S7 + SetDictWACS("WACS_S7", WACS_S7); +#endif +#ifdef WACS_LEQUAL + SetDictWACS("WACS_LEQUAL", WACS_LEQUAL); +#endif +#ifdef WACS_GEQUAL + SetDictWACS("WACS_GEQUAL", WACS_GEQUAL); +#endif +#ifdef WACS_PI + SetDictWACS("WACS_PI", WACS_PI); +#endif +#ifdef WACS_NEQUAL + SetDictWACS("WACS_NEQUAL", WACS_NEQUAL); +#endif +#ifdef WACS_STERLING + SetDictWACS("WACS_STERLING", WACS_STERLING); +#endif + + /* Double-line and thick-line symbols have no ACS_* counterpart, and are + only provided by some implementations. */ +#ifdef WACS_D_ULCORNER + SetDictWACS("WACS_D_ULCORNER", WACS_D_ULCORNER); + SetDictWACS("WACS_BDDB", WACS_D_ULCORNER); +#endif +#ifdef WACS_D_LLCORNER + SetDictWACS("WACS_D_LLCORNER", WACS_D_LLCORNER); + SetDictWACS("WACS_DDBB", WACS_D_LLCORNER); +#endif +#ifdef WACS_D_URCORNER + SetDictWACS("WACS_D_URCORNER", WACS_D_URCORNER); + SetDictWACS("WACS_BBDD", WACS_D_URCORNER); +#endif +#ifdef WACS_D_LRCORNER + SetDictWACS("WACS_D_LRCORNER", WACS_D_LRCORNER); + SetDictWACS("WACS_DBBD", WACS_D_LRCORNER); +#endif +#ifdef WACS_D_LTEE + SetDictWACS("WACS_D_LTEE", WACS_D_LTEE); + SetDictWACS("WACS_DDDB", WACS_D_LTEE); +#endif +#ifdef WACS_D_RTEE + SetDictWACS("WACS_D_RTEE", WACS_D_RTEE); + SetDictWACS("WACS_DBDD", WACS_D_RTEE); +#endif +#ifdef WACS_D_BTEE + SetDictWACS("WACS_D_BTEE", WACS_D_BTEE); + SetDictWACS("WACS_DDBD", WACS_D_BTEE); +#endif +#ifdef WACS_D_TTEE + SetDictWACS("WACS_D_TTEE", WACS_D_TTEE); + SetDictWACS("WACS_BDDD", WACS_D_TTEE); +#endif +#ifdef WACS_D_HLINE + SetDictWACS("WACS_D_HLINE", WACS_D_HLINE); + SetDictWACS("WACS_BDBD", WACS_D_HLINE); +#endif +#ifdef WACS_D_VLINE + SetDictWACS("WACS_D_VLINE", WACS_D_VLINE); + SetDictWACS("WACS_DBDB", WACS_D_VLINE); +#endif +#ifdef WACS_D_PLUS + SetDictWACS("WACS_D_PLUS", WACS_D_PLUS); + SetDictWACS("WACS_DDDD", WACS_D_PLUS); +#endif + +#ifdef WACS_T_ULCORNER + SetDictWACS("WACS_T_ULCORNER", WACS_T_ULCORNER); + SetDictWACS("WACS_BTTB", WACS_T_ULCORNER); +#endif +#ifdef WACS_T_LLCORNER + SetDictWACS("WACS_T_LLCORNER", WACS_T_LLCORNER); + SetDictWACS("WACS_TTBB", WACS_T_LLCORNER); +#endif +#ifdef WACS_T_URCORNER + SetDictWACS("WACS_T_URCORNER", WACS_T_URCORNER); + SetDictWACS("WACS_BBTT", WACS_T_URCORNER); +#endif +#ifdef WACS_T_LRCORNER + SetDictWACS("WACS_T_LRCORNER", WACS_T_LRCORNER); + SetDictWACS("WACS_TBBT", WACS_T_LRCORNER); +#endif +#ifdef WACS_T_LTEE + SetDictWACS("WACS_T_LTEE", WACS_T_LTEE); + SetDictWACS("WACS_TTTB", WACS_T_LTEE); +#endif +#ifdef WACS_T_RTEE + SetDictWACS("WACS_T_RTEE", WACS_T_RTEE); + SetDictWACS("WACS_TBTT", WACS_T_RTEE); +#endif +#ifdef WACS_T_BTEE + SetDictWACS("WACS_T_BTEE", WACS_T_BTEE); + SetDictWACS("WACS_TTBT", WACS_T_BTEE); +#endif +#ifdef WACS_T_TTEE + SetDictWACS("WACS_T_TTEE", WACS_T_TTEE); + SetDictWACS("WACS_BTTT", WACS_T_TTEE); +#endif +#ifdef WACS_T_HLINE + SetDictWACS("WACS_T_HLINE", WACS_T_HLINE); + SetDictWACS("WACS_BTBT", WACS_T_HLINE); +#endif +#ifdef WACS_T_VLINE + SetDictWACS("WACS_T_VLINE", WACS_T_VLINE); + SetDictWACS("WACS_TBTB", WACS_T_VLINE); +#endif +#ifdef WACS_T_PLUS + SetDictWACS("WACS_T_PLUS", WACS_T_PLUS); + SetDictWACS("WACS_TTTT", WACS_T_PLUS); +#endif +#undef SetDictWACS +#endif /* HAVE_NCURSESW */ + SetDictInt("LINES", LINES); SetDictInt("COLS", COLS); #undef SetDictInt From daebcac92d422d7fe9611cbe89044af23ea857c3 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 21 Aug 2026 10:04:25 +0300 Subject: [PATCH 04/11] gh-156100: Fix crashes in the sqlite3 Connection.autocommit setter (GH-156104) Deleting the attribute crashed, and setting it to an integer which does not fit in C long reported success with OverflowError set. --- Lib/test/test_sqlite3/test_transactions.py | 17 +++++++++++++++- ...-08-20-12-05-00.gh-issue-156100.Lm7qWz.rst | 4 ++++ Modules/_sqlite/connection.c | 20 ++++++++++++++----- 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-20-12-05-00.gh-issue-156100.Lm7qWz.rst diff --git a/Lib/test/test_sqlite3/test_transactions.py b/Lib/test/test_sqlite3/test_transactions.py index a3de7a7a82ec1cb..2e5d60fe9ba5fc0 100644 --- a/Lib/test/test_sqlite3/test_transactions.py +++ b/Lib/test/test_sqlite3/test_transactions.py @@ -389,10 +389,25 @@ def test_autocommit_setget(self): def test_autocommit_setget_invalid(self): msg = "autocommit must be True, False, or.*LEGACY" - for mode in "a", 12, (), None: + for mode in "a", 12, (), None, 2**1000, -2**1000: with self.subTest(mode=mode): with self.assertRaisesRegex(ValueError, msg): sqlite.connect(":memory:", autocommit=mode) + with memory_database() as cx: + with self.assertRaisesRegex(ValueError, msg): + cx.autocommit = mode + # a failed assignment does not change the value + self.assertEqual(cx.autocommit, + sqlite.LEGACY_TRANSACTION_CONTROL) + + def test_autocommit_delete(self): + with memory_database() as cx: + cx.autocommit = False + with self.assertRaisesRegex(AttributeError, + "cannot delete autocommit attribute"): + del cx.autocommit + # a failed deletion does not change the value + self.assertIs(cx.autocommit, False) def test_autocommit_disabled(self): expected = [ diff --git a/Misc/NEWS.d/next/Library/2026-08-20-12-05-00.gh-issue-156100.Lm7qWz.rst b/Misc/NEWS.d/next/Library/2026-08-20-12-05-00.gh-issue-156100.Lm7qWz.rst new file mode 100644 index 000000000000000..8c296a9a8919fc3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-20-12-05-00.gh-issue-156100.Lm7qWz.rst @@ -0,0 +1,4 @@ +Fix crashes in :class:`sqlite3.Connection` when deleting the +:attr:`~sqlite3.Connection.autocommit` attribute or setting it to an integer +which does not fit in C :c:expr:`long`. +Both now raise an exception. diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 892740b05e55c98..ec47471873f7822 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -104,11 +104,16 @@ autocommit_converter(PyObject *val, enum autocommit_mode *result) *result = AUTOCOMMIT_DISABLED; return 1; } - if (PyLong_Check(val) && - PyLong_AsLong(val) == LEGACY_TRANSACTION_CONTROL) - { - *result = AUTOCOMMIT_LEGACY; - return 1; + if (PyLong_Check(val)) { + int overflow; + long value = PyLong_AsLongAndOverflow(val, &overflow); + if (value == -1 && PyErr_Occurred()) { + return 0; + } + if (!overflow && value == LEGACY_TRANSACTION_CONTROL) { + *result = AUTOCOMMIT_LEGACY; + return 1; + } } PyErr_SetString(PyExc_ValueError, @@ -2621,6 +2626,11 @@ static int set_autocommit(PyObject *op, PyObject *val, void *Py_UNUSED(closure)) { pysqlite_Connection *self = _pysqlite_Connection_CAST(op); + if (val == NULL) { + PyErr_SetString(PyExc_AttributeError, + "cannot delete autocommit attribute"); + return -1; + } if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) { return -1; } From ca6e733efcb624ebcd63a38b73eb92496bc7ca1b Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 21 Aug 2026 10:05:50 +0300 Subject: [PATCH 05/11] gh-156124: Fix a crash when deleting ctypes Pointer.contents (GH-156125) In the free-threaded build the setter passed the value to Py_BEGIN_CRITICAL_SECTION2() before checking it for NULL. --- Lib/test/test_ctypes/test_delattr.py | 7 ++++++- .../2026-08-20-16-30-00.gh-issue-156124.Kq3vXz.rst | 2 ++ Modules/_ctypes/_ctypes.c | 10 +++++----- 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-20-16-30-00.gh-issue-156124.Kq3vXz.rst diff --git a/Lib/test/test_ctypes/test_delattr.py b/Lib/test/test_ctypes/test_delattr.py index e80b5fa6efb5455..eb99c0dafc86563 100644 --- a/Lib/test/test_ctypes/test_delattr.py +++ b/Lib/test/test_ctypes/test_delattr.py @@ -1,5 +1,5 @@ import unittest -from ctypes import Structure, c_char, c_int +from ctypes import POINTER, Structure, c_char, c_int class X(Structure): @@ -16,6 +16,11 @@ def test_chararray(self): with self.assertRaises(TypeError): del chararray.value + def test_pointer_contents(self): + ptr = POINTER(c_int)(c_int(42)) + with self.assertRaises(TypeError): + del ptr.contents + def test_struct(self): struct = X() with self.assertRaises(TypeError): diff --git a/Misc/NEWS.d/next/Library/2026-08-20-16-30-00.gh-issue-156124.Kq3vXz.rst b/Misc/NEWS.d/next/Library/2026-08-20-16-30-00.gh-issue-156124.Kq3vXz.rst new file mode 100644 index 000000000000000..64882cbc40e2450 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-20-16-30-00.gh-issue-156124.Kq3vXz.rst @@ -0,0 +1,2 @@ +Fix a crash in the free-threaded build when deleting the :attr:`!contents` +attribute of a :mod:`ctypes` pointer. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 034f26807f84aa8..3882b9a5ddff3c6 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5713,11 +5713,6 @@ Pointer_set_contents_lock_held(PyObject *op, PyObject *value, void *closure) PyObject *keep; CDataObject *self = _CDataObject_CAST(op); - if (value == NULL) { - PyErr_SetString(PyExc_TypeError, - "Pointer does not support item deletion"); - return -1; - } ctypes_state *st = get_module_state_by_def(Py_TYPE(Py_TYPE(self))); StgInfo *stginfo; if (PyStgInfo_FromObject(st, op, &stginfo) < 0) { @@ -5761,6 +5756,11 @@ Pointer_set_contents_lock_held(PyObject *op, PyObject *value, void *closure) static int Pointer_set_contents(PyObject *op, PyObject *value, void *closure) { + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, + "Pointer does not support item deletion"); + return -1; + } int res; Py_BEGIN_CRITICAL_SECTION2(op, value); res = Pointer_set_contents_lock_held(op, value, closure); From 53760b3f8d76ecc5a69204b880afcec6d8ed706f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 21 Aug 2026 10:12:58 +0300 Subject: [PATCH 06/11] gh-156101: Fix sqlite3 Cursor.arraysize on a failed assignment (GH-156105) PyLong_AsUInt32() stores 0 in the target on error, so the attribute was clobbered when the assigned value was too large. --- Lib/test/test_sqlite3/test_dbapi.py | 5 +++++ .../Library/2026-08-20-12-10-00.gh-issue-156101.Qb2xNv.rst | 3 +++ Modules/_sqlite/cursor.c | 7 ++++++- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-20-12-10-00.gh-issue-156101.Qb2xNv.rst diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index c21448a92361d7e..375f12e8d4791d7 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1075,9 +1075,14 @@ def test_invalid_array_size(self): UINT32_MAX = (1 << 32) - 1 setter = functools.partial(setattr, self.cu, 'arraysize') + self.cu.arraysize = 2 self.assertRaises(TypeError, setter, 1.0) self.assertRaises(ValueError, setter, -3) self.assertRaises(OverflowError, setter, UINT32_MAX + 1) + self.assertRaises(OverflowError, setter, 2**1000) + self.assertRaises(ValueError, setter, -2**1000) + # a failed assignment does not change the value + self.assertEqual(self.cu.arraysize, 2) def test_fetchmany(self): # no active SQL statement diff --git a/Misc/NEWS.d/next/Library/2026-08-20-12-10-00.gh-issue-156101.Qb2xNv.rst b/Misc/NEWS.d/next/Library/2026-08-20-12-10-00.gh-issue-156101.Qb2xNv.rst new file mode 100644 index 000000000000000..817f4a7207d7fa1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-20-12-10-00.gh-issue-156101.Qb2xNv.rst @@ -0,0 +1,3 @@ +Fix :attr:`sqlite3.Cursor.arraysize` being set to 0 if the assigned value is +too large. +The attribute is now left unchanged if the assignment fails. diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 5a61e43617984d9..3778ccd32fe4c49 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -1378,7 +1378,12 @@ static int _sqlite3_Cursor_arraysize_set_impl(pysqlite_Cursor *self, PyObject *value) /*[clinic end generated code: output=af59a6b09f8cce6e input=ace48cb114e26060]*/ { - return PyLong_AsUInt32(value, &self->arraysize); + uint32_t arraysize; + if (PyLong_AsUInt32(value, &arraysize) < 0) { + return -1; + } + self->arraysize = arraysize; + return 0; } static PyMethodDef cursor_methods[] = { From b9d9c3f99c7b1847659ea55ff1d37530a3d5740a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 21 Aug 2026 10:20:21 +0300 Subject: [PATCH 07/11] gh-155499: Fix curses window.border() and window.box() with 0 (GH-155871) The integer 0 asks for the default character, but it was rejected when passed together with a string character. Choose the drawing function by what the arguments need: only a complexchar cannot be drawn as a byte character, and a string character is narrowed when one is needed. Document the defaults as 0 rather than as the ACS_* codes, which are not defined before initscr(). * Remove the NEWS entry The regression is not in any release: wide characters in border() and box() are new in 3.16. --- Doc/library/curses.rst | 8 +- Lib/test/test_curses.py | 72 +++++++++++++++- Modules/_cursesmodule.c | 138 ++++++++++++++++++++----------- Modules/clinic/_cursesmodule.c.h | 7 +- 4 files changed, 166 insertions(+), 59 deletions(-) diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index e5face72d1e5c9b..c1afdb71c89e888 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -1585,10 +1585,14 @@ Borders and lines The wide default value is used when the border is drawn from string characters or :class:`complexchar` cells. + If any parameter is a byte character or an integer other than ``0``, the + border is drawn from byte characters, and every string character must be + encodable as a single byte. + .. versionchanged:: next Wide and combining characters, and :class:`complexchar` cells, are now accepted. A single call cannot mix - them with integer or byte characters. + :class:`complexchar` cells with integer or byte characters. .. method:: window.box([vertch, horch]) @@ -1598,7 +1602,7 @@ Borders and lines .. versionchanged:: next Wide and combining characters, and :class:`complexchar` cells, are now accepted. A single call cannot mix - them with integer or byte characters. + :class:`complexchar` cells with integer or byte characters. .. method:: window.hline(ch, n[, attr]) window.hline(y, x, ch, n[, attr]) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 6a7a8e320438f85..ac0110c671455c3 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -484,8 +484,76 @@ def test_wide_characters(self): if self._encodable(vline + hline): stdscr.border(vline, vline, hline, hline) stdscr.box(vline, hline) - # border() and box() cannot mix integer and wide-string characters. - self.assertRaises(TypeError, stdscr.box, vline, ord('-')) + # border() and box() cannot mix a complexchar with an integer + # character; a wide string character is narrowed instead, which only + # works if it is a single byte. + self.assertRaises(TypeError, stdscr.box, + curses.complexchar(vline), ord('-')) + + @requires_wide_build + def test_border_default_characters(self): + # 0 requests the default character, as an omitted argument does, + # even in a border drawn with wide characters. + win = curses.newwin(5, 10, 5, 2) + maxy, maxx = win.getmaxyx() + corners = [(0, 0), (0, maxx-1), (maxy-1, 0), (maxy-1, maxx-1)] + win.border('|', '|', '-', '-', 0, 0, 0, 0) + with_zeros = [win.in_wch(y, x) for y, x in corners] + win.erase() + win.border('|', '|', '-', '-') + self.assertEqual([win.in_wch(y, x) for y, x in corners], with_zeros) + win.border(0, '|', 0, '-', 0, 0, 0, 0) + vline = curses.complexchar('|') + hline = curses.complexchar('-') + win.border(vline, vline, hline, hline, 0, 0, 0, 0) + # box() takes 0 for either side, and draws the same default + # characters as an omitted border() argument. + win.erase() + win.border('|', '|') + default_corner = win.in_wch(0, 0) + default_hline = win.in_wch(0, 1) + win.erase() + win.border(0, 0, '-', '-') + default_vline = win.in_wch(1, 0) + win.erase() + win.box('|', 0) + self.assertEqual(win.in_wch(0, 0), default_corner) + self.assertEqual(win.in_wch(0, 1), default_hline) + win.erase() + win.box(0, '-') + self.assertEqual(win.in_wch(1, 0), default_vline) + win.box(vline, 0) + + @requires_wide_build + def test_border_mixed_characters(self): + # Integer and bytes characters other than 0 are only drawn by the + # narrow function, which draws string characters as single bytes. + win = curses.newwin(5, 10, 5, 2) + win.border('|', '|', '-', '-', 65, 66, 67, 68) + self.assertEqual(win.instr(0, 0), b'A--------B') + self.assertEqual(win.instr(1, 0), b'| |') + self.assertEqual(win.instr(4, 0), b'C--------D') + win.border('|', b'!') + self.assertEqual(win.instr(1, 0), b'| !') + # b'\0' is a byte character, not the sentinel, but the narrow function + # draws a zero character as the default one. + win.border('|', b'\0') + # A complexchar cannot be drawn as a byte. + cc = curses.complexchar('|') + self.assertRaises(TypeError, win.border, cc, 65) + self.assertRaises(TypeError, win.border, cc, b'!') + # Neither can a string character that is not a single byte. + vline = '\u2502' + if len(vline.encode(win.encoding, 'replace')) != 1: + self.assertRaises(OverflowError, win.border, vline, 65) + # box() follows the same rules. + win.box('|', 45) + self.assertEqual(win.instr(1, 0), b'| |') + win.box(b'|', '-') + self.assertRaises(TypeError, win.box, cc, 45) + self.assertRaises(TypeError, win.box, cc, b'-') + if len(vline.encode(win.encoding, 'replace')) != 1: + self.assertRaises(OverflowError, win.box, vline, 45) @requires_wide_build def test_wacs_constants(self): diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index ea399e25213aef2..e208b2d3d52acce 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -2699,21 +2699,21 @@ _curses_window_bkgdset_impl(PyCursesWindowObject *self, PyObject *ch, /*[clinic input] _curses.window.border - ls: object(c_default="NULL") = _curses.ACS_VLINE + ls: object(c_default="NULL") = 0 Left side. - rs: object(c_default="NULL") = _curses.ACS_VLINE + rs: object(c_default="NULL") = 0 Right side. - ts: object(c_default="NULL") = _curses.ACS_HLINE + ts: object(c_default="NULL") = 0 Top side. - bs: object(c_default="NULL") = _curses.ACS_HLINE + bs: object(c_default="NULL") = 0 Bottom side. - tl: object(c_default="NULL") = _curses.ACS_ULCORNER + tl: object(c_default="NULL") = 0 Upper-left corner. - tr: object(c_default="NULL") = _curses.ACS_URCORNER + tr: object(c_default="NULL") = 0 Upper-right corner. - bl: object(c_default="NULL") = _curses.ACS_LLCORNER + bl: object(c_default="NULL") = 0 Bottom-left corner. - br: object(c_default="NULL") = _curses.ACS_LRCORNER + br: object(c_default="NULL") = 0 Bottom-right corner. / @@ -2730,7 +2730,7 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls, PyObject *rs, PyObject *ts, PyObject *bs, PyObject *tl, PyObject *tr, PyObject *bl, PyObject *br) -/*[clinic end generated code: output=670ef38d3d7c2aa3 input=42568c1458221d24]*/ +/*[clinic end generated code: output=670ef38d3d7c2aa3 input=d826ce9d6335479a]*/ { chtype ch[8]; int i, rtn; @@ -2743,36 +2743,49 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls, #ifdef HAVE_NCURSESW cchar_t wch[8]; const cchar_t *wch_p[8]; - int use_wide = 0; - int types[8]; + /* Only wborder_set() draws a complexchar and only wborder() an integer + or bytes character; a string character suits both, and so does the + integer 0, which asks for the default character. */ + int has_narrow = 0, has_str = 0, has_cchar = 0; for (i = 0; i < 8; i++) { - types[i] = 0; + wch_p[i] = NULL; /* use the default character */ if (objs[i] != NULL) { - types[i] = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0, + int type = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0, "border", &ch[i], &wch[i]); - if (types[i] == 0) { + if (type == 0) { return NULL; } - if (types[i] == 2) { - use_wide = 1; + if (type == 2) { + wch_p[i] = &wch[i]; + if (PyUnicode_Check(objs[i])) { + has_str = 1; + } + else { + has_cchar = 1; + } + } + else if (!PyLong_CheckExact(objs[i]) || ch[i] != 0) { + has_narrow = 1; /* b'\0' is a byte character, not the 0 */ } } } - if (use_wide) { + if (has_narrow) { + if (has_cchar) { + PyErr_SetString(PyExc_TypeError, + "border() cannot mix complexchar characters " + "with integer or bytes characters"); + return NULL; + } + /* Narrow the string characters. */ for (i = 0; i < 8; i++) { - if (objs[i] == NULL) { - wch_p[i] = NULL; /* use the default character */ - } - else if (types[i] == 2) { - wch_p[i] = &wch[i]; - } - else { - PyErr_SetString(PyExc_TypeError, - "border() cannot mix integer or bytes " - "characters with wide string characters"); + if (objs[i] != NULL && PyUnicode_Check(objs[i]) && + !PyCurses_ConvertToChtype(self, objs[i], &ch[i])) + { return NULL; } } + } + else if (has_str || has_cchar) { rtn = wborder_set(self->win, wch_p[0], wch_p[1], wch_p[2], wch_p[3], wch_p[4], wch_p[5], wch_p[6], wch_p[7]); @@ -2815,42 +2828,67 @@ _curses_window_box_impl(PyCursesWindowObject *self, int group_right_1, PyObject *verch, PyObject *horch) /*[clinic end generated code: output=f3fcb038bb287192 input=e11acb7dbf6790b6]*/ { - chtype ch1 = 0, ch2 = 0; + chtype ch[2] = {0, 0}; + PyObject *objs[2] = {verch, horch}; + int i; #ifdef HAVE_NCURSESW - cchar_t wch1, wch2; - int t1 = 0, t2 = 0; + cchar_t wch[2]; + const cchar_t *wch_p[2] = {NULL, NULL}; + int has_narrow = 0, has_str = 0, has_cchar = 0; if (group_right_1) { - t1 = PyCurses_ConvertToCell(self, verch, A_NORMAL, 0, "box", &ch1, &wch1); - if (t1 == 0) { - return NULL; - } - t2 = PyCurses_ConvertToCell(self, horch, A_NORMAL, 0, "box", &ch2, &wch2); - if (t2 == 0) { - return NULL; + for (i = 0; i < 2; i++) { + int type = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0, + "box", &ch[i], &wch[i]); + if (type == 0) { + return NULL; + } + if (type == 2) { + wch_p[i] = &wch[i]; + if (PyUnicode_Check(objs[i])) { + has_str = 1; + } + else { + has_cchar = 1; + } + } + else if (!PyLong_CheckExact(objs[i]) || ch[i] != 0) { + has_narrow = 1; /* b'\0' is a byte character, not the 0 */ + } } } - if (t1 == 2 || t2 == 2) { - if (t1 != 2 || t2 != 2) { + if (has_narrow) { + if (has_cchar) { PyErr_SetString(PyExc_TypeError, - "box() cannot mix integer or bytes characters " - "with wide string characters"); + "box() cannot mix complexchar characters " + "with integer or bytes characters"); return NULL; } - int rtn = wborder_set(self->win, &wch1, &wch1, &wch2, &wch2, - NULL, NULL, NULL, NULL); - return curses_window_check_err(self, rtn, "wborder_set", "box"); + /* Narrow the string characters. */ + for (i = 0; i < 2; i++) { + if (PyUnicode_Check(objs[i]) && + !PyCurses_ConvertToChtype(self, objs[i], &ch[i])) + { + return NULL; + } + } + } + else if (has_str || has_cchar) { + int rtn = box_set(self->win, wch_p[0], wch_p[1]); + return curses_window_check_err(self, rtn, "box_set", "box"); } #else if (group_right_1) { - if (!PyCurses_ConvertToCell(self, verch, A_NORMAL, 0, "box", &ch1)) { - return NULL; - } - if (!PyCurses_ConvertToCell(self, horch, A_NORMAL, 0, "box", &ch2)) { - return NULL; + for (i = 0; i < 2; i++) { + if (!PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0, "box", + &ch[i])) + { + return NULL; + } } } #endif - return curses_window_check_err(self, box(self->win, ch1, ch2), "box", NULL); + return curses_window_check_err(self, box(self->win, ch[0], ch[1]), + "box", NULL); } #if defined(HAVE_NCURSES_H) || defined(MVWDELCH_IS_EXPRESSION) diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index b4cb294e3bb61a5..d2f30178b1c33c7 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -757,10 +757,7 @@ _curses_window_bkgdset(PyObject *self, PyObject *args) } PyDoc_STRVAR(_curses_window_border__doc__, -"border($self, ls=_curses.ACS_VLINE, rs=_curses.ACS_VLINE,\n" -" ts=_curses.ACS_HLINE, bs=_curses.ACS_HLINE,\n" -" tl=_curses.ACS_ULCORNER, tr=_curses.ACS_URCORNER,\n" -" bl=_curses.ACS_LLCORNER, br=_curses.ACS_LRCORNER, /)\n" +"border($self, ls=0, rs=0, ts=0, bs=0, tl=0, tr=0, bl=0, br=0, /)\n" "--\n" "\n" "Draw a border around the edges of the window.\n" @@ -6585,4 +6582,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=680f621e7c1f101b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=4e98ddbfb69f2c04 input=a9049054013a1b77]*/ From 8e96dd6f25b53f0773d3f3f2a898eaa1501ee92e Mon Sep 17 00:00:00 2001 From: Steve Stagg Date: Fri, 21 Aug 2026 08:58:49 +0100 Subject: [PATCH 08/11] gh-152817: Prevent deletion of sqlite3 `cursor.row_factory` attr, missed from: gh-149738 (GH-152818) --- Lib/test/test_sqlite3/test_factory.py | 8 +++++++ ...-05-13-06-54-41.gh-issue-149738.4BLFoH.rst | 2 +- Modules/_sqlite/cursor.c | 22 ++++++++++++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sqlite3/test_factory.py b/Lib/test/test_sqlite3/test_factory.py index b9b18fdee872269..2dd42921d31ddd5 100644 --- a/Lib/test/test_sqlite3/test_factory.py +++ b/Lib/test/test_sqlite3/test_factory.py @@ -156,6 +156,14 @@ def test_delete_connection_text_factory(self): with self.assertRaises(AttributeError): del self.con.text_factory + def test_delete_cursor_row_factory(self): + # gh-149738: deleting row_factory should raise an exception + cur = self.con.cursor() + with self.assertRaises(AttributeError): + del cur.row_factory + # Executing a query here should succeed. + self.assertEqual(tuple(cur.execute("select 1").fetchone()), (1,)) + def test_sqlite_row_index_unicode(self): row = self.con.execute("select 1 as \xff").fetchone() self.assertEqual(row["\xff"], 1) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst index e62b681d716650b..e1935555b091742 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst @@ -1,2 +1,2 @@ :mod:`sqlite3`: Disallow removing ``row_factory`` and ``text_factory`` attributes -of a connection to prevent a crash on a query. +of a connection or cursor to prevent a crash on a query. diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 3778ccd32fe4c49..96fb3dc9e42d3ac 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -1405,13 +1405,33 @@ static struct PyMemberDef cursor_members[] = {"description", _Py_T_OBJECT, offsetof(pysqlite_Cursor, description), Py_READONLY}, {"lastrowid", _Py_T_OBJECT, offsetof(pysqlite_Cursor, lastrowid), Py_READONLY}, {"rowcount", Py_T_LONG, offsetof(pysqlite_Cursor, rowcount), Py_READONLY}, - {"row_factory", _Py_T_OBJECT, offsetof(pysqlite_Cursor, row_factory), 0}, {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(pysqlite_Cursor, in_weakreflist), Py_READONLY}, {NULL} }; +static PyObject * +cursor_get_row_factory(PyObject *op, void *Py_UNUSED(closure)) +{ + pysqlite_Cursor *self = _pysqlite_Cursor_CAST(op); + return Py_NewRef(self->row_factory); +} + +static int +cursor_set_row_factory(PyObject *op, PyObject *value, void *Py_UNUSED(closure)) +{ + pysqlite_Cursor *self = _pysqlite_Cursor_CAST(op); + if (value == NULL) { + PyErr_SetString(PyExc_AttributeError, + "cannot delete row_factory attribute"); + return -1; + } + Py_XSETREF(self->row_factory, Py_NewRef(value)); + return 0; +} + static struct PyGetSetDef cursor_getsets[] = { _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF + {"row_factory", cursor_get_row_factory, cursor_set_row_factory}, {NULL}, }; From 8db5262b377ebb74c494169fcbc5b601914a3418 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 21 Aug 2026 05:01:50 -0400 Subject: [PATCH 09/11] gh-153227: Unpin development version of Pygments (#156163) --- Doc/pylock.toml | 141 ++++++++++++++++++++----------------------- Doc/requirements.txt | 4 +- 2 files changed, 66 insertions(+), 79 deletions(-) diff --git a/Doc/pylock.toml b/Doc/pylock.toml index 64a051c2399b0af..af4627c2ad31d49 100644 --- a/Doc/pylock.toml +++ b/Doc/pylock.toml @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile Doc/requirements.txt --exclude-newer P14D --exclude-newer-package linklint=PT0S --exclude-newer-package python-docs-theme=PT0S --no-cache --output-file Doc/pylock.toml --python-version 3.12 --universal +# uv pip compile Doc/requirements.txt --exclude-newer P14D --exclude-newer-package linklint=PT0S --exclude-newer-package python-docs-theme=PT0S --exclude-newer-package pygments=PT0S --no-cache --output-file Doc/pylock.toml --python-version 3.12 --universal lock-version = "1.0" created-by = "uv" requires-python = ">=3.12" @@ -24,80 +24,68 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/b4/03/374bd9e31b58e8a [[packages]] name = "certifi" -version = "2026.6.17" -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", upload-time = 2026-06-17T10:31:07Z, size = 134594, hashes = { sha256 = "024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", upload-time = 2026-06-17T10:31:06Z, size = 133289, hashes = { sha256 = "2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db" } }] +version = "2026.7.22" +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", upload-time = 2026-07-22T03:35:12Z, size = 138112, hashes = { sha256 = "741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", upload-time = 2026-07-22T03:35:11Z, size = 136983, hashes = { sha256 = "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775" } }] [[packages]] name = "charset-normalizer" -version = "3.4.7" -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", upload-time = 2026-04-02T09:28:39Z, size = 144271, hashes = { sha256 = "ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5" } } +version = "3.4.9" +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", upload-time = 2026-07-07T14:34:58Z, size = 152439, hashes = { sha256 = "673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b" } } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", upload-time = 2026-04-02T09:26:24Z, size = 311328, hashes = { sha256 = "eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46" } }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-04-02T09:26:25Z, size = 208061, hashes = { sha256 = "6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2" } }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-04-02T09:26:26Z, size = 229031, hashes = { sha256 = "e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b" } }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", upload-time = 2026-04-02T09:26:28Z, size = 225239, hashes = { sha256 = "edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a" } }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-04-02T09:26:29Z, size = 216589, hashes = { sha256 = "5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116" } }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", upload-time = 2026-04-02T09:26:30Z, size = 202733, hashes = { sha256 = "203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb" } }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-04-02T09:26:31Z, size = 212652, hashes = { sha256 = "298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1" } }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", upload-time = 2026-04-02T09:26:33Z, size = 211229, hashes = { sha256 = "708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15" } }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", upload-time = 2026-04-02T09:26:34Z, size = 203552, hashes = { sha256 = "0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5" } }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", upload-time = 2026-04-02T09:26:36Z, size = 230806, hashes = { sha256 = "4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d" } }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", upload-time = 2026-04-02T09:26:37Z, size = 212316, hashes = { sha256 = "aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7" } }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", upload-time = 2026-04-02T09:26:38Z, size = 227274, hashes = { sha256 = "fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464" } }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", upload-time = 2026-04-02T09:26:40Z, size = 218468, hashes = { sha256 = "bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49" } }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", upload-time = 2026-04-02T09:26:41Z, size = 148460, hashes = { sha256 = "2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c" } }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", upload-time = 2026-04-02T09:26:42Z, size = 159330, hashes = { sha256 = "5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6" } }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", upload-time = 2026-04-02T09:26:44Z, size = 147828, hashes = { sha256 = "56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d" } }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", upload-time = 2026-04-02T09:26:45Z, size = 309627, hashes = { sha256 = "f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063" } }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-04-02T09:26:46Z, size = 207008, hashes = { sha256 = "0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c" } }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-04-02T09:26:48Z, size = 228303, hashes = { sha256 = "a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66" } }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", upload-time = 2026-04-02T09:26:49Z, size = 224282, hashes = { sha256 = "3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18" } }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-04-02T09:26:50Z, size = 215595, hashes = { sha256 = "e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd" } }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", upload-time = 2026-04-02T09:26:52Z, size = 201986, hashes = { sha256 = "f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215" } }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-04-02T09:26:53Z, size = 211711, hashes = { sha256 = "e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859" } }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", upload-time = 2026-04-02T09:26:54Z, size = 210036, hashes = { sha256 = "7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8" } }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", upload-time = 2026-04-02T09:26:56Z, size = 202998, hashes = { sha256 = "481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5" } }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", upload-time = 2026-04-02T09:26:57Z, size = 230056, hashes = { sha256 = "f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832" } }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", upload-time = 2026-04-02T09:26:58Z, size = 211537, hashes = { sha256 = "f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6" } }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", upload-time = 2026-04-02T09:27:00Z, size = 226176, hashes = { sha256 = "3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48" } }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", upload-time = 2026-04-02T09:27:02Z, size = 217723, hashes = { sha256 = "64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a" } }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", upload-time = 2026-04-02T09:27:03Z, size = 148085, hashes = { sha256 = "4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e" } }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", upload-time = 2026-04-02T09:27:04Z, size = 158819, hashes = { sha256 = "3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110" } }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", upload-time = 2026-04-02T09:27:05Z, size = 147915, hashes = { sha256 = "80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b" } }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", upload-time = 2026-04-02T09:27:07Z, size = 309234, hashes = { sha256 = "c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0" } }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-04-02T09:27:08Z, size = 208042, hashes = { sha256 = "1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a" } }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-04-02T09:27:09Z, size = 228706, hashes = { sha256 = "54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b" } }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", upload-time = 2026-04-02T09:27:11Z, size = 224727, hashes = { sha256 = "715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41" } }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-04-02T09:27:12Z, size = 215882, hashes = { sha256 = "bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e" } }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", upload-time = 2026-04-02T09:27:13Z, size = 200860, hashes = { sha256 = "c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae" } }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-04-02T09:27:15Z, size = 211564, hashes = { sha256 = "3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18" } }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", upload-time = 2026-04-02T09:27:16Z, size = 211276, hashes = { sha256 = "e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b" } }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", upload-time = 2026-04-02T09:27:18Z, size = 201238, hashes = { sha256 = "a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356" } }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", upload-time = 2026-04-02T09:27:19Z, size = 230189, hashes = { sha256 = "2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab" } }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", upload-time = 2026-04-02T09:27:20Z, size = 211352, hashes = { sha256 = "e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46" } }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", upload-time = 2026-04-02T09:27:22Z, size = 227024, hashes = { sha256 = "d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44" } }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", upload-time = 2026-04-02T09:27:23Z, size = 217869, hashes = { sha256 = "7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72" } }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", upload-time = 2026-04-02T09:27:25Z, size = 148541, hashes = { sha256 = "5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10" } }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", upload-time = 2026-04-02T09:27:26Z, size = 159634, hashes = { sha256 = "92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f" } }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", upload-time = 2026-04-02T09:27:28Z, size = 148384, hashes = { sha256 = "67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246" } }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", upload-time = 2026-04-02T09:27:29Z, size = 330133, hashes = { sha256 = "effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24" } }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-04-02T09:27:30Z, size = 216257, hashes = { sha256 = "fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79" } }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-04-02T09:27:32Z, size = 234851, hashes = { sha256 = "733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960" } }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", upload-time = 2026-04-02T09:27:34Z, size = 233393, hashes = { sha256 = "a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4" } }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-04-02T09:27:35Z, size = 223251, hashes = { sha256 = "6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e" } }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", upload-time = 2026-04-02T09:27:36Z, size = 206609, hashes = { sha256 = "a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1" } }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-04-02T09:27:38Z, size = 220014, hashes = { sha256 = "3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44" } }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", upload-time = 2026-04-02T09:27:39Z, size = 218979, hashes = { sha256 = "8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e" } }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", upload-time = 2026-04-02T09:27:40Z, size = 209238, hashes = { sha256 = "cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3" } }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", upload-time = 2026-04-02T09:27:42Z, size = 236110, hashes = { sha256 = "0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0" } }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", upload-time = 2026-04-02T09:27:43Z, size = 219824, hashes = { sha256 = "752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e" } }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", upload-time = 2026-04-02T09:27:45Z, size = 233103, hashes = { sha256 = "8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb" } }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", upload-time = 2026-04-02T09:27:46Z, size = 225194, hashes = { sha256 = "ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe" } }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", upload-time = 2026-04-02T09:27:48Z, size = 159827, hashes = { sha256 = "c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0" } }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", upload-time = 2026-04-02T09:27:49Z, size = 174168, hashes = { sha256 = "03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c" } }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", upload-time = 2026-04-02T09:27:51Z, size = 153018, hashes = { sha256 = "c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d" } }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", upload-time = 2026-04-02T09:28:37Z, size = 61958, hashes = { sha256 = "3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d" } }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", upload-time = 2026-07-07T14:33:15Z, size = 319300, hashes = { sha256 = "45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0" } }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-07-07T14:33:17Z, size = 215802, hashes = { sha256 = "9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9" } }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-07-07T14:33:18Z, size = 237171, hashes = { sha256 = "9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44" } }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", upload-time = 2026-07-07T14:33:20Z, size = 233075, hashes = { sha256 = "7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9" } }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-07-07T14:33:21Z, size = 224256, hashes = { sha256 = "5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd" } }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", upload-time = 2026-07-07T14:33:23Z, size = 208784, hashes = { sha256 = "90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84" } }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-07-07T14:33:24Z, size = 219928, hashes = { sha256 = "9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b" } }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", upload-time = 2026-07-07T14:33:26Z, size = 218489, hashes = { sha256 = "60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde" } }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", upload-time = 2026-07-07T14:33:27Z, size = 210267, hashes = { sha256 = "a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39" } }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", upload-time = 2026-07-07T14:33:29Z, size = 226030, hashes = { sha256 = "03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62" } }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", upload-time = 2026-07-07T14:33:30Z, size = 151185, hashes = { sha256 = "78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642" } }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", upload-time = 2026-07-07T14:33:32Z, size = 162557, hashes = { sha256 = "4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0" } }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", upload-time = 2026-07-07T14:33:33Z, size = 152665, hashes = { sha256 = "78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2" } }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", upload-time = 2026-07-07T14:33:35Z, size = 317688, hashes = { sha256 = "440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614" } }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-07-07T14:33:36Z, size = 214982, hashes = { sha256 = "21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698" } }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-07-07T14:33:38Z, size = 236460, hashes = { sha256 = "e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b" } }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", upload-time = 2026-07-07T14:33:40Z, size = 232003, hashes = { sha256 = "bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9" } }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-07-07T14:33:41Z, size = 223149, hashes = { sha256 = "84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33" } }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", upload-time = 2026-07-07T14:33:43Z, size = 207901, hashes = { sha256 = "5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63" } }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-07-07T14:33:44Z, size = 219176, hashes = { sha256 = "a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0" } }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", upload-time = 2026-07-07T14:33:46Z, size = 217356, hashes = { sha256 = "416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe" } }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", upload-time = 2026-07-07T14:33:47Z, size = 209614, hashes = { sha256 = "75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35" } }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", upload-time = 2026-07-07T14:33:49Z, size = 224991, hashes = { sha256 = "69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8" } }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", upload-time = 2026-07-07T14:33:50Z, size = 150622, hashes = { sha256 = "51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9" } }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", upload-time = 2026-07-07T14:33:52Z, size = 161947, hashes = { sha256 = "fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115" } }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", upload-time = 2026-07-07T14:33:53Z, size = 152594, hashes = { sha256 = "611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012" } }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", upload-time = 2026-07-07T14:33:54Z, size = 317253, hashes = { sha256 = "0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380" } }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-07-07T14:33:56Z, size = 215898, hashes = { sha256 = "8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9" } }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-07-07T14:33:57Z, size = 236718, hashes = { sha256 = "33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4" } }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", upload-time = 2026-07-07T14:33:59Z, size = 232519, hashes = { sha256 = "f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a" } }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-07-07T14:34:01Z, size = 223143, hashes = { sha256 = "c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046" } }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", upload-time = 2026-07-07T14:34:03Z, size = 206742, hashes = { sha256 = "f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81" } }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-07-07T14:34:04Z, size = 219191, hashes = { sha256 = "4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917" } }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", upload-time = 2026-07-07T14:34:06Z, size = 218328, hashes = { sha256 = "a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41" } }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", upload-time = 2026-07-07T14:34:07Z, size = 207406, hashes = { sha256 = "d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1" } }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", upload-time = 2026-07-07T14:34:09Z, size = 225157, hashes = { sha256 = "898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf" } }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", upload-time = 2026-07-07T14:34:10Z, size = 151095, hashes = { sha256 = "c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48" } }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", upload-time = 2026-07-07T14:34:12Z, size = 162796, hashes = { sha256 = "16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b" } }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", upload-time = 2026-07-07T14:34:14Z, size = 153334, hashes = { sha256 = "40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519" } }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", upload-time = 2026-07-07T14:34:15Z, size = 338848, hashes = { sha256 = "609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198" } }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-07-07T14:34:17Z, size = 223022, hashes = { sha256 = "51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32" } }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-07-07T14:34:18Z, size = 241590, hashes = { sha256 = "cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632" } }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", upload-time = 2026-07-07T14:34:20Z, size = 239584, hashes = { sha256 = "fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf" } }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-07-07T14:34:22Z, size = 230224, hashes = { sha256 = "df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990" } }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", upload-time = 2026-07-07T14:34:23Z, size = 212667, hashes = { sha256 = "f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d" } }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-07-07T14:34:25Z, size = 227179, hashes = { sha256 = "32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e" } }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", upload-time = 2026-07-07T14:34:27Z, size = 225372, hashes = { sha256 = "83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c" } }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", upload-time = 2026-07-07T14:34:28Z, size = 215222, hashes = { sha256 = "cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2" } }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", upload-time = 2026-07-07T14:34:30Z, size = 231958, hashes = { sha256 = "ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534" } }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", upload-time = 2026-07-07T14:34:31Z, size = 155580, hashes = { sha256 = "0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226" } }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", upload-time = 2026-07-07T14:34:33Z, size = 167620, hashes = { sha256 = "9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177" } }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", upload-time = 2026-07-07T14:34:35Z, size = 158037, hashes = { sha256 = "19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501" } }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", upload-time = 2026-07-07T14:34:56Z, size = 64538, hashes = { sha256 = "68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5" } }, ] [[packages]] @@ -162,14 +150,15 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c [[packages]] name = "pygments" -version = "2.20.0" -archive = { url = "https://github.com/pygments/pygments/archive/2cad2642058441b59782a6a18f03c98c42d081f1.tar.gz", hashes = { sha256 = "e6ae46831285e86355eabb969bf2d0520655b001dd882147637f4bc500e56bfb" } } +version = "2.21.0" +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", upload-time = 2026-08-17T08:02:48Z, size = 5005329, hashes = { sha256 = "610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", upload-time = 2026-08-17T08:02:44Z, size = 1250147, hashes = { sha256 = "2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9" } }] [[packages]] name = "python-docs-theme" -version = "2026.4" -sdist = { url = "https://files.pythonhosted.org/packages/fd/59/dbb07775a15ddf9f7f8d5f6ef4cd4da5e8afd908cc27e6585bb132e6366a/python_docs_theme-2026.4.tar.gz", upload-time = 2026-04-19T18:35:13Z, size = 29782, hashes = { sha256 = "a815f80c5a09f734449eb2498fbcbad05340976a7a543e431f57de92218a9315" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/db/05/b9298eb9330c70a3d1465a6116ab01dad095538c2e574a2d704bb0002f4d/python_docs_theme-2026.4-py3-none-any.whl", upload-time = 2026-04-19T18:35:12Z, size = 73742, hashes = { sha256 = "f755d80ebe8d7aa4fad8ee964ff999635c72eebd24ab10928a0e9726363d65fc" } }] +version = "2026.7" +sdist = { url = "https://files.pythonhosted.org/packages/54/ba/6de432a297e933eeee26a950298254061d3738b183b0d4c01d512a6a2575/python_docs_theme-2026.7.tar.gz", upload-time = 2026-07-27T20:12:04Z, size = 38838, hashes = { sha256 = "465431be2ebc5239e8f41b0acfae0cf8d842b50ec2fea549f61d9e6a3802432d" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/28/11/4dcaea01fd7bb557159f16b8015906b7a002327704972facac4af706307d/python_docs_theme-2026.7-py3-none-any.whl", upload-time = 2026-07-27T20:12:03Z, size = 47030, hashes = { sha256 = "6099e550bdce042d709db29375228a4fd51bfb238b7fad413782f2b402190c9e" } }] [[packages]] name = "requests" diff --git a/Doc/requirements.txt b/Doc/requirements.txt index c5cd360ff317521..edaec74546b3c12 100644 --- a/Doc/requirements.txt +++ b/Doc/requirements.txt @@ -9,9 +9,7 @@ # Keep this version in sync with ``Doc/conf.py``. sphinx<9.0.0 -# Temporary direct requirement, pending release of Pygments > 2.20.0 -# https://github.com/pygments/pygments/discussions/3145 -pygments @ https://github.com/pygments/pygments/archive/2cad2642058441b59782a6a18f03c98c42d081f1.tar.gz +pygments>=2.21 blurb From 033dc43bcf299066e34cd4714933c56f3339953c Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 21 Aug 2026 10:03:51 +0100 Subject: [PATCH 10/11] gh-155952: Fix `` Argument Clinic defaults in several signatures (#155954) --- Lib/test/test_inspect/test_inspect.py | 2 +- .../Library/2026-08-17-12-40-00.gh-issue-155952.Vq3Lm8.rst | 3 +++ Modules/_localemodule.c | 4 ++-- Modules/_sqlite/clinic/connection.c.h | 4 ++-- Modules/_sqlite/connection.c | 4 ++-- Modules/clinic/_localemodule.c.h | 4 ++-- Objects/clinic/typevarobject.c.h | 4 ++-- Objects/typevarobject.c | 4 ++-- Python/_warnings.c | 4 ++-- Python/clinic/_warnings.c.h | 4 ++-- 10 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-17-12-40-00.gh-issue-155952.Vq3Lm8.rst diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index ff7475447e95a03..c68c643cb97fb45 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6411,7 +6411,7 @@ def test_typing_module_has_signatures(self): methods_unsupported_signature=methods_unsupported_signature) def test_warnings_module_has_signatures(self): - unsupported_signature = {'warn', 'warn_explicit'} + unsupported_signature = {'warn_explicit'} self._test_module_has_signatures(warnings, unsupported_signature=unsupported_signature) def test_weakref_module_has_signatures(self): diff --git a/Misc/NEWS.d/next/Library/2026-08-17-12-40-00.gh-issue-155952.Vq3Lm8.rst b/Misc/NEWS.d/next/Library/2026-08-17-12-40-00.gh-issue-155952.Vq3Lm8.rst new file mode 100644 index 000000000000000..92b4a55692ee50a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-17-12-40-00.gh-issue-155952.Vq3Lm8.rst @@ -0,0 +1,3 @@ +Fix the signatures of :meth:`sqlite3.Connection.execute`, :func:`warnings.warn` +and :func:`locale.setlocale` which rendered ```` instead of the +correct defaults for parameters. diff --git a/Modules/_localemodule.c b/Modules/_localemodule.c index 20783dc00f90d3c..ba8b421c25a47cc 100644 --- a/Modules/_localemodule.c +++ b/Modules/_localemodule.c @@ -126,7 +126,7 @@ check_locale_name_all(const char *locale) _locale.setlocale category: int - locale: str(accept={str, NoneType}) = NULL + locale: str(accept={str, NoneType}) = None / Activates/queries locale processing. @@ -134,7 +134,7 @@ Activates/queries locale processing. static PyObject * _locale_setlocale_impl(PyObject *module, int category, const char *locale) -/*[clinic end generated code: output=a0e777ae5d2ff117 input=dbe18f1d66c57a6a]*/ +/*[clinic end generated code: output=a0e777ae5d2ff117 input=b53449b63407179b]*/ { char *result; PyObject *result_object; diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index b645bf3464bcea1..2feea0ec97bbcae 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -933,7 +933,7 @@ pysqlite_connection_load_extension(PyObject *self, PyObject *const *args, Py_ssi #endif /* defined(PY_SQLITE_ENABLE_LOAD_EXTENSION) */ PyDoc_STRVAR(pysqlite_connection_execute__doc__, -"execute($self, sql, parameters=, /)\n" +"execute($self, sql, parameters=(), /)\n" "--\n" "\n" "Executes an SQL statement."); @@ -1725,4 +1725,4 @@ getconfig(PyObject *self, PyObject *arg) #ifndef DESERIALIZE_METHODDEF #define DESERIALIZE_METHODDEF #endif /* !defined(DESERIALIZE_METHODDEF) */ -/*[clinic end generated code: output=1418b72751ef68fc input=a9049054013a1b77]*/ +/*[clinic end generated code: output=11ccc746e9223121 input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index ec47471873f7822..ede996b0598ee77 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1877,7 +1877,7 @@ pysqlite_connection_call(PyObject *op, PyObject *args, PyObject *kwargs) _sqlite3.Connection.execute as pysqlite_connection_execute sql: unicode - parameters: object = NULL + parameters: object(c_default = 'NULL') = () / Executes an SQL statement. @@ -1886,7 +1886,7 @@ Executes an SQL statement. static PyObject * pysqlite_connection_execute_impl(pysqlite_Connection *self, PyObject *sql, PyObject *parameters) -/*[clinic end generated code: output=5be05ae01ee17ee4 input=27aa7792681ddba2]*/ +/*[clinic end generated code: output=5be05ae01ee17ee4 input=847390a17de45cc7]*/ { PyObject* result = 0; diff --git a/Modules/clinic/_localemodule.c.h b/Modules/clinic/_localemodule.c.h index 5e0880b0d0bb4c0..118946b81474ea1 100644 --- a/Modules/clinic/_localemodule.c.h +++ b/Modules/clinic/_localemodule.c.h @@ -5,7 +5,7 @@ preserve #include "pycore_modsupport.h" // _PyArg_CheckPositional() PyDoc_STRVAR(_locale_setlocale__doc__, -"setlocale($module, category, locale=, /)\n" +"setlocale($module, category, locale=None, /)\n" "--\n" "\n" "Activates/queries locale processing."); @@ -595,4 +595,4 @@ _locale_getencoding(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _LOCALE_BIND_TEXTDOMAIN_CODESET_METHODDEF #define _LOCALE_BIND_TEXTDOMAIN_CODESET_METHODDEF #endif /* !defined(_LOCALE_BIND_TEXTDOMAIN_CODESET_METHODDEF) */ -/*[clinic end generated code: output=034a3c219466d207 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=bb987603f9de8824 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/typevarobject.c.h b/Objects/clinic/typevarobject.c.h index d2f350a3487f08b..95f09faceba0b59 100644 --- a/Objects/clinic/typevarobject.c.h +++ b/Objects/clinic/typevarobject.c.h @@ -727,7 +727,7 @@ typealias_reduce(PyObject *self, PyObject *Py_UNUSED(ignored)) } PyDoc_STRVAR(typealias_new__doc__, -"typealias(name, value, *, type_params=, qualname=None)\n" +"typealias(name, value, *, type_params=(), qualname=None)\n" "--\n" "\n" "Create a TypeAliasType."); @@ -803,4 +803,4 @@ typealias_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=2e7dd170924d92e5 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=310ab79d0f3a4b5c input=a9049054013a1b77]*/ diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index b2c3c79c93ff195..53c350838969616 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -2145,7 +2145,7 @@ typealias.__new__ as typealias_new name: object(subclass_of="&PyUnicode_Type") value: object * - type_params: object = NULL + type_params: object(c_default="NULL") = () qualname: object(c_default="NULL") = None Create a TypeAliasType. @@ -2154,7 +2154,7 @@ Create a TypeAliasType. static PyObject * typealias_new_impl(PyTypeObject *type, PyObject *name, PyObject *value, PyObject *type_params, PyObject *qualname) -/*[clinic end generated code: output=b7f6d9f1c577cd9c input=cbec290f8c4886ef]*/ +/*[clinic end generated code: output=b7f6d9f1c577cd9c input=6516623275f87b5e]*/ { if (type_params != NULL && !PyTuple_Check(type_params)) { PyErr_SetString(PyExc_TypeError, "type_params must be a tuple"); diff --git a/Python/_warnings.c b/Python/_warnings.c index 33385e86c049ce4..586cec195fac680 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -1156,7 +1156,7 @@ warn as warnings_warn source: object = None If supplied, the destroyed object which emitted a ResourceWarning * - skip_file_prefixes: object(type='PyTupleObject *', subclass_of='&PyTuple_Type') = NULL + skip_file_prefixes: object(type='PyTupleObject *', subclass_of='&PyTuple_Type', c_default='NULL') = () An optional tuple of module filename prefixes indicating frames to skip during stacklevel computations for stack frame attribution. @@ -1167,7 +1167,7 @@ static PyObject * warnings_warn_impl(PyObject *module, PyObject *message, PyObject *category, Py_ssize_t stacklevel, PyObject *source, PyTupleObject *skip_file_prefixes) -/*[clinic end generated code: output=a68e0f6906c65f80 input=eb37c6a18bec4ea1]*/ +/*[clinic end generated code: output=a68e0f6906c65f80 input=2b52e8b20f508f51]*/ { category = get_category(message, category); if (category == NULL) diff --git a/Python/clinic/_warnings.c.h b/Python/clinic/_warnings.c.h index 8bda830ccb924eb..a64418be0a830ba 100644 --- a/Python/clinic/_warnings.c.h +++ b/Python/clinic/_warnings.c.h @@ -45,7 +45,7 @@ warnings_release_lock(PyObject *module, PyObject *Py_UNUSED(ignored)) PyDoc_STRVAR(warnings_warn__doc__, "warn($module, /, message, category=None, stacklevel=1, source=None, *,\n" -" skip_file_prefixes=)\n" +" skip_file_prefixes=())\n" "--\n" "\n" "Issue a warning, or maybe ignore it or raise an exception.\n" @@ -284,4 +284,4 @@ warnings_filters_mutated_lock_held(PyObject *module, PyObject *Py_UNUSED(ignored { return warnings_filters_mutated_lock_held_impl(module); } -/*[clinic end generated code: output=610ed5764bf40bb5 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=fc026d6a21e12170 input=a9049054013a1b77]*/ From 67f4d53425d4f7df559b4a0ba5bfb66e795854c7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 21 Aug 2026 13:50:25 +0300 Subject: [PATCH 11/11] gh-156166: Fix setting and deleting SSLContext._msg_callback (GH-156167) The setter released the old callback before validating the new value, so a failed assignment or a deletion removed it. --- Lib/test/test_ssl.py | 12 ++++++++++ ...-08-21-13-30-00.gh-issue-156166.Xv8pQm.rst | 3 +++ Modules/_ssl/debughelpers.c | 22 +++++++++++++------ 3 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-21-13-30-00.gh-issue-156166.Xv8pQm.rst diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 14e4620669491f2..285bcac65ab57b3 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -5598,6 +5598,18 @@ def msg_cb(conn, direction, version, content_type, msg_type, data): with self.assertRaises(TypeError): client_context._msg_callback = object() + # the attribute of the underlying C type accepts only a callable + # and cannot be deleted + descr = _ssl._SSLContext.__dict__['_msg_callback'] + with self.assertRaises(TypeError): + descr.__set__(client_context, object()) + # a failed assignment does not change the value + self.assertIs(client_context._msg_callback, msg_cb) + with self.assertRaisesRegex(AttributeError, 'cannot be deleted'): + descr.__delete__(client_context) + # a failed deletion does not change the value + self.assertIs(client_context._msg_callback, msg_cb) + def test_msg_callback_exception(self): client_context, server_context, hostname = testing_context() diff --git a/Misc/NEWS.d/next/Library/2026-08-21-13-30-00.gh-issue-156166.Xv8pQm.rst b/Misc/NEWS.d/next/Library/2026-08-21-13-30-00.gh-issue-156166.Xv8pQm.rst new file mode 100644 index 000000000000000..2417479d333e3e6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-21-13-30-00.gh-issue-156166.Xv8pQm.rst @@ -0,0 +1,3 @@ +:mod:`ssl`: A failed assignment or deletion of the ``_msg_callback`` +attribute of :class:`ssl.SSLContext` no longer removes the current callback. +Deleting it now raises :exc:`AttributeError` instead of :exc:`TypeError`. diff --git a/Modules/_ssl/debughelpers.c b/Modules/_ssl/debughelpers.c index b2d552f97e5b0e1..e8da76907971ed1 100644 --- a/Modules/_ssl/debughelpers.c +++ b/Modules/_ssl/debughelpers.c @@ -102,20 +102,28 @@ _PySSLContext_set_msg_callback(PyObject *op, PyObject *arg, void *Py_UNUSED(closure)) { PySSLContext *self = PySSLContext_CAST(op); - Py_CLEAR(self->msg_cb); + if (arg == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_msg_callback' of '%.100s' objects " + "cannot be deleted", Py_TYPE(op)->tp_name); + return -1; + } + if (arg != Py_None && !PyCallable_Check(arg)) { + PyErr_SetString(PyExc_TypeError, + "not a callable object"); + return -1; + } + /* Releasing the old callback can run arbitrary code. */ + PyObject *old_cb = self->msg_cb; if (arg == Py_None) { + self->msg_cb = NULL; SSL_CTX_set_msg_callback(self->ctx, NULL); } else { - if (!PyCallable_Check(arg)) { - SSL_CTX_set_msg_callback(self->ctx, NULL); - PyErr_SetString(PyExc_TypeError, - "not a callable object"); - return -1; - } self->msg_cb = Py_NewRef(arg); SSL_CTX_set_msg_callback(self->ctx, _PySSL_msg_callback); } + Py_XDECREF(old_cb); return 0; }