From 22a6c51c94a4fde986b8964f1d36d5ec3ac20dcc Mon Sep 17 00:00:00 2001 From: Wenzel Jakob Date: Thu, 30 Jul 2026 14:00:08 +0200 Subject: [PATCH 1/6] gh-151728: Clear the typing caches at interpreter shutdown (GH-154858) The typing module caches every subscripted type. When an extension module leaks a reference to typing, these caches also keep types owned by other (correct) extension modules alive past interpreter shutdown, where nothing can free them anymore. The cache_clear callables are already collected in typing._cleanups, so registering them with atexit is enough to avoid this. --- Lib/test/libregrtest/utils.py | 3 +-- Lib/typing.py | 11 +++++++++++ .../2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst | 4 ++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py index 32f02429ff33076..892d204700179f6 100644 --- a/Lib/test/libregrtest/utils.py +++ b/Lib/test/libregrtest/utils.py @@ -272,8 +272,7 @@ def clear_caches(): except KeyError: pass else: - for f in typing._cleanups: - f() + typing._clear_caches() import inspect abs_classes = filter(inspect.isabstract, typing.__dict__.values()) diff --git a/Lib/typing.py b/Lib/typing.py index 054420865d7fb50..809c0ff88607a59 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -19,6 +19,7 @@ """ from abc import abstractmethod, ABCMeta +import atexit import collections from collections import defaultdict import collections.abc @@ -392,6 +393,16 @@ def _flatten_literal_params(parameters): _caches = {} +def _clear_caches(): + for cleanup in _cleanups: + cleanup() + + +# Release the LRU caches at shutdown, they otherwise redistribute reference +# leaks of one extension to types of unrelated ones. See GH-151728. +atexit.register(_clear_caches) + + def _tp_cache(func=None, /, *, typed=False): """Internal wrapper caching __getitem__ of generic types. diff --git a/Misc/NEWS.d/next/Library/2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst b/Misc/NEWS.d/next/Library/2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst new file mode 100644 index 000000000000000..39c63a1aea3193a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst @@ -0,0 +1,4 @@ +Clear the internal :mod:`typing` caches from an exit handler. Previously, an +extension module that leaked a reference to :mod:`typing` would also keep every +subscripted type alive past interpreter shutdown, including types owned by +unrelated extension modules. From b3be16db02e71368774aab62c8ce3f6fb8cc5452 Mon Sep 17 00:00:00 2001 From: Aniket <148300120+Aniketsy@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:18:38 +0530 Subject: [PATCH 2/6] gh-151941: Fix Sphinx reference warnings in `Doc/c-api/` (GH-152044) --- Doc/c-api/exceptions.rst | 15 +++++++++++++++ Doc/c-api/init_config.rst | 9 ++++----- Doc/c-api/intro.rst | 2 +- Doc/tools/.nitignore | 3 --- Tools/check-c-api-docs/ignored_c_api.txt | 2 -- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index d04074f6e6a7993..40522f8c7b13756 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -119,6 +119,21 @@ Printing and clearing .. versionadded:: 3.12 +.. c:function:: void PyErr_Display(PyObject *unused, PyObject *value, PyObject *tb) + + Legacy variant of :c:func:`PyErr_DisplayException`. + + Print the exception *value* with its traceback to :data:`sys.stderr`. + If *value* has no traceback set, *tb* is used as its traceback. + The first argument is ignored. + + If :data:`sys.stderr` is ``None``, nothing is printed. + If :data:`sys.stderr` is not set, the exception is dumped to the + C ``stderr`` stream instead. + + .. deprecated:: 3.12 + Use :c:func:`PyErr_DisplayException` instead. + Raising exceptions ================== diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index d6b9837987a3999..ef09639189a6c27 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -1235,9 +1235,9 @@ PyConfig .. c:member:: wchar_t* base_executable - Python base executable: :data:`sys._base_executable`. + Python base executable: ``sys._base_executable``. - Set by the :envvar:`__PYVENV_LAUNCHER__` environment variable. + Set by the ``__PYVENV_LAUNCHER__`` environment variable. Set from :c:member:`PyConfig.executable` if ``NULL``. @@ -1748,7 +1748,7 @@ PyConfig * On macOS, use :envvar:`PYTHONEXECUTABLE` environment variable if set. * If the ``WITH_NEXT_FRAMEWORK`` macro is defined, use - :envvar:`__PYVENV_LAUNCHER__` environment variable if set. + ``__PYVENV_LAUNCHER__`` environment variable if set. * Use ``argv[0]`` of :c:member:`~PyConfig.argv` if available and non-empty. * Otherwise, use ``L"python"`` on Windows, or ``L"python3"`` on other @@ -1984,8 +1984,7 @@ PyConfig The :mod:`warnings` module adds :data:`sys.warnoptions` in the reverse order: the last :c:member:`PyConfig.warnoptions` item becomes the first - item of :data:`warnings.filters` which is checked first (highest - priority). + item of ``warnings.filters`` which is checked first (highest priority). The :option:`-W` command line options adds its value to :c:member:`~PyConfig.warnoptions`, it can be used multiple times. diff --git a/Doc/c-api/intro.rst b/Doc/c-api/intro.rst index 4c0c9af45e8360d..701d2a31b8c9059 100644 --- a/Doc/c-api/intro.rst +++ b/Doc/c-api/intro.rst @@ -1163,7 +1163,7 @@ when defined by the compiler, will also implicitly enable :c:macro:`!Py_DEBUG`. In addition to the reference count debugging described below, extra checks are performed. See :ref:`Python Debug Build ` for more details. -Defining :c:macro:`Py_TRACE_REFS` enables reference tracing +Defining ``Py_TRACE_REFS`` enables reference tracing (see the :option:`configure --with-trace-refs option <--with-trace-refs>`). When defined, a circular doubly linked list of active objects is maintained by adding two extra fields to every :c:type:`PyObject`. Total allocations are tracked as well. Upon diff --git a/Doc/tools/.nitignore b/Doc/tools/.nitignore index ab592cfa5a1bbbd..6a4ab9f5aa3bf65 100644 --- a/Doc/tools/.nitignore +++ b/Doc/tools/.nitignore @@ -2,9 +2,6 @@ # as tested on the CI via check-warnings.py in reusable-docs.yml. # Keep lines sorted lexicographically to help avoid merge conflicts. -Doc/c-api/init_config.rst -Doc/c-api/intro.rst -Doc/c-api/stable.rst Doc/library/ast.rst Doc/library/asyncio-extending.rst Doc/library/email.charset.rst diff --git a/Tools/check-c-api-docs/ignored_c_api.txt b/Tools/check-c-api-docs/ignored_c_api.txt index aeae9e6553a3aa6..af7b2772ef5e085 100644 --- a/Tools/check-c-api-docs/ignored_c_api.txt +++ b/Tools/check-c-api-docs/ignored_c_api.txt @@ -37,8 +37,6 @@ PyWrapperFlag_KEYWORDS Py_UniversalNewlineFgets # cpython/pylifecycle.h Py_FrozenMain -# pythonrun.h -PyErr_Display # cpython/objimpl.h PyObject_GET_WEAKREFS_LISTPTR # cpython/pythonrun.h From 0e54a406b45d3858c31f61d5e613f5c97ecfa355 Mon Sep 17 00:00:00 2001 From: Bhuvansh Date: Thu, 30 Jul 2026 19:45:05 +0530 Subject: [PATCH 3/6] gh-154587: Fix Windows buffer declaration in _cursesmodule (GH-154609) --- Modules/_cursesmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index b8680edc6c0bed0..132b55c0a500fb6 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1998,7 +1998,7 @@ PyCursesWindow_New(cursesmodule_state *state, { if (encoding == NULL) { #if defined(MS_WINDOWS) - char *buffer[100]; + char buffer[100]; UINT cp; cp = GetConsoleOutputCP(); if (cp != 0) { From 405daf5328a1f9f8355ba76b0174b236364c2646 Mon Sep 17 00:00:00 2001 From: vjabuilds <77214151+vjabuilds@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:44:18 +0200 Subject: [PATCH 4/6] Improved test coverage for codeop.py. (GH-154100) * Ported existing tests to cover both the CommandCompiler and Compile classes * Added tests that cover the case when PyCF_ONLY_AST is set * Added tests that cover the case when the compile method returns a code object with flags Co-authored-by: Tomas R. --- Lib/test/test_codeop.py | 172 +++++++++++++++++++++++++++------------- 1 file changed, 116 insertions(+), 56 deletions(-) diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index ed10bd3dcb6d2b9..d57452602ce5574 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -4,43 +4,64 @@ """ import unittest import warnings -from test.support import warnings_helper +from test.support import subTests, warnings_helper from textwrap import dedent +import functools -from codeop import compile_command, PyCF_DONT_IMPLY_DEDENT +from codeop import compile_command, CommandCompiler, Compile +from codeop import PyCF_DONT_IMPLY_DEDENT, PyCF_ONLY_AST +import ast + + +WRAPPING_COMPILERS = [compile_command, CommandCompiler()] +RAW_COMPILERS = [Compile()] +COMPILERS = WRAPPING_COMPILERS + RAW_COMPILERS -class CodeopTests(unittest.TestCase): - def assertValid(self, str, symbol='single'): +class CodeopTests(unittest.TestCase): + def assertValid(self, str, symbol='single', *, compiler): '''succeed iff str is a valid piece of code''' expected = compile(str, "", symbol, PyCF_DONT_IMPLY_DEDENT) - self.assertEqual(compile_command(str, "", symbol), expected) + self.assertEqual(compiler(str, "", symbol), expected) - def assertIncomplete(self, str, symbol='single'): + def assertIncomplete(self, str, symbol='single', *, compiler): '''succeed iff str is the start of a valid piece of code''' - self.assertEqual(compile_command(str, symbol=symbol), None) - - def assertInvalid(self, str, symbol='single', is_syntax=1): + if compiler in WRAPPING_COMPILERS: + self.assertEqual(compiler(str, "", symbol=symbol), None) + else: + # Compile has should raise like built-in compile + with self.assertRaises(SyntaxError) as cm_original_error: + compile(str, "", symbol, compiler.flags) + expected_error = cm_original_error.exception + with self.assertRaises(type(expected_error)) as cm_wrapped_error: + compiler(str, "", symbol=symbol) + self.assertEqual( + expected_error.args, + cm_wrapped_error.exception.args + ) + + def assertInvalid(self, str, symbol='single', is_syntax=1, *, compiler): '''succeed iff str is the start of an invalid piece of code''' try: - compile_command(str,symbol=symbol) + compiler(str,"", symbol=symbol) self.fail("No exception raised for invalid code") except SyntaxError: self.assertTrue(is_syntax) except OverflowError: self.assertTrue(not is_syntax) - def test_valid(self): - av = self.assertValid - - # special case - self.assertEqual(compile_command(""), - compile("pass", "", 'single', - PyCF_DONT_IMPLY_DEDENT)) - self.assertEqual(compile_command("\n"), - compile("pass", "", 'single', - PyCF_DONT_IMPLY_DEDENT)) - + @subTests('compiler', WRAPPING_COMPILERS) + def test_empty(self, compiler): + self.assertEqual( + compiler("", "", 'single'), + compile("pass", "", 'single', PyCF_DONT_IMPLY_DEDENT)) + self.assertEqual( + compiler("\n", "", 'single'), + compile("pass", "", 'single', PyCF_DONT_IMPLY_DEDENT)) + + @subTests('compiler', COMPILERS) + def test_valid(self, compiler): + av = functools.partial(self.assertValid, compiler=compiler) av("a = 1") av("\na = 1") av("a = 1\n") @@ -92,8 +113,9 @@ def test_valid(self): av("def f():\n pass\n#foo\n") av("@a.b.c\ndef f():\n pass\n") - def test_incomplete(self): - ai = self.assertIncomplete + @subTests('compiler', COMPILERS) + def test_incomplete(self, compiler): + ai = functools.partial(self.assertIncomplete, compiler=compiler) ai("(a **") ai("(a,b,") @@ -226,8 +248,9 @@ def test_incomplete(self): ai('a = f"""') ai('a = \\') - def test_invalid(self): - ai = self.assertInvalid + @subTests('compiler', COMPILERS) + def test_invalid(self, compiler): + ai = functools.partial(self.assertInvalid, compiler=compiler) ai("a b") ai("a @") @@ -263,8 +286,9 @@ def test_invalid(self): ai("[i for i in range(10)] = (1, 2, 3)") - def test_invalid_exec(self): - ai = self.assertInvalid + @subTests('compiler', COMPILERS) + def test_invalid_exec(self, compiler): + ai = functools.partial(self.assertInvalid, compiler=compiler) ai("raise = 4", symbol="exec") ai('def a-b', symbol='exec') ai('await?', symbol='exec') @@ -272,58 +296,94 @@ def test_invalid_exec(self): ai('a await raise b', symbol='exec') ai('a await raise b?+1', symbol='exec') - def test_filename(self): - self.assertEqual(compile_command("a = 1\n", "abc").co_filename, - compile("a = 1\n", "abc", 'single').co_filename) - self.assertNotEqual(compile_command("a = 1\n", "abc").co_filename, - compile("a = 1\n", "def", 'single').co_filename) - - def test_warning(self): + @subTests('compiler', COMPILERS) + def test_filename(self, compiler): + self.assertEqual( + compiler("a = 1\n", "abc", "single").co_filename, + compile("a = 1\n", "abc", 'single').co_filename + ) + self.assertNotEqual( + compiler("a = 1\n", "abc", "single").co_filename, + compile("a = 1\n", "def", 'single').co_filename + ) + + def assertReturnsModule(self, code, compiler): + retval = compiler(code, "", 'exec', PyCF_ONLY_AST) + self.assertIsInstance(retval, ast.Module) + + @subTests('compiler', RAW_COMPILERS) + def test_ast_return_value(self, compiler): + validate_ast = self.assertReturnsModule + validate_ast("x = 5", compiler) + validate_ast("\nx = 5", compiler) + validate_ast("x = 5\n", compiler) + validate_ast("x = 5\n\n", compiler) + validate_ast("\n\nx = 5\n\n", compiler) + + @subTests('compiler', COMPILERS) + def test_warning(self, compiler): # Test that the warning is only returned once. with warnings_helper.check_warnings( ('"is" with \'str\' literal', SyntaxWarning), ('"\\\\e" is an invalid escape sequence', SyntaxWarning), ) as w: - compile_command(r"'\e' is 0") - self.assertEqual(len(w.warnings), 2) + compiler(r"'\e' is 0", "", "single") + self.assertEqual(len(w.warnings), 2) # bpo-41520: check SyntaxWarning treated as an SyntaxError with warnings.catch_warnings(), self.assertRaises(SyntaxError): warnings.simplefilter('error', SyntaxWarning) - compile_command('1 is 1', symbol='exec') + compiler('1 is 1', "", 'exec') # Check SyntaxWarning treated as an SyntaxError with warnings.catch_warnings(), self.assertRaises(SyntaxError): warnings.simplefilter('error', SyntaxWarning) - compile_command(r"'\e'", symbol='exec') + compiler(r"'\e'", "", 'exec') - def test_incomplete_warning(self): + @subTests('compiler', WRAPPING_COMPILERS) + def test_incomplete_warning(self, compiler): with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - self.assertIncomplete("'\\e' + (") + compiler("'\\e' + (") self.assertEqual(w, []) - def test_invalid_warning(self): + @subTests('compiler', RAW_COMPILERS) + def test_raw_raises_error(self, compiler): + warnings_cm = warnings_helper.check_warnings( + ('"\\\\e" is an invalid esceape sequence', SyntaxWarning) + ) + with self.assertRaises(SyntaxError), warnings_cm as w: + compiler("'\\e' + (", "", 'single') + self.assertEqual(len(w.warnings), 1) + + @subTests('compiler', COMPILERS) + def test_invalid_warning(self, compiler): with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - self.assertInvalid("'\\e' 1") + self.assertInvalid("'\\e' 1", compiler=compiler) self.assertEqual(len(w), 1) - self.assertEqual(w[0].category, SyntaxWarning) - self.assertRegex(str(w[0].message), 'invalid escape sequence') - self.assertEqual(w[0].filename, '') - - def assertSyntaxErrorMatches(self, code, message): - with self.subTest(code): - with self.assertRaisesRegex(SyntaxError, message): - compile_command(code, symbol='exec') - - def test_syntax_errors(self): - self.assertSyntaxErrorMatches( - dedent("""\ + for warning in w: + self.assertEqual(warning.category, SyntaxWarning) + self.assertRegex(str(warning), 'invalid escape sequence') + self.assertEqual(warning.filename, '') + + @subTests('compiler', COMPILERS) + def test_syntax_errors(self, compiler): + code = dedent("""\ def foo(x,x): pass - """), "duplicate parameter 'x' in function definition") - + """) + message = "duplicate parameter 'x' in function definition" + with self.assertRaisesRegex(SyntaxError, message): + compiler(code, "", 'exec') + + @subTests('compiler', RAW_COMPILERS) + def test_future_imports(self, compiler): + original_flags = compiler.flags + compiler('from __future__ import annotations', "", 'single') + self.assertGreater(compiler.flags, original_flags) + # reset flags to ensure test has no side-effects + compiler.flags = original_flags if __name__ == "__main__": From 2f381f5a90474d9dad12e1e4946f348dd4b513bc Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 30 Jul 2026 18:02:59 +0300 Subject: [PATCH 5/6] gh-154037: Fix race in the hash(Decimal) (#154045) --- Modules/_decimal/_decimal.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 6fbce0ed85e695d..ac3ffb23248f342 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -5935,11 +5935,13 @@ static Py_hash_t dec_hash(PyObject *op) { PyDecObject *self = _PyDecObject_CAST(op); - if (self->hash == -1) { - self->hash = _dec_hash(self); - } + Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->hash); - return self->hash; + if (hash == -1) { + hash = _dec_hash(self); + FT_ATOMIC_STORE_SSIZE_RELAXED(self->hash, hash); + } + return hash; } /*[clinic input] From a147366eb74ca3afd5953069d209de7ea6b5da0f Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Thu, 30 Jul 2026 08:59:59 -0700 Subject: [PATCH 6/6] GH-148468: Accept string-like proxy objects in colorized argparse help (#154653) Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/argparse.py | 2 +- Lib/test/test_argparse.py | 35 +++++++++++++++++++ ...-07-24-21-49-45.gh-issue-148468.pHoF_Q.rst | 1 + 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-24-21-49-45.gh-issue-148468.pHoF_Q.rst diff --git a/Lib/argparse.py b/Lib/argparse.py index f8739d031e01c58..0840c4c45cffaa2 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -713,7 +713,7 @@ def _format_args(self, action, default_metavar): return result def _expand_help(self, action): - help_string = self._get_help_string(action) + help_string = str(self._get_help_string(action)) if '%' not in help_string: return self._apply_text_markup(help_string) params = dict(vars(action), prog=self._prog) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 442cbb1aaadfe3f..287fcb7a084a499 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7898,6 +7898,41 @@ def test_backtick_markup_in_argument_help_color_disabled(self): self.assertIn("set the `foo` value", help_text) self.assertNotIn("\x1b[", help_text) + def test_argument_help_interpolation_accepts_string_like_proxy(self): + class LazyStr: + def __init__(self, message): + self._message = message + + def __str__(self): + return self._message + + def __getattr__(self, name): + return getattr(str(self), name) + + def __contains__(self, item): + return item in self._message + + def __mod__(self, other): + return self._message % other + + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument( + "--foo", + default="bar", + help=LazyStr("foo (default: %(default)s)"), + ) + parser.add_argument( + "--baz", + help=LazyStr("baz plain text"), + ) + + interp = self.theme.interpolated_value + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f"foo (default: {interp}bar{reset})", help_text) + self.assertIn("baz plain text", help_text) + def test_help_with_format_specifiers(self): # GH-142950: format specifiers like %x should work with color=True parser = argparse.ArgumentParser(prog='PROG', color=True) diff --git a/Misc/NEWS.d/next/Library/2026-07-24-21-49-45.gh-issue-148468.pHoF_Q.rst b/Misc/NEWS.d/next/Library/2026-07-24-21-49-45.gh-issue-148468.pHoF_Q.rst new file mode 100644 index 000000000000000..d1d0deb139bc207 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-24-21-49-45.gh-issue-148468.pHoF_Q.rst @@ -0,0 +1 @@ +Fix a regression in :mod:`argparse` where colorized argument help containing format specifiers did not accept string-like proxy objects.