From 59abdf82073c5a431ec1a3a6f0ba6edfdf7a3e1c Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 1 Jun 2026 21:41:21 +0300 Subject: [PATCH 1/5] gh-115119: Remove superfluous TEST_COVERAGE private macro from _decimal module (GH-149756) It was previously shared with `libmpdec`, which is no longer vendored. --- Modules/_decimal/_decimal.c | 2 +- configure | 6 ------ configure.ac | 5 ----- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 2760792a3fe18ed..4db1b60be77844d 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -188,7 +188,7 @@ find_state_ternary(PyObject *left, PyObject *right, PyObject *modulus) * sizeof(size_t) == sizeof(mpd_uint_t) == sizeof(mpd_ssize_t) */ -#ifdef TEST_COVERAGE +#ifdef Py_DEBUG #undef Py_LOCAL_INLINE #define Py_LOCAL_INLINE Py_LOCAL #endif diff --git a/configure b/configure index 81862372f7d7774..94735c8d018a1f7 100755 --- a/configure +++ b/configure @@ -16593,12 +16593,6 @@ printf "%s\n" "yes" >&6; } have_mpdec=yes fi -# Disable forced inlining in debug builds, see GH-94847 -if test "x$with_pydebug" = xyes -then : - as_fn_append LIBMPDEC_CFLAGS " -DTEST_COVERAGE" -fi - # Check whether _decimal should use a coroutine-local or thread-local context { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for --with-decimal-contextvar" >&5 printf %s "checking for --with-decimal-contextvar... " >&6; } diff --git a/configure.ac b/configure.ac index c8dce3fd49bda16..9192211c7c1871d 100644 --- a/configure.ac +++ b/configure.ac @@ -4398,11 +4398,6 @@ PKG_CHECK_MODULES([LIBMPDEC], [libmpdec >= 2.5.0], [have_mpdec=yes], [ ]) ]) -# Disable forced inlining in debug builds, see GH-94847 -AS_VAR_IF( - [with_pydebug], [yes], - [AS_VAR_APPEND([LIBMPDEC_CFLAGS], [" -DTEST_COVERAGE"])]) - # Check whether _decimal should use a coroutine-local or thread-local context AC_MSG_CHECKING([for --with-decimal-contextvar]) AC_ARG_WITH( From 9ba2a891798a06508f63e216d3a1b6907b39eec4 Mon Sep 17 00:00:00 2001 From: Taeknology <20297177+Taeknology@users.noreply.github.com> Date: Tue, 2 Jun 2026 04:22:42 +0900 Subject: [PATCH 2/5] gh-148672: Document namespace subpackages inside regular packages (#150056) --- Doc/reference/import.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Doc/reference/import.rst b/Doc/reference/import.rst index 83f0ee75e7aebd3..4c8811560de2e3f 100644 --- a/Doc/reference/import.rst +++ b/Doc/reference/import.rst @@ -122,6 +122,12 @@ Importing ``parent.one`` will implicitly execute ``parent/__init__.py`` and ``parent.three`` will execute ``parent/two/__init__.py`` and ``parent/three/__init__.py`` respectively. +A subdirectory inside a regular package that does not contain an +``__init__.py`` file is treated as an implicit +:ref:`namespace package ` (a "namespace +subpackage") rooted in that parent. See :pep:`420` for the underlying +specification. + .. _reference-namespace-package: @@ -153,6 +159,12 @@ physically located next to ``parent/two``. In this case, Python will create a namespace package for the top-level ``parent`` package whenever it or one of its subpackages is imported. +Namespace packages may also be nested inside a regular package. When the +import system searches a regular package's ``__path__`` and encounters a +subdirectory that does not contain an ``__init__.py`` file, that +subdirectory becomes a :term:`portion` contributing to a namespace +subpackage of the enclosing regular package. + See also :pep:`420` for the namespace package specification. From b23646adf1f484d6ee61ea04529b48443508715a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 1 Jun 2026 23:02:40 +0300 Subject: [PATCH 3/5] gh-93417: Improve test_mimetypes (GH-150725) * Separate tests for module-level API and for the MimeTypes class. * Add tests for mimetypes.init() and MimeTypes() with knownfiles and with explicitly passed files. --- Lib/test/mime.types2 | 3 + Lib/test/test_mimetypes.py | 359 +++++++++++++++++++++---------------- 2 files changed, 206 insertions(+), 156 deletions(-) create mode 100644 Lib/test/mime.types2 diff --git a/Lib/test/mime.types2 b/Lib/test/mime.types2 new file mode 100644 index 000000000000000..b05c318d1c05c2b --- /dev/null +++ b/Lib/test/mime.types2 @@ -0,0 +1,3 @@ +# MIME type Extensions +testing/test2 test test2 +testing/test3 test test3 diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py index eccdc5bdf707951..1a3b49b87b121f2 100644 --- a/Lib/test/test_mimetypes.py +++ b/Lib/test/test_mimetypes.py @@ -31,50 +31,11 @@ def tearDownModule(): mimetypes.knownfiles = knownfiles -class MimeTypesTestCase(unittest.TestCase): +class MimeTypesModuleTestCase(unittest.TestCase): def setUp(self): - self.db = mimetypes.MimeTypes() - - def test_case_sensitivity(self): - eq = self.assertEqual - eq(self.db.guess_file_type("foobar.html"), ("text/html", None)) - eq(self.db.guess_type("scheme:foobar.html"), ("text/html", None)) - eq(self.db.guess_file_type("foobar.HTML"), ("text/html", None)) - eq(self.db.guess_type("scheme:foobar.HTML"), ("text/html", None)) - eq(self.db.guess_file_type("foobar.tgz"), ("application/x-tar", "gzip")) - eq(self.db.guess_type("scheme:foobar.tgz"), ("application/x-tar", "gzip")) - eq(self.db.guess_file_type("foobar.TGZ"), ("application/x-tar", "gzip")) - eq(self.db.guess_type("scheme:foobar.TGZ"), ("application/x-tar", "gzip")) - eq(self.db.guess_file_type("foobar.tar.Z"), ("application/x-tar", "compress")) - eq(self.db.guess_type("scheme:foobar.tar.Z"), ("application/x-tar", "compress")) - eq(self.db.guess_file_type("foobar.tar.z"), (None, None)) - eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None)) - - def test_default_data(self): - eq = self.assertEqual - eq(self.db.guess_file_type("foo.html"), ("text/html", None)) - eq(self.db.guess_file_type("foo.HTML"), ("text/html", None)) - eq(self.db.guess_file_type("foo.tgz"), ("application/x-tar", "gzip")) - eq(self.db.guess_file_type("foo.tar.gz"), ("application/x-tar", "gzip")) - eq(self.db.guess_file_type("foo.tar.Z"), ("application/x-tar", "compress")) - eq(self.db.guess_file_type("foo.tar.bz2"), ("application/x-tar", "bzip2")) - eq(self.db.guess_file_type("foo.tar.xz"), ("application/x-tar", "xz")) - - def test_data_urls(self): - eq = self.assertEqual - guess_type = self.db.guess_type - eq(guess_type("data:invalidDataWithoutComma"), (None, None)) - eq(guess_type("data:,thisIsTextPlain"), ("text/plain", None)) - eq(guess_type("data:;base64,thisIsTextPlain"), ("text/plain", None)) - eq(guess_type("data:text/x-foo,thisIsTextXFoo"), ("text/x-foo", None)) - - def test_file_parsing(self): - eq = self.assertEqual - sio = io.StringIO("x-application/x-unittest pyunit\n") - self.db.readfp(sio) - eq(self.db.guess_file_type("foo.pyunit"), - ("x-application/x-unittest", None)) - eq(self.db.guess_extension("x-application/x-unittest"), ".pyunit") + mimetypes.inited = False + mimetypes._default_mime_types() + mimetypes._db = None def test_read_mime_types(self): eq = self.assertEqual @@ -109,100 +70,6 @@ def test_read_mime_types(self): mock_open.assert_called_with(filename, encoding='utf-8') eq(mime_dict[".Français"], "application/no-mans-land") - def test_non_standard_types(self): - eq = self.assertEqual - # First try strict - eq(self.db.guess_file_type('foo.xul', strict=True), (None, None)) - # And then non-strict - eq(self.db.guess_file_type('foo.xul', strict=False), ('text/xul', None)) - eq(self.db.guess_file_type('foo.XUL', strict=False), ('text/xul', None)) - eq(self.db.guess_file_type('foo.invalid', strict=False), (None, None)) - eq(self.db.guess_extension('image/jpeg', strict=False), '.jpg') - eq(self.db.guess_extension('image/JPEG', strict=False), '.jpg') - - def test_filename_with_url_delimiters(self): - # bpo-38449: URL delimiters cases should be handled also. - # They would have different mime types if interpreted as URL as - # compared to when interpreted as filename because of the semicolon. - eq = self.assertEqual - gzip_expected = ('application/x-tar', 'gzip') - for name in ( - ';1.tar.gz', - '?1.tar.gz', - '#1.tar.gz', - '#1#.tar.gz', - ';1#.tar.gz', - ';&1=123;?.tar.gz', - '?k1=v1&k2=v2.tar.gz', - ): - for prefix in ('', '/', '\\', - 'c:', 'c:/', 'c:\\', 'c:/d/', 'c:\\d\\', - '//share/server/', '\\\\share\\server\\'): - path = prefix + name - with self.subTest(path=path): - eq(self.db.guess_file_type(path), gzip_expected) - eq(self.db.guess_type(path), gzip_expected) - expected = (None, None) if os.name == 'nt' else gzip_expected - for prefix in ('//', '\\\\', '//share/', '\\\\share\\'): - path = prefix + name - with self.subTest(path=path): - eq(self.db.guess_file_type(path), expected) - eq(self.db.guess_type(path), expected) - eq(self.db.guess_file_type(r" \"\`;b&b&c |.tar.gz"), gzip_expected) - eq(self.db.guess_type(r" \"\`;b&b&c |.tar.gz"), gzip_expected) - - eq(self.db.guess_file_type(r'foo/.tar.gz'), (None, 'gzip')) - eq(self.db.guess_type(r'foo/.tar.gz'), (None, 'gzip')) - expected = (None, 'gzip') if os.name == 'nt' else gzip_expected - eq(self.db.guess_file_type(r'foo\.tar.gz'), expected) - eq(self.db.guess_type(r'foo\.tar.gz'), expected) - eq(self.db.guess_type(r'scheme:foo\.tar.gz'), gzip_expected) - - def test_url(self): - result = self.db.guess_type('http://example.com/host.html') - result = self.db.guess_type('http://host.html') - msg = 'URL only has a host name, not a file' - self.assertSequenceEqual(result, (None, None), msg) - result = self.db.guess_type('http://example.com/host.html') - msg = 'Should be text/html' - self.assertSequenceEqual(result, ('text/html', None), msg) - result = self.db.guess_type('http://example.com/host.html#x.tar') - self.assertSequenceEqual(result, ('text/html', None)) - result = self.db.guess_type('http://example.com/host.html?q=x.tar') - self.assertSequenceEqual(result, ('text/html', None)) - - def test_guess_all_types(self): - # First try strict. Use a set here for testing the results because if - # test_urllib2 is run before test_mimetypes, global state is modified - # such that the 'all' set will have more items in it. - all = self.db.guess_all_extensions('text/plain', strict=True) - self.assertTrue(set(all) >= {'.bat', '.c', '.h', '.ksh', '.pl', '.txt'}) - self.assertEqual(len(set(all)), len(all)) # no duplicates - # And now non-strict - all = self.db.guess_all_extensions('image/jpeg', strict=False) - self.assertEqual(all, ['.jpg', '.jpe', '.jpeg']) - # And now for no hits - all = self.db.guess_all_extensions('image/jpg', strict=True) - self.assertEqual(all, []) - # And now for type existing in both strict and non-strict mappings. - self.db.add_type('test-type', '.strict-ext') - self.db.add_type('test-type', '.non-strict-ext', strict=False) - all = self.db.guess_all_extensions('test-type', strict=False) - self.assertEqual(all, ['.strict-ext', '.non-strict-ext']) - all = self.db.guess_all_extensions('test-type') - self.assertEqual(all, ['.strict-ext']) - # Test that changing the result list does not affect the global state - all.append('.no-such-ext') - all = self.db.guess_all_extensions('test-type') - self.assertNotIn('.no-such-ext', all) - - def test_encoding(self): - filename = support.findfile("mime.types") - mimes = mimetypes.MimeTypes([filename]) - exts = mimes.guess_all_extensions('application/vnd.geocube+xml', - strict=True) - self.assertEqual(exts, ['.g3', '.g\xb3']) - def test_init_reinitializes(self): # Issue 4936: make sure an init starts clean # First, put some poison into the types table @@ -341,6 +208,205 @@ def test_init_stability(self): self.assertEqual(types_map, mimetypes.types_map) self.assertEqual(common_types, mimetypes.common_types) + def test_init_files(self): + guess_file_type = mimetypes.guess_file_type + self.assertEqual(guess_file_type("file.test2")[0], None) + + filename = support.findfile("mime.types2") + mimetypes.init([filename]) + self.assertEqual(guess_file_type("file.test2")[0], "testing/test2") + + mimetypes.init() + self.assertEqual(guess_file_type("file.test2")[0], None) + + def test_init_knownfiles(self): + guess_file_type = mimetypes.guess_file_type + self.assertEqual(guess_file_type("file.test2")[0], None) + + filename = support.findfile("mime.types2") + mimetypes.knownfiles = [filename, "non-existent"] + self.addCleanup(mimetypes.knownfiles.clear) + + mimetypes.init() + self.assertEqual(guess_file_type("file.test2")[0], "testing/test2") + + def test_added_types_are_used(self): + mimetypes.add_type('testing/default-type', '') + mime_type, _ = mimetypes.guess_type('') + self.assertEqual(mime_type, 'testing/default-type') + + mime_type, _ = mimetypes.guess_type('test.myext') + self.assertEqual(mime_type, None) + + mimetypes.add_type('testing/type', '.myext') + mime_type, _ = mimetypes.guess_type('test.myext') + self.assertEqual(mime_type, 'testing/type') + + def test_add_type_with_undotted_extension_not_supported(self): + msg = "Extension 'undotted' must start with '.'" + with self.assertRaisesRegex(ValueError, msg): + mimetypes.add_type("testing/type", "undotted") + with self.assertRaisesRegex(ValueError, msg): + mimetypes.add_type("", "undotted") + + +class MimeTypesClassTestCase(unittest.TestCase): + def setUp(self): + self.db = mimetypes.MimeTypes() + + def test_init_files(self): + guess_file_type = self.db.guess_file_type + self.assertEqual(guess_file_type("file.test2")[0], None) + + filename = support.findfile("mime.types2") + db = mimetypes.MimeTypes([filename]) + guess_file_type = db.guess_file_type + self.assertEqual(guess_file_type("file.test2")[0], "testing/test2") + + def test_init_knownfiles(self): + filename = support.findfile("mime.types2") + mimetypes.knownfiles = [filename, "non-existent"] + self.addCleanup(mimetypes.knownfiles.clear) + + db = mimetypes.MimeTypes() + guess_file_type = db.guess_file_type + self.assertEqual(guess_file_type("file.test2")[0], None) + + def test_case_sensitivity(self): + eq = self.assertEqual + eq(self.db.guess_file_type("foobar.html"), ("text/html", None)) + eq(self.db.guess_type("scheme:foobar.html"), ("text/html", None)) + eq(self.db.guess_file_type("foobar.HTML"), ("text/html", None)) + eq(self.db.guess_type("scheme:foobar.HTML"), ("text/html", None)) + eq(self.db.guess_file_type("foobar.tgz"), ("application/x-tar", "gzip")) + eq(self.db.guess_type("scheme:foobar.tgz"), ("application/x-tar", "gzip")) + eq(self.db.guess_file_type("foobar.TGZ"), ("application/x-tar", "gzip")) + eq(self.db.guess_type("scheme:foobar.TGZ"), ("application/x-tar", "gzip")) + eq(self.db.guess_file_type("foobar.tar.Z"), ("application/x-tar", "compress")) + eq(self.db.guess_type("scheme:foobar.tar.Z"), ("application/x-tar", "compress")) + eq(self.db.guess_file_type("foobar.tar.z"), (None, None)) + eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None)) + + def test_default_data(self): + eq = self.assertEqual + eq(self.db.guess_file_type("foo.html"), ("text/html", None)) + eq(self.db.guess_file_type("foo.HTML"), ("text/html", None)) + eq(self.db.guess_file_type("foo.tgz"), ("application/x-tar", "gzip")) + eq(self.db.guess_file_type("foo.tar.gz"), ("application/x-tar", "gzip")) + eq(self.db.guess_file_type("foo.tar.Z"), ("application/x-tar", "compress")) + eq(self.db.guess_file_type("foo.tar.bz2"), ("application/x-tar", "bzip2")) + eq(self.db.guess_file_type("foo.tar.xz"), ("application/x-tar", "xz")) + + def test_data_urls(self): + eq = self.assertEqual + guess_type = self.db.guess_type + eq(guess_type("data:invalidDataWithoutComma"), (None, None)) + eq(guess_type("data:,thisIsTextPlain"), ("text/plain", None)) + eq(guess_type("data:;base64,thisIsTextPlain"), ("text/plain", None)) + eq(guess_type("data:text/x-foo,thisIsTextXFoo"), ("text/x-foo", None)) + + def test_file_parsing(self): + eq = self.assertEqual + sio = io.StringIO("x-application/x-unittest pyunit\n") + self.db.readfp(sio) + eq(self.db.guess_file_type("foo.pyunit"), + ("x-application/x-unittest", None)) + eq(self.db.guess_extension("x-application/x-unittest"), ".pyunit") + + def test_non_standard_types(self): + eq = self.assertEqual + # First try strict + eq(self.db.guess_file_type('foo.xul', strict=True), (None, None)) + # And then non-strict + eq(self.db.guess_file_type('foo.xul', strict=False), ('text/xul', None)) + eq(self.db.guess_file_type('foo.XUL', strict=False), ('text/xul', None)) + eq(self.db.guess_file_type('foo.invalid', strict=False), (None, None)) + eq(self.db.guess_extension('image/jpeg', strict=False), '.jpg') + eq(self.db.guess_extension('image/JPEG', strict=False), '.jpg') + + def test_filename_with_url_delimiters(self): + # bpo-38449: URL delimiters cases should be handled also. + # They would have different mime types if interpreted as URL as + # compared to when interpreted as filename because of the semicolon. + eq = self.assertEqual + gzip_expected = ('application/x-tar', 'gzip') + for name in ( + ';1.tar.gz', + '?1.tar.gz', + '#1.tar.gz', + '#1#.tar.gz', + ';1#.tar.gz', + ';&1=123;?.tar.gz', + '?k1=v1&k2=v2.tar.gz', + ): + for prefix in ('', '/', '\\', + 'c:', 'c:/', 'c:\\', 'c:/d/', 'c:\\d\\', + '//share/server/', '\\\\share\\server\\'): + path = prefix + name + with self.subTest(path=path): + eq(self.db.guess_file_type(path), gzip_expected) + eq(self.db.guess_type(path), gzip_expected) + expected = (None, None) if os.name == 'nt' else gzip_expected + for prefix in ('//', '\\\\', '//share/', '\\\\share\\'): + path = prefix + name + with self.subTest(path=path): + eq(self.db.guess_file_type(path), expected) + eq(self.db.guess_type(path), expected) + eq(self.db.guess_file_type(r" \"\`;b&b&c |.tar.gz"), gzip_expected) + eq(self.db.guess_type(r" \"\`;b&b&c |.tar.gz"), gzip_expected) + + eq(self.db.guess_file_type(r'foo/.tar.gz'), (None, 'gzip')) + eq(self.db.guess_type(r'foo/.tar.gz'), (None, 'gzip')) + expected = (None, 'gzip') if os.name == 'nt' else gzip_expected + eq(self.db.guess_file_type(r'foo\.tar.gz'), expected) + eq(self.db.guess_type(r'foo\.tar.gz'), expected) + eq(self.db.guess_type(r'scheme:foo\.tar.gz'), gzip_expected) + + def test_url(self): + result = self.db.guess_type('http://example.com/host.html') + result = self.db.guess_type('http://host.html') + msg = 'URL only has a host name, not a file' + self.assertSequenceEqual(result, (None, None), msg) + result = self.db.guess_type('http://example.com/host.html') + msg = 'Should be text/html' + self.assertSequenceEqual(result, ('text/html', None), msg) + result = self.db.guess_type('http://example.com/host.html#x.tar') + self.assertSequenceEqual(result, ('text/html', None)) + result = self.db.guess_type('http://example.com/host.html?q=x.tar') + self.assertSequenceEqual(result, ('text/html', None)) + + def test_guess_all_types(self): + # First try strict. Use a set here for testing the results because if + # test_urllib2 is run before test_mimetypes, global state is modified + # such that the 'all' set will have more items in it. + all = self.db.guess_all_extensions('text/plain', strict=True) + self.assertTrue(set(all) >= {'.bat', '.c', '.h', '.ksh', '.pl', '.txt'}) + self.assertEqual(len(set(all)), len(all)) # no duplicates + # And now non-strict + all = self.db.guess_all_extensions('image/jpeg', strict=False) + self.assertEqual(all, ['.jpg', '.jpe', '.jpeg']) + # And now for no hits + all = self.db.guess_all_extensions('image/jpg', strict=True) + self.assertEqual(all, []) + # And now for type existing in both strict and non-strict mappings. + self.db.add_type('test-type', '.strict-ext') + self.db.add_type('test-type', '.non-strict-ext', strict=False) + all = self.db.guess_all_extensions('test-type', strict=False) + self.assertEqual(all, ['.strict-ext', '.non-strict-ext']) + all = self.db.guess_all_extensions('test-type') + self.assertEqual(all, ['.strict-ext']) + # Test that changing the result list does not affect the global state + all.append('.no-such-ext') + all = self.db.guess_all_extensions('test-type') + self.assertNotIn('.no-such-ext', all) + + def test_encoding(self): + filename = support.findfile("mime.types") + mimes = mimetypes.MimeTypes([filename]) + exts = mimes.guess_all_extensions('application/vnd.geocube+xml', + strict=True) + self.assertEqual(exts, ['.g3', '.g\xb3']) + def test_path_like_ob(self): filename = "LICENSE.txt" filepath = os_helper.FakePath(filename) @@ -378,25 +444,6 @@ def test_keywords_args_api(self): self.assertEqual(self.db.guess_all_extensions( type='image/jpeg', strict=True), ['.jpg', '.jpe', '.jpeg']) - def test_added_types_are_used(self): - mimetypes.add_type('testing/default-type', '') - mime_type, _ = mimetypes.guess_type('') - self.assertEqual(mime_type, 'testing/default-type') - - mime_type, _ = mimetypes.guess_type('test.myext') - self.assertEqual(mime_type, None) - - mimetypes.add_type('testing/type', '.myext') - mime_type, _ = mimetypes.guess_type('test.myext') - self.assertEqual(mime_type, 'testing/type') - - def test_add_type_with_undotted_extension_not_supported(self): - msg = "Extension 'undotted' must start with '.'" - with self.assertRaisesRegex(ValueError, msg): - mimetypes.add_type("testing/type", "undotted") - with self.assertRaisesRegex(ValueError, msg): - mimetypes.add_type("", "undotted") - @unittest.skipUnless(sys.platform.startswith("win"), "Windows only") class Win32MimeTypesTestCase(unittest.TestCase): From ce073ec4ccce359b87e95187a0f9e95335330c31 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 1 Jun 2026 21:27:39 +0100 Subject: [PATCH 4/5] gh-150429: Fix sampling profiler generator stack test (#150433) --- .../test_sampling_profiler/test_blocking.py | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py b/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py index 102eb51b556cc77..1f4b6da32810561 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py @@ -39,8 +39,9 @@ class TestBlockingModeStackAccuracy(unittest.TestCase): @classmethod def setUpClass(cls): # Test script that uses a generator consumed in a loop. - # When consume_generator is on the arithmetic lines (temp1, temp2, etc.), - # fibonacci_generator should NOT be in the stack at all. + # When consume_generator is the executing leaf frame on the arithmetic + # lines (temp1, temp2, etc.), fibonacci_generator should NOT be in the + # stack at all. # Line numbers are important here - see ARITHMETIC_LINES below. cls.generator_script = textwrap.dedent(''' def fibonacci_generator(n): @@ -65,29 +66,32 @@ def main(): main() ''') # Line numbers of the arithmetic operations in consume_generator. - # These are the lines where fibonacci_generator should NOT be in the stack. - # The socket injection code adds 7 lines before our script. - # temp1 = value + 1 -> line 17 - # temp2 = value * 2 -> line 18 - # temp3 = value - 1 -> line 19 - # result = ... -> line 20 - cls.ARITHMETIC_LINES = {17, 18, 19, 20} + # These are the lines where fibonacci_generator should NOT be in the + # stack when consume_generator is the executing leaf frame. They account + # for the socket prelude added by test_subprocess(). + # temp1 = value + 1 -> line 16 + # temp2 = value * 2 -> line 17 + # temp3 = value - 1 -> line 18 + # result = ... -> line 19 + cls.ARITHMETIC_LINES = {16, 17, 18, 19} def test_generator_not_under_consumer_arithmetic(self): """Test that fibonacci_generator doesn't appear when consume_generator does arithmetic. - When consume_generator is executing arithmetic lines (temp1, temp2, etc.), - fibonacci_generator should NOT be anywhere in the stack - it's not being - called at that point. + When consume_generator is the leaf frame on arithmetic lines (temp1, + temp2, etc.), fibonacci_generator should NOT be anywhere in the stack - + it's not being called at that point. Non-leaf frame line numbers are + caller/resume metadata, not proof that the frame is executing. Valid stacks: - - consume_generator at 'for value in gen:' line WITH fibonacci_generator - at the top (generator is yielding) + - fibonacci_generator at the top (generator is executing), with + consume_generator below it - consume_generator at arithmetic lines WITHOUT fibonacci_generator (we're just doing math, not calling the generator) Invalid stacks (indicate torn/inconsistent reads): - - consume_generator at arithmetic lines WITH fibonacci_generator + - consume_generator leaf frame at arithmetic lines WITH + fibonacci_generator anywhere in the stack Note: call_tree is ordered from bottom (index 0) to top (index -1). @@ -110,6 +114,8 @@ def test_generator_not_under_consumer_arithmetic(self): total_samples = 0 invalid_stacks = 0 arithmetic_samples = 0 + generator_samples = 0 + generator_not_leaf_samples = 0 for (call_tree, _thread_id), count in collector.stack_counter.items(): total_samples += count @@ -117,15 +123,21 @@ def test_generator_not_under_consumer_arithmetic(self): if not call_tree: continue - # Find consume_generator in the stack and check its line number - for i, (filename, lineno, funcname) in enumerate(call_tree): - if funcname == "consume_generator" and lineno in self.ARITHMETIC_LINES: - arithmetic_samples += count - # Check if fibonacci_generator appears anywhere in this stack - func_names = [frame[2] for frame in call_tree] - if "fibonacci_generator" in func_names: - invalid_stacks += count - break + # Non-leaf frame line numbers can point at resume locations while + # a callee is the executing leaf frame. + _, lineno, funcname = call_tree[-1] + func_names = [frame[2] for frame in call_tree] + + if "fibonacci_generator" in func_names: + generator_samples += count + if funcname != "fibonacci_generator": + generator_not_leaf_samples += count + + if funcname == "consume_generator" and lineno in self.ARITHMETIC_LINES: + arithmetic_samples += count + # Check if fibonacci_generator appears anywhere in this stack. + if "fibonacci_generator" in func_names: + invalid_stacks += count self.assertGreater(total_samples, 10, f"Expected at least 10 samples, got {total_samples}") @@ -134,8 +146,15 @@ def test_generator_not_under_consumer_arithmetic(self): self.assertGreater(arithmetic_samples, 0, f"Expected some samples on arithmetic lines, got {arithmetic_samples}") + self.assertGreater(generator_samples, 0, + f"Expected some samples in fibonacci_generator, got {generator_samples}") + + self.assertEqual(generator_not_leaf_samples, 0, + f"Found {generator_not_leaf_samples}/{generator_samples} stacks where " + f"fibonacci_generator appears but is not the leaf frame.") + self.assertEqual(invalid_stacks, 0, f"Found {invalid_stacks}/{arithmetic_samples} invalid stacks where " f"fibonacci_generator appears in the stack when consume_generator " - f"is on an arithmetic line. This indicates torn/inconsistent stack " - f"traces are being captured.") + f"is the leaf frame on an arithmetic line. This indicates " + f"torn/inconsistent stack traces are being captured.") From 71fc4c66d3e675a5481b6b76e6c707c9b6f1e0e0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 2 Jun 2026 06:25:46 +0800 Subject: [PATCH 5/5] gh-150644: Tag Apple system log messages as public. (#150645) macOS 26 changed the default visibility of "dynamic" system messages. This changes the logging strategy to tag all messages as "public" so they are visible in the system log without special configuration. --- .../next/macOS/2026-05-31-10-40-00.gh-issue-150644.zLWyjj.rst | 3 +++ Python/pylifecycle.c | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/macOS/2026-05-31-10-40-00.gh-issue-150644.zLWyjj.rst diff --git a/Misc/NEWS.d/next/macOS/2026-05-31-10-40-00.gh-issue-150644.zLWyjj.rst b/Misc/NEWS.d/next/macOS/2026-05-31-10-40-00.gh-issue-150644.zLWyjj.rst new file mode 100644 index 000000000000000..7452a7c765c0a86 --- /dev/null +++ b/Misc/NEWS.d/next/macOS/2026-05-31-10-40-00.gh-issue-150644.zLWyjj.rst @@ -0,0 +1,3 @@ +When system logging is enabled (with ``config.use_system_logger``, messages +are now tagged as public. This allows the macOS 26 system logger to view +messages without special configuration. diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 182233329498076..97511d2daf2c8f7 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3327,7 +3327,9 @@ apple_log_write_impl(PyObject *self, PyObject *args) // Pass the user-provided text through explicit %s formatting // to avoid % literals being interpreted as a formatting directive. - os_log_with_type(OS_LOG_DEFAULT, logtype, "%s", text); + // Using {public} ensures "dynamic" string messages are visible + // in the log without special configuration. + os_log_with_type(OS_LOG_DEFAULT, logtype, "%{public}s", text); Py_RETURN_NONE; }