https://github.com/python/cpython/commit/a0491c893956668ec09308e6334f7817b18efbe9
commit: a0491c893956668ec09308e6334f7817b18efbe9
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-29T22:03:16+02:00
summary:

gh-158439: Add _PyUnicodeWriter.recheck_maxchar (#158442)

If PyUnicodeWriter_WriteUTF8() or PyUnicodeWriter_Format() fails,
PyUnicodeWriter_Finish() now checks if the string uses the most
efficient storage. Previously, PyUnicodeWriter_Finish() could return
a string with an inefficient storage.

* Add _PyUnicodeWriter.recheck_maxchar. PyUnicodeWriter_WriteUTF8()
  and PyUnicodeWriter_Format() now set recheck_maxchar to 1 on error.
* Move error handling from caller to _PyUnicode_DecodeUTF8Writer().
* PyUnicodeWriter_DecodeUTF8Stateful(): rename length parameter to
  size and rename string parameter to str.

files:
A Misc/NEWS.d/next/C_API/2026-09-29-19-01-56.gh-issue-158439.ybKo_6.rst
M Doc/c-api/unicode.rst
M Include/cpython/unicodeobject.h
M Lib/test/test_capi/test_unicode.py
M Objects/unicode_writer.c
M Objects/unicodeobject.c

diff --git a/Doc/c-api/unicode.rst b/Doc/c-api/unicode.rst
index 3b635fa7fa37441..5f918b447950d9b 100644
--- a/Doc/c-api/unicode.rst
+++ b/Doc/c-api/unicode.rst
@@ -1945,7 +1945,7 @@ object.
    On success, return ``0``.
    On error, set an exception, leave the writer unchanged, and return ``-1``.
 
-.. c:function:: int PyUnicodeWriter_DecodeUTF8Stateful(PyUnicodeWriter 
*writer, const char *string, Py_ssize_t length, const char *errors, Py_ssize_t 
*consumed)
+.. c:function:: int PyUnicodeWriter_DecodeUTF8Stateful(PyUnicodeWriter 
*writer, const char *str, Py_ssize_t size, const char *errors, Py_ssize_t 
*consumed)
 
    Decode the string *str* from UTF-8 with *errors* error handler and write the
    output into *writer*.
diff --git a/Include/cpython/unicodeobject.h b/Include/cpython/unicodeobject.h
index ea91f4158eb3929..3da18a6ad35db3d 100644
--- a/Include/cpython/unicodeobject.h
+++ b/Include/cpython/unicodeobject.h
@@ -516,8 +516,8 @@ PyAPI_FUNC(int) PyUnicodeWriter_Format(
     ...);
 PyAPI_FUNC(int) PyUnicodeWriter_DecodeUTF8Stateful(
     PyUnicodeWriter *writer,
-    const char *string,         /* UTF-8 encoded string */
-    Py_ssize_t length,          /* size of string */
+    const char *str,            /* UTF-8 encoded string */
+    Py_ssize_t size,            /* size of string */
     const char *errors,         /* error handling */
     Py_ssize_t *consumed);      /* bytes consumed */
 
@@ -538,6 +538,9 @@ typedef struct {
     /* minimum character (default: 127, ASCII) */
     Py_UCS4 min_char;
 
+    // If non-zero, _PyUnicodeWriter_Finish() needs to check maxchar.
+    int recheck_maxchar;
+
     /* If non-zero, overallocate the buffer (default: 0). */
     unsigned char overallocate;
 
diff --git a/Lib/test/test_capi/test_unicode.py 
b/Lib/test/test_capi/test_unicode.py
index 032b910a280083b..fc2bec2124a39be 100644
--- a/Lib/test/test_capi/test_unicode.py
+++ b/Lib/test/test_capi/test_unicode.py
@@ -1976,7 +1976,11 @@ def test_invalid_utf8(self):
             writer.write_utf8(b"invalid=\xFF", -1)
 
     def test_recover_utf8_error(self):
-        # test recovering from PyUnicodeWriter_WriteUTF8() error
+        # Recover from PyUnicodeWriter_WriteUTF8() errors. A temporary write
+        # changes the buffer kind to UCS-2 before raising UnicodeDecodeError.
+        # Then, PyUnicodeWriter_Finish() has to change the buffer kind back to
+        # ASCII.
+
         writer = self.create_writer(0)
         writer.write_utf8(b"value=", -1)
 
@@ -1986,12 +1990,13 @@ def test_recover_utf8_error(self):
         with self.assertRaises(UnicodeDecodeError):
             s = "truncated\u20AC".encode()
             writer.write_utf8(s, len(s) - 1)
+        with self.assertRaises(UnicodeDecodeError):
+            # Change buffer kind to UCS-2 then raise UnicodeDecodeError
+            s = "\u20AC\u20AC".encode()
+            writer.write_utf8(s, len(s) - 1)
 
-        # retry write with a valid string
         writer.write_utf8(b"valid", -1)
-
-        self.assertEqual(writer.finish(),
-                         "value=valid")
+        self.assertEqual(writer.finish(), "value=valid")
 
     def test_decode_utf8(self):
         # test PyUnicodeWriter_DecodeUTF8Stateful()
@@ -2299,6 +2304,29 @@ def test_recover_error(self):
 
         self.assertEqual(writer.finish(), 'Hello World.')
 
+    def test_recheck_maxchar(self):
+        # PyUnicodeWriter_Format() changes buffer kind to UCS-2 before raising
+        # an exception. Then, PyUnicodeWriter_Finish() has to change the buffer
+        # kind back to ASCII.
+        from ctypes import py_object
+
+        class StrError:
+            def __str__(self):
+                raise RuntimeError("bug")
+
+        writer = self.create_writer(0)
+        # Allocate ASCII buffer
+        writer.write_str('ascii')
+
+        obj = StrError()
+        ucs2_utf8 = '\u20ac'.encode()
+        with self.assertRaises(RuntimeError):
+            # Change buffer kind to UCS-2, but then raise RuntimeError
+            self.writer_format(writer, b"%s%S", ucs2_utf8, py_object(obj))
+
+        writer.write_str('.')
+        self.assertEqual(writer.finish(), 'ascii.')
+
     def test_readonly_optim(self):
         # Read-only optimization: if the first and only write is a Python str
         # object and no buffer was allocated yet, return the object unchanged
diff --git 
a/Misc/NEWS.d/next/C_API/2026-09-29-19-01-56.gh-issue-158439.ybKo_6.rst 
b/Misc/NEWS.d/next/C_API/2026-09-29-19-01-56.gh-issue-158439.ybKo_6.rst
new file mode 100644
index 000000000000000..59739c7a5e16092
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-09-29-19-01-56.gh-issue-158439.ybKo_6.rst
@@ -0,0 +1,4 @@
+If :c:func:`PyUnicodeWriter_WriteUTF8` or :c:func:`PyUnicodeWriter_Format`
+fails, :c:func:`PyUnicodeWriter_Finish` now checks if the string uses the most
+efficient storage. Previously, :c:func:`PyUnicodeWriter_Finish` could return a
+string with an inefficient storage. Patch by Victor Stinner.
diff --git a/Objects/unicode_writer.c b/Objects/unicode_writer.c
index c1a2af4d9ac1fe9..26deffa6baac63e 100644
--- a/Objects/unicode_writer.c
+++ b/Objects/unicode_writer.c
@@ -160,7 +160,8 @@ PyUnicodeWriter_Create(Py_ssize_t length)
 }
 
 
-void PyUnicodeWriter_Discard(PyUnicodeWriter *writer)
+void
+PyUnicodeWriter_Discard(PyUnicodeWriter *writer)
 {
     if (writer == NULL) {
         return;
@@ -511,8 +512,7 @@ PyUnicodeWriter_WriteASCII(PyUnicodeWriter *writer,
     assert(writer != NULL);
     _Py_AssertHoldsTstate();
 
-    _PyUnicodeWriter *priv_writer = (_PyUnicodeWriter*)writer;
-    return _PyUnicodeWriter_WriteASCIIString(priv_writer, str, size);
+    return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, str, 
size);
 }
 
 
@@ -525,40 +525,25 @@ PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer,
         size = strlen(str);
     }
 
-    _PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
-    Py_ssize_t old_pos = _writer->pos;
-    int res = _PyUnicode_DecodeUTF8Writer(_writer, str, size,
-                                          _Py_ERROR_STRICT, NULL, NULL);
-    if (res < 0) {
-        _writer->pos = old_pos;
-    }
-    return res;
+    return _PyUnicode_DecodeUTF8Writer((_PyUnicodeWriter*)writer, str, size,
+                                       _Py_ERROR_STRICT, NULL, NULL);
 }
 
 
 int
 PyUnicodeWriter_DecodeUTF8Stateful(PyUnicodeWriter *writer,
-                                   const char *string,
-                                   Py_ssize_t length,
+                                   const char *str,
+                                   Py_ssize_t size,
                                    const char *errors,
                                    Py_ssize_t *consumed)
 {
-    if (length < 0) {
-        length = strlen(string);
-    }
-
-    _PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
-    Py_ssize_t old_pos = _writer->pos;
-    int res = _PyUnicode_DecodeUTF8Writer(_writer, string, length,
-                                          _Py_ERROR_UNKNOWN, errors,
-                                          consumed);
-    if (res < 0) {
-        _writer->pos = old_pos;
-        if (consumed) {
-            *consumed = 0;
-        }
+    if (size < 0) {
+        size = strlen(str);
     }
-    return res;
+
+    return _PyUnicode_DecodeUTF8Writer((_PyUnicodeWriter*)writer, str, size,
+                                       _Py_ERROR_UNKNOWN, errors,
+                                       consumed);
 }
 
 
@@ -600,30 +585,55 @@ _PyUnicodeWriter_Finish(_PyUnicodeWriter *writer)
 
     Py_ssize_t final_size = writer->pos;
     if (final_size == 0) {
+        // Get the empty string singleton
         PyObject *empty = _PyUnicode_GetEmpty();
         Py_XDECREF(str);  // writer->buffer can be NULL if the position is 0
         return empty;
     }
 
-    Py_ssize_t length = PyUnicode_GET_LENGTH(str);
+    if (writer->readonly) {
+        assert(final_size == PyUnicode_GET_LENGTH(str));
+        goto done;
+    }
+    assert(final_size <= PyUnicode_GET_LENGTH(str));
+
     if (final_size == 1 && PyUnicode_KIND(str) == PyUnicode_1BYTE_KIND) {
-        assert(length >= 1);
+        // Get the single character singleton
+        assert(PyUnicode_GET_LENGTH(str) >= 1);
         const Py_UCS1 *data = PyUnicode_1BYTE_DATA(str);
         Py_UCS1 ch = data[0];
-        PyObject *latin1_char = _Py_LATIN1_CHR(ch);
         Py_DECREF(str);
-        return latin1_char;
+        str = _Py_LATIN1_CHR(ch);
+        goto done;
+    }
+
+    if (writer->recheck_maxchar) {
+        Py_UCS4 maxchar = _PyUnicode_FindMaxChar(str, 0, final_size);
+        if (maxchar != writer->maxchar) {
+            // Adjust the string kind
+            PyObject *str2 = PyUnicode_New(final_size, maxchar);
+            if (str2 == NULL) {
+                Py_DECREF(str);
+                return NULL;
+            }
+            _PyUnicode_FastCopyCharacters(str2, 0, str, 0, final_size);
+            Py_SETREF(str, str2);
+            goto done;
+        }
     }
 
-    if (!writer->readonly && length != final_size) {
+    if (PyUnicode_GET_LENGTH(str) != final_size) {
+        // Truncate the string
         PyObject *str2 = _PyUnicode_ResizeCompact(str, final_size);
         if (str2 == NULL) {
             Py_DECREF(str);
             return NULL;
         }
         str = str2;
+        goto done;
     }
 
+done:
     assert(_PyUnicode_CheckConsistency(str, 1));
     return str;
 }
diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c
index 8446fdbfcb64a94..893621f041c9adb 100644
--- a/Objects/unicodeobject.c
+++ b/Objects/unicodeobject.c
@@ -3166,16 +3166,21 @@ PyUnicodeWriter_Format(PyUnicodeWriter *writer, const 
char *format, ...)
 }
 
 int
-_PyUnicodeWriter_FormatV(PyUnicodeWriter *writer, const char *format,
+_PyUnicodeWriter_FormatV(PyUnicodeWriter *pub_writer, const char *format,
                          va_list vargs)
 {
-    _PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
-    Py_ssize_t old_pos = _writer->pos;
+    _PyUnicodeWriter *writer = (_PyUnicodeWriter*)pub_writer;
+    Py_ssize_t old_pos = writer->pos;
+    Py_UCS4 old_maxchar = writer->maxchar;
 
-    int res = unicode_from_format(_writer, format, vargs);
+    int res = unicode_from_format(writer, format, vargs);
 
     if (res < 0) {
-        _writer->pos = old_pos;
+        writer->pos = old_pos;
+        if (writer->maxchar > old_maxchar) {
+            // _PyUnicodeWriter_Finish() will check maxchar
+            writer->recheck_maxchar = 1;
+        }
     }
     return res;
 }
@@ -5364,7 +5369,7 @@ unicode_decode_utf8(const char *s, Py_ssize_t size,
 }
 
 
-// Used by PyUnicodeWriter_WriteUTF8() implementation
+// Used by PyUnicodeWriter_WriteUTF8() and PyUnicodeWriter_DecodeUTF8Stateful()
 int
 _PyUnicode_DecodeUTF8Writer(_PyUnicodeWriter *writer,
                             const char *s, Py_ssize_t size,
@@ -5378,18 +5383,20 @@ _PyUnicode_DecodeUTF8Writer(_PyUnicodeWriter *writer,
         return 0;
     }
 
+    Py_ssize_t old_pos = writer->pos;
+    Py_UCS4 old_maxchar = writer->maxchar;
+
     // fast path: try ASCII string.
     if (_PyUnicodeWriter_Prepare(writer, size, 127) < 0) {
-        return -1;
+        goto error;
     }
     assert(_PyUnicodeWriter_CanWrite(writer));
 
     const char *starts = s;
     const char *end = s + size;
-    Py_ssize_t decoded = 0;
-    Py_UCS1 *dest = (Py_UCS1*)writer->data + writer->pos * writer->kind;
     if (writer->kind == PyUnicode_1BYTE_KIND) {
-        decoded = ascii_decode(s, end, dest);
+        Py_UCS1 *dest = (Py_UCS1*)writer->data + writer->pos * writer->kind;
+        Py_ssize_t decoded = ascii_decode(s, end, dest);
         writer->pos += decoded;
 
         if (decoded == size) {
@@ -5401,8 +5408,24 @@ _PyUnicode_DecodeUTF8Writer(_PyUnicodeWriter *writer,
         s += decoded;
     }
 
-    return unicode_decode_utf8_impl(writer, starts, s, end,
-                                    error_handler, errors, consumed);
+    int res = unicode_decode_utf8_impl(writer, starts, s, end,
+                                       error_handler, errors, consumed);
+    if (res < 0) {
+        goto error;
+    }
+    return 0;
+
+error:
+    // Restore the writer to its previous state
+    writer->pos = old_pos;
+    if (writer->maxchar > old_maxchar) {
+        // _PyUnicodeWriter_Finish() will check maxchar
+        writer->recheck_maxchar = 1;
+    }
+    if (consumed) {
+        *consumed = 0;
+    }
+    return -1;
 }
 
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to