From ec49d012619d22f2953204ca0736bb8dd1fd18bc Mon Sep 17 00:00:00 2001 From: stratakis <14812606+stratakis@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:12:25 +0200 Subject: [PATCH 1/6] gh-158294: Avoid relying on SystemTap probe handler ordering (#158346) --- Lib/test/dtracedata/call_stack.stp | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/Lib/test/dtracedata/call_stack.stp b/Lib/test/dtracedata/call_stack.stp index d4455fc8489af20..2901f44654be379 100644 --- a/Lib/test/dtracedata/call_stack.stp +++ b/Lib/test/dtracedata/call_stack.stp @@ -10,15 +10,6 @@ function basename:string(path:string) return last_token; } -probe @PYTHON_SYSTEMTAP_PROBE@("function__entry") -{ - funcname = user_string($arg2); - - if (funcname == "start") { - tracing = 1; - } -} - probe @PYTHON_SYSTEMTAP_PROBE@("function__entry"), @PYTHON_SYSTEMTAP_PROBE@("function__return") { @@ -26,17 +17,14 @@ probe @PYTHON_SYSTEMTAP_PROBE@("function__entry"), funcname = user_string($arg2); lineno = $arg3; - if (tracing) { + if (funcname == "start") { + if ($$name == "function__entry") { + tracing = 1; + } else { + tracing = 0; + } + } else if (tracing) { printf("%d\t%s:%s:%s:%d\n", gettimeofday_us(), $$name, basename(filename), funcname, lineno); } } - -probe @PYTHON_SYSTEMTAP_PROBE@("function__return") -{ - funcname = user_string($arg2); - - if (funcname == "start") { - tracing = 0; - } -} From 1407bb9ebc675619cb479415c573508ca773ef2f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:23:24 +0300 Subject: [PATCH 2/6] Run Dependabot independently on each branch (#158361) Co-authored-by: Ezio Melotti --- .github/dependabot.yml | 122 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dca3e12ec18270c..5f9e3c323e30b82 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,12 +16,128 @@ updates: # Cooldowns protect against supply chain attacks by avoiding the # highest-risk window immediately after new releases. default-days: 14 + - package-ecosystem: "pip" directory: "/Tools/" schedule: interval: "quarterly" - labels: - - "skip issue" - - "skip news" + labels: ["skip issue", "skip news"] + groups: + pip: + patterns: ["*"] + cooldown: + default-days: 14 + + # Release branches: Dependabot only reads this file from the default + # branch, so each branch that should get its own actions bumps needs an + # entry here with `target-branch`. Add one when a new release branch is + # created, and remove when the branch reaches end-of-life. + - package-ecosystem: "github-actions" + target-branch: "3.15" + directory: "/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + actions: + patterns: ["*"] + cooldown: + default-days: 14 + + - package-ecosystem: "github-actions" + target-branch: "3.14" + directory: "/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + actions: + patterns: ["*"] + cooldown: + default-days: 14 + + - package-ecosystem: "github-actions" + target-branch: "3.13" + directory: "/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + actions: + patterns: ["*"] + cooldown: + default-days: 14 + + - package-ecosystem: "github-actions" + target-branch: "3.12" + directory: "/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + actions: + patterns: ["*"] + cooldown: + default-days: 14 + + - package-ecosystem: "github-actions" + target-branch: "3.11" + directory: "/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + actions: + patterns: ["*"] + cooldown: + default-days: 14 + + - package-ecosystem: "github-actions" + target-branch: "3.10" + directory: "/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + actions: + patterns: ["*"] + cooldown: + default-days: 14 + + # Only bump bugfix branches for pip. Remove + # the entry when branch goes security-only. + - package-ecosystem: "pip" + target-branch: "3.15" + directory: "/Tools/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + pip: + patterns: ["*"] + cooldown: + default-days: 14 + + - package-ecosystem: "pip" + target-branch: "3.14" + directory: "/Tools/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + pip: + patterns: ["*"] + cooldown: + default-days: 14 + + - package-ecosystem: "pip" + target-branch: "3.13" + directory: "/Tools/" + schedule: + interval: "quarterly" + labels: ["skip issue", "skip news"] + groups: + pip: + patterns: ["*"] cooldown: default-days: 14 From cac016736e5742395fff20b73692876f05ae312c Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Tue, 29 Sep 2026 05:30:57 -0400 Subject: [PATCH 3/6] gh-153888: Resolve lazy imports before using them as a mock spec (GH-157768) Resolve lazy imports before using them as a mock spec unittest.mock.patch() reads the attribute it replaces out of target.__dict__, which for an unresolved lazy import holds a types.LazyImportType placeholder. spec=True, spec_set=True and autospec=True then specced the placeholder, so patching a function produced a NonCallableMagicMock. Reimplementation of python/cpython#153977: - Resolve in _patch.__enter__(), and only when the spec is taken from the original, rather than unconditionally in get_original(). Resolving on every patch() breaks patch("mod.dep", new=...) for a lazy import of a module that is not installed. - Test spec and spec_set as well as autospec, as asked for in review. - Build the lazy binding in the test instead of importing json lazily at module level: "lazy from" binds eagerly when the module is already imported, so that test passed without the fix too. --- Lib/test/test_unittest/testmock/testpatch.py | 35 +++++++++++++++++++ Lib/unittest/mock.py | 6 +++- ...-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst | 2 ++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst diff --git a/Lib/test/test_unittest/testmock/testpatch.py b/Lib/test/test_unittest/testmock/testpatch.py index bd85fdcfc472a61..bd2a7fb6d6731d7 100644 --- a/Lib/test/test_unittest/testmock/testpatch.py +++ b/Lib/test/test_unittest/testmock/testpatch.py @@ -5,6 +5,7 @@ import os import sys from collections import OrderedDict +from types import LazyImportType, ModuleType import unittest import test @@ -2101,5 +2102,39 @@ def test(_): test() +class PatchLazyImportTest(unittest.TestCase): + + def lazy_module(self): + # `lazy from` binds eagerly when the module it imports from is already + # imported, so publish the target only once the statement has run. + self.enterContext(uncache('lazy_patch_user', 'lazy_patch_target')) + user = ModuleType('lazy_patch_user') + exec('lazy from lazy_patch_target import function', user.__dict__) + sys.modules['lazy_patch_user'] = user + target = ModuleType('lazy_patch_target') + exec('def function(arg): pass', target.__dict__) + sys.modules['lazy_patch_target'] = target + + self.assertIsInstance(user.__dict__['function'], LazyImportType) + return user + + def test_autospec(self): + module = self.lazy_module() + with patch.object(module, 'function', autospec=True) as mock_function: + mock_function('arg') + with self.assertRaises(TypeError): + mock_function('arg', 'extra') + + def test_spec(self): + module = self.lazy_module() + with patch.object(module, 'function', spec=True) as mock_function: + mock_function('arg') + + def test_spec_set(self): + module = self.lazy_module() + with patch.object(module, 'function', spec_set=True) as mock_function: + mock_function('arg') + + if __name__ == '__main__': unittest.main() diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 1effc70b5323139..d3d47ff81e85969 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -35,7 +35,7 @@ import threading from annotationlib import Format from dataclasses import fields, is_dataclass -from types import CodeType, ModuleType, MethodType +from types import CodeType, LazyImportType, ModuleType, MethodType from unittest.util import safe_repr from functools import wraps, partial from threading import RLock @@ -1507,6 +1507,10 @@ def __enter__(self): original, local = self.get_original() + if (isinstance(original, LazyImportType) + and (spec is True or spec_set is True or autospec is True)): + original = original.resolve() + if new is DEFAULT and autospec is None: inherit = False if spec is True: diff --git a/Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst b/Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst new file mode 100644 index 000000000000000..278c9560cec216e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst @@ -0,0 +1,2 @@ +Fix :func:`unittest.mock.patch` with ``autospec``, ``spec``, or ``spec_set`` +when used with lazy-imported objects. From 969af80daf09fcd2ce8f358e5f9d54e75f8ef330 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 29 Sep 2026 11:31:54 +0200 Subject: [PATCH 4/6] gh-158219: Fix bytearray constructor to not use hashed object (#158329) * Add _PyBytes_GET_CACHED_HASH() static inline function. * _PyBytes_IsMutable() makes sure that the hash value is not cached yet. * bytearray_reinit_from_bytes() checks that the bytes object is mutable. Co-authored-by: Cody Maloney --- Include/internal/pycore_bytesobject.h | 16 ++++++++++ Lib/test/test_bytes.py | 30 +++++++++++++++++-- ...-09-28-11-52-49.gh-issue-158219.tNjBVV.rst | 2 ++ Objects/bytearrayobject.c | 12 ++++++-- Objects/bytesobject.c | 19 +++++------- 5 files changed, 61 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-11-52-49.gh-issue-158219.tNjBVV.rst diff --git a/Include/internal/pycore_bytesobject.h b/Include/internal/pycore_bytesobject.h index 8f764f0fa6d6e12..e709940ad984316 100644 --- a/Include/internal/pycore_bytesobject.h +++ b/Include/internal/pycore_bytesobject.h @@ -88,6 +88,22 @@ extern void _PyBytes_CheckOverflow( const char *type_name); #endif + +// Return the cached hash value, or -1 if not cached yet. +static inline Py_hash_t +_PyBytes_GET_CACHED_HASH(PyBytesObject *self) +{ +_Py_COMP_DIAG_PUSH +_Py_COMP_DIAG_IGNORE_DEPR_DECLS +#ifdef Py_GIL_DISABLED + return _Py_atomic_load_ssize_relaxed(&self->ob_shash); +#else + return self->ob_shash; +#endif +_Py_COMP_DIAG_POP +} + + /* --- PyBytesWriter ------------------------------------------------------ */ struct PyBytesWriter { diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 419bee5583de47b..ad5091b54d79119 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -5,14 +5,15 @@ """ import array +import codecs import contextlib +import copy +import functools import operator import os +import pickle import re import sys -import copy -import functools -import pickle import tempfile import textwrap import threading @@ -1706,6 +1707,29 @@ def test_take_bytes_optimization(self): bytes_header_size = sys.getsizeof(b'') self.assertEqual(ba.__alloc__(), 499 + bytes_header_size) + def test_take_bytes_hash(self): + # gh-158219: bytearray constructor must not use a bytes object + # if its hash value is already cached. + + def encode(string, errors='strict'): + encoded = string.encode('utf-8') + hash(encoded) # a codec may hash its own output + return encoded, len(string) + + def hashing_codec(name): + if name != 'test_take_bytes_hash': + return None + return codecs.CodecInfo(encode, None, name=name) + + codecs.register(hashing_codec) + self.addCleanup(codecs.unregister, hashing_codec) + + ba = bytearray('hello', 'test_take_bytes_hash') + ba[0] = ord('H') + taken = ba.take_bytes() + self.assertEqual(taken, b'Hello') + self.assertEqual(hash(taken), hash(b'Hello')) + def test_take_bytes_reentrant_resize(self): # gh-153570: n.__index__() can resize the bytearray, so take_bytes() # must re-read the size afterwards. It cached the size before the diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-11-52-49.gh-issue-158219.tNjBVV.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-11-52-49.gh-issue-158219.tNjBVV.rst new file mode 100644 index 000000000000000..691a588e03c2b2f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-11-52-49.gh-issue-158219.tNjBVV.rst @@ -0,0 +1,2 @@ +Fix :class:`bytearray` constructor: do not use a bytes object if its hash +value is already computed. Patch by Cody Maloney and Victor Stinner. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 16c384035478185..caf8bad88a280d3 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -58,8 +58,13 @@ bytearray_reinit_from_bytes(PyByteArrayObject *self, Py_ssize_t size) Py_ssize_t alloc = PyBytes_GET_SIZE(self->ob_bytes_object); assert(0 <= size && size <= alloc); - /* Only the empty bytes may be immortal. */ - assert((alloc == 0) == _Py_IsImmortal(self->ob_bytes_object)); + if (alloc != 0) { + assert(_PyBytes_IsMutable(self->ob_bytes_object)); + } + else { + // Use the empty bytes string singleton for an empty bytearray + assert(_Py_IsImmortal(self->ob_bytes_object)); + } self->ob_bytes = self->ob_start = PyBytes_AS_STRING(self->ob_bytes_object); Py_SET_SIZE(self, size); @@ -1030,7 +1035,8 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg, /* Most encodes return a new unique bytes, just use it as buffer. */ if (_PyObject_IsUniquelyReferenced(encoded) - && PyBytes_CheckExact(encoded)) + && PyBytes_CheckExact(encoded) + && _PyBytes_GET_CACHED_HASH((PyBytesObject*)encoded) == -1) { Py_ssize_t size = PyBytes_GET_SIZE(encoded); self->ob_bytes_object = encoded; diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 91cbfa23e30b208..683306fe724a5b1 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -63,18 +63,7 @@ _Py_COMP_DIAG_IGNORE_DEPR_DECLS _Py_COMP_DIAG_POP } -static inline Py_hash_t -get_ob_shash(PyBytesObject *a) -{ -_Py_COMP_DIAG_PUSH -_Py_COMP_DIAG_IGNORE_DEPR_DECLS -#ifdef Py_GIL_DISABLED - return _Py_atomic_load_ssize_relaxed(&a->ob_shash); -#else - return a->ob_shash; -#endif -_Py_COMP_DIAG_POP -} +#define get_ob_shash(op) _PyBytes_GET_CACHED_HASH(op) /* @@ -3346,6 +3335,12 @@ _PyBytes_IsMutable(PyObject *self) unsigned char ch = PyBytes_AS_STRING(self)[0]; assert(self != (PyObject*)CHARACTER(ch)); } + + // gh-158219: The hash value must not be cached yet. Otherwise, it means + // that the bytes object was already used in Python somehow (ex: as a + // dictionary key). + assert(get_ob_shash((PyBytesObject *)self) == -1); + return 1; } #endif From b4be3a5804cbbecb9e1d2984241b455bf15065a8 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 29 Sep 2026 12:17:17 +0200 Subject: [PATCH 5/6] gh-158334: Fix bytes.translate() when the input is returned unchanged (#158344) Do no read output_start pointer after PyBytesWriter_Finish() since the pointer became a dangling pointer. Co-authored-by: Vyron Vasileiadis --- Lib/test/test_bytes.py | 28 ++++++++++++++++++++++++++++ Objects/bytesobject.c | 14 +++++++++----- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index ad5091b54d79119..b55863256cc37c3 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1147,6 +1147,34 @@ def test_translate(self): c = b.translate(None, delete=b'e') self.assertEqual(c, b'hllo') + # short inputs starting with NUL bytes + table = bytes.maketrans(b'\x00', b'Z') + for data in b'\x00', b'\x00' * 8, b'\x00' * 8 + b'a' * 247: + c = self.type2test(data).translate(table) + self.assertEqual(c, data.replace(b'\x00', b'Z')) + + @support.cpython_only + def test_translate_unchanged(self): + if self.type2test != bytes: + self.skipTest("test specific bytes.translate()") + + # bytes.translate() returns the input string unchanged + # if no byte is modified + size = 1024 + b = b'hell' + b'o' * size + rosetta = bytearray(range(256)) + rosetta[ord('#')] = ord('?') + self.assertIs(b.translate(rosetta), b) + + # bytes.translate() always create a new object + # if the input string is a bytes subclass + class bytes_subclass(bytes): + pass + b = bytes_subclass(b) + result = b.translate(rosetta) + self.assertIsNot(result, b) + self.assertEqual(result, b) + def test_sq_item(self): _testlimitedcapi = import_helper.import_module('_testlimitedcapi') obj = self.type2test((42,)) diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 683306fe724a5b1..fcda380dbeb7624 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -2324,15 +2324,19 @@ bytes_translate_impl(PyBytesObject *self, PyObject *table, c = Py_CHARMASK(*input++); *output++ = table_chars[c]; } - PyObject *result = PyBytesWriter_Finish(writer); /* Check if anything changed (for returning original object) */ /* We save this check until the end so that the compiler will */ /* unroll the loop above leading to MUCH faster code. */ - if (result != NULL && PyBytes_CheckExact(input_obj)) { - if (memcmp(PyBytes_AS_STRING(input_obj), output_start, inlen) == 0) { - Py_SETREF(result, Py_NewRef(input_obj)); - } + PyObject *result; + if (PyBytes_CheckExact(input_obj) + && memcmp(PyBytes_AS_STRING(input_obj), output_start, inlen) == 0) + { + PyBytesWriter_Discard(writer); + result = Py_NewRef(input_obj); + } + else { + result = PyBytesWriter_Finish(writer); } PyBuffer_Release(&del_table_view); From aa287d7f443c2e219b43f7b804b209971964d206 Mon Sep 17 00:00:00 2001 From: Pengyu Lee Date: Tue, 29 Sep 2026 18:48:11 +0800 Subject: [PATCH 6/6] gh-158241: Prevent double release of assembler byte writers (#158242) Co-authored-by: Victor Stinner --- Lib/test/test_compile.py | 20 +++++++++++++++++++ ...-09-26-23-44-44.gh-issue-158241.NZ9OSU.rst | 2 ++ Python/assemble.c | 3 +++ 3 files changed, 25 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-23-44-44.gh-issue-158241.NZ9OSU.rst diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 553ac70d83a802c..8b6a07be477dcc5 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -39,6 +39,26 @@ def test_no_ending_newline(self): def test_empty(self): compile("", "", "exec") + @support.requires_subprocess() + @support.nomemtest + def test_assemble_init_allocation_failure(self): + # gh-158241: Check error handling on MemoryError in Python/assemble.c + code = textwrap.dedent(""" + from test import support + + failures = 0 + for n in range(1, 100): + with support.inject_memory_error_cm(n, n + 1): + try: + compile("x", "", "exec") + except MemoryError: + failures += 1 + + if failures == 0: + raise AssertionError + """) + script_helper.assert_python_ok('-c', code) + def test_other_newlines(self): compile("\r\n", "", "exec") compile("\r", "", "exec") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-23-44-44.gh-issue-158241.NZ9OSU.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-23-44-44.gh-issue-158241.NZ9OSU.rst new file mode 100644 index 000000000000000..fd64c9b695ff2f9 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-26-23-44-44.gh-issue-158241.NZ9OSU.rst @@ -0,0 +1,2 @@ +Fix a crash and possible code object corruption following a +:exc:`MemoryError` while compiling Python code. diff --git a/Python/assemble.c b/Python/assemble.c index db9efff5e08ca96..2c8abb116b2e336 100644 --- a/Python/assemble.c +++ b/Python/assemble.c @@ -81,8 +81,11 @@ assemble_init(struct assembler *a, int firstlineno) return SUCCESS; error: PyBytesWriter_Discard(a->a_bytecode_writer); + a->a_bytecode_writer = NULL; PyBytesWriter_Discard(a->a_linetable_writer); + a->a_linetable_writer = NULL; PyBytesWriter_Discard(a->a_except_table_writer); + a->a_except_table_writer = NULL; return ERROR; }