From c3706f4c687d76b85702c1f946abd3f3d3eafcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Wed, 2 Sep 2026 18:01:29 +0800 Subject: [PATCH 1/2] gh-156810: Write the profiler's collapsed-stack export as UTF-8 (#156811) --- Lib/profiling/sampling/stack_collector.py | 3 ++- .../test_sampling_profiler/test_collectors.py | 22 +++++++++++++++++++ ...-09-02-17-28-16.gh-issue-156810.cLpEnc.rst | 4 ++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-02-17-28-16.gh-issue-156810.cLpEnc.rst diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index 8de460856666d7f..60ff5d7776b061d 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -60,7 +60,8 @@ def export(self, filename): lines.sort(key=lambda x: (-x[1], x[0])) - with open(filename, "w") as f: + with open(filename, "w", + encoding="utf-8", errors="surrogatepass") as f: for stack, count in lines: f.write(f"{stack} {count}\n") print(f"Collapsed stack output written to {filename}") diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index 069a72eb1c88a6a..dcfca9a9bceb503 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -466,6 +466,28 @@ def test_collapsed_stack_collector_export(self): self.assertIn(stack1_expected, lines) self.assertIn(stack2_expected, lines) + def test_collapsed_stack_collector_export_non_ascii_names(self): + # gh-156810: frame names are written verbatim, so the output must be + # opened with an encoding that can represent non-ASCII and + # surrogate-escaped (undecodable-path) names. + collapsed_out = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(close_and_unlink, collapsed_out) + + collector = CollapsedStackCollector(1000) + frame = MockFrameInfo("/tmp/ba\udc80d.py", 5, "计算") + collector.collect([ + MockInterpreterInfo(0, [MockThreadInfo(1, [frame])]) + ]) + + with captured_stdout(), captured_stderr(): + collector.export(collapsed_out.name) + + with open(collapsed_out.name, encoding="utf-8", + errors="surrogatepass") as f: + content = f.read() + self.assertIn("计算", content) + self.assertIn("ba\udc80d.py", content) + def test_flamegraph_collector_basic(self): """Test basic FlamegraphCollector functionality.""" collector = FlamegraphCollector(1000) diff --git a/Misc/NEWS.d/next/Library/2026-09-02-17-28-16.gh-issue-156810.cLpEnc.rst b/Misc/NEWS.d/next/Library/2026-09-02-17-28-16.gh-issue-156810.cLpEnc.rst new file mode 100644 index 000000000000000..610b0b4fcdc1baa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-02-17-28-16.gh-issue-156810.cLpEnc.rst @@ -0,0 +1,4 @@ +Fix a :exc:`UnicodeEncodeError` crash in the sampling profiler's +collapsed-stack export (``--collapsed``) when a sampled frame's function or +file name contains non-ASCII or surrogate-escaped characters. The output file +is now written as UTF-8. From d557d642d4272073bce05f3ce0bce28dd90288dd Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 2 Sep 2026 12:48:00 +0200 Subject: [PATCH 2/2] gh-155742: Use PyBytesWriter in io _textiowrapper_writeflush() (#155743) Replace soft deprecated PyBytes_FromStringAndSize() with PyBytesWriter. Replace PyBytes_AsStringAndSize() with PyBytes_AS_STRING() and PyBytes_GET_SIZE(). --- Modules/_io/textio.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index 5630f0309d98cff..a744a885932cdf5 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -1671,17 +1671,17 @@ _textiowrapper_writeflush(textio *self) } else { assert(PyList_Check(pending)); - b = PyBytes_FromStringAndSize(NULL, self->pending_bytes_count); - if (b == NULL) { + PyBytesWriter *writer = PyBytesWriter_Create(self->pending_bytes_count); + if (writer == NULL) { return -1; } - char *buf = PyBytes_AsString(b); + char *buf = PyBytesWriter_GetData(writer); Py_ssize_t pos = 0; for (Py_ssize_t i = 0; i < PyList_GET_SIZE(pending); i++) { PyObject *obj = PyList_GET_ITEM(pending, i); - char *src; + const char *src; Py_ssize_t len; if (PyUnicode_Check(obj)) { assert(PyUnicode_IS_ASCII(obj)); @@ -1690,15 +1690,18 @@ _textiowrapper_writeflush(textio *self) } else { assert(PyBytes_Check(obj)); - if (PyBytes_AsStringAndSize(obj, &src, &len) < 0) { - Py_DECREF(b); - return -1; - } + src = PyBytes_AS_STRING(obj); + len = PyBytes_GET_SIZE(obj); } memcpy(buf + pos, src, len); pos += len; } assert(pos == self->pending_bytes_count); + + b = PyBytesWriter_Finish(writer); + if (b == NULL) { + return -1; + } } self->pending_bytes_count = 0;