gh-119182: Add PyUnicodeWriter C API (#119184)

This commit is contained in:
Victor Stinner 2024-06-17 17:10:52 +02:00 committed by GitHub
parent 2c7209a3bd
commit 5c4235cd8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 533 additions and 18 deletions

View file

@ -1502,3 +1502,87 @@ They all return ``NULL`` or ``-1`` if an exception occurs.
:c:func:`PyUnicode_InternInPlace`, returning either a new Unicode string
object that has been interned, or a new ("owned") reference to an earlier
interned string object with the same value.
PyUnicodeWriter
^^^^^^^^^^^^^^^
The :c:type:`PyUnicodeWriter` API can be used to create a Python :class:`str`
object.
.. versionadded:: 3.14
.. c:type:: PyUnicodeWriter
A Unicode writer instance.
The instance must be destroyed by :c:func:`PyUnicodeWriter_Finish` on
success, or :c:func:`PyUnicodeWriter_Discard` on error.
.. c:function:: PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length)
Create a Unicode writer instance.
Set an exception and return ``NULL`` on error.
.. c:function:: PyObject* PyUnicodeWriter_Finish(PyUnicodeWriter *writer)
Return the final Python :class:`str` object and destroy the writer instance.
Set an exception and return ``NULL`` on error.
.. c:function:: void PyUnicodeWriter_Discard(PyUnicodeWriter *writer)
Discard the internal Unicode buffer and destroy the writer instance.
.. c:function:: int PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch)
Write the single Unicode character *ch* into *writer*.
On success, return ``0``.
On error, set an exception, leave the writer unchanged, and return ``-1``.
.. c:function:: int PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, const char *str, Py_ssize_t size)
Decode the string *str* from UTF-8 in strict mode and write the output into *writer*.
*size* is the string length in bytes. If *size* is equal to ``-1``, call
``strlen(str)`` to get the string length.
On success, return ``0``.
On error, set an exception, leave the writer unchanged, and return ``-1``.
To use a different error handler than ``strict``,
:c:func:`PyUnicode_DecodeUTF8` can be used with
:c:func:`PyUnicodeWriter_WriteStr`.
.. c:function:: int PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj)
Call :c:func:`PyObject_Str` on *obj* and write the output into *writer*.
On success, return ``0``.
On error, set an exception, leave the writer unchanged, and return ``-1``.
.. c:function:: int PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj)
Call :c:func:`PyObject_Repr` on *obj* and write the output into *writer*.
On success, return ``0``.
On error, set an exception, leave the writer unchanged, and return ``-1``.
.. c:function:: int PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, Py_ssize_t start, Py_ssize_t end)
Write the substring ``str[start:end]`` into *writer*.
*str* must be Python :class:`str` object. *start* must be greater than or
equal to 0, and less than or equal to *end*. *end* must be less than or
equal to *str* length.
On success, return ``0``.
On error, set an exception, leave the writer unchanged, and return ``-1``.
.. c:function:: int PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...)
Similar to :c:func:`PyUnicode_FromFormat`, but write the output directly into *writer*.
On success, return ``0``.
On error, set an exception, leave the writer unchanged, and return ``-1``.

View file

@ -283,6 +283,21 @@ New Features
* Add :c:func:`PyLong_GetSign` function to get the sign of :class:`int` objects.
(Contributed by Sergey B Kirpichev in :gh:`116560`.)
* Add a new :c:type:`PyUnicodeWriter` API to create a Python :class:`str`
object:
* :c:func:`PyUnicodeWriter_Create`.
* :c:func:`PyUnicodeWriter_Discard`.
* :c:func:`PyUnicodeWriter_Finish`.
* :c:func:`PyUnicodeWriter_WriteChar`.
* :c:func:`PyUnicodeWriter_WriteUTF8`.
* :c:func:`PyUnicodeWriter_WriteStr`.
* :c:func:`PyUnicodeWriter_WriteRepr`.
* :c:func:`PyUnicodeWriter_WriteSubstring`.
* :c:func:`PyUnicodeWriter_Format`.
(Contributed by Victor Stinner in :gh:`119182`.)
Porting to Python 3.14
----------------------

View file

@ -444,7 +444,40 @@ PyAPI_FUNC(PyObject*) PyUnicode_FromKindAndData(
Py_ssize_t size);
/* --- _PyUnicodeWriter API ----------------------------------------------- */
/* --- Public PyUnicodeWriter API ----------------------------------------- */
typedef struct PyUnicodeWriter PyUnicodeWriter;
PyAPI_FUNC(PyUnicodeWriter*) PyUnicodeWriter_Create(Py_ssize_t length);
PyAPI_FUNC(void) PyUnicodeWriter_Discard(PyUnicodeWriter *writer);
PyAPI_FUNC(PyObject*) PyUnicodeWriter_Finish(PyUnicodeWriter *writer);
PyAPI_FUNC(int) PyUnicodeWriter_WriteChar(
PyUnicodeWriter *writer,
Py_UCS4 ch);
PyAPI_FUNC(int) PyUnicodeWriter_WriteUTF8(
PyUnicodeWriter *writer,
const char *str,
Py_ssize_t size);
PyAPI_FUNC(int) PyUnicodeWriter_WriteStr(
PyUnicodeWriter *writer,
PyObject *obj);
PyAPI_FUNC(int) PyUnicodeWriter_WriteRepr(
PyUnicodeWriter *writer,
PyObject *obj);
PyAPI_FUNC(int) PyUnicodeWriter_WriteSubstring(
PyUnicodeWriter *writer,
PyObject *str,
Py_ssize_t start,
Py_ssize_t end);
PyAPI_FUNC(int) PyUnicodeWriter_Format(
PyUnicodeWriter *writer,
const char *format,
...);
/* --- Private _PyUnicodeWriter API --------------------------------------- */
typedef struct {
PyObject *buffer;
@ -466,7 +499,7 @@ typedef struct {
/* If readonly is 1, buffer is a shared string (cannot be modified)
and size is set to 0. */
unsigned char readonly;
} _PyUnicodeWriter ;
} _PyUnicodeWriter;
// Initialize a Unicode writer.
//

View file

@ -0,0 +1,13 @@
Add a new :c:type:`PyUnicodeWriter` API to create a Python :class:`str` object:
* :c:func:`PyUnicodeWriter_Create`.
* :c:func:`PyUnicodeWriter_Discard`.
* :c:func:`PyUnicodeWriter_Finish`.
* :c:func:`PyUnicodeWriter_WriteChar`.
* :c:func:`PyUnicodeWriter_WriteUTF8`.
* :c:func:`PyUnicodeWriter_WriteStr`.
* :c:func:`PyUnicodeWriter_WriteRepr`.
* :c:func:`PyUnicodeWriter_WriteSubstring`.
* :c:func:`PyUnicodeWriter_Format`.
Patch by Victor Stinner.

View file

@ -221,6 +221,221 @@ unicode_copycharacters(PyObject *self, PyObject *args)
}
static PyObject *
test_unicodewriter(PyObject *self, PyObject *Py_UNUSED(args))
{
PyUnicodeWriter *writer = PyUnicodeWriter_Create(100);
if (writer == NULL) {
return NULL;
}
// test PyUnicodeWriter_WriteUTF8()
if (PyUnicodeWriter_WriteUTF8(writer, "var", -1) < 0) {
goto error;
}
// test PyUnicodeWriter_WriteChar()
if (PyUnicodeWriter_WriteChar(writer, '=') < 0) {
goto error;
}
// test PyUnicodeWriter_WriteSubstring()
PyObject *str = PyUnicode_FromString("[long]");
if (str == NULL) {
goto error;
}
int ret = PyUnicodeWriter_WriteSubstring(writer, str, 1, 5);
Py_CLEAR(str);
if (ret < 0) {
goto error;
}
// test PyUnicodeWriter_WriteStr()
str = PyUnicode_FromString(" value ");
if (str == NULL) {
goto error;
}
ret = PyUnicodeWriter_WriteStr(writer, str);
Py_CLEAR(str);
if (ret < 0) {
goto error;
}
// test PyUnicodeWriter_WriteRepr()
str = PyUnicode_FromString("repr");
if (str == NULL) {
goto error;
}
ret = PyUnicodeWriter_WriteRepr(writer, str);
Py_CLEAR(str);
if (ret < 0) {
goto error;
}
PyObject *result = PyUnicodeWriter_Finish(writer);
if (result == NULL) {
return NULL;
}
assert(PyUnicode_EqualToUTF8(result, "var=long value 'repr'"));
Py_DECREF(result);
Py_RETURN_NONE;
error:
PyUnicodeWriter_Discard(writer);
return NULL;
}
static PyObject *
test_unicodewriter_utf8(PyObject *self, PyObject *Py_UNUSED(args))
{
PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
if (writer == NULL) {
return NULL;
}
if (PyUnicodeWriter_WriteUTF8(writer, "ascii", -1) < 0) {
goto error;
}
if (PyUnicodeWriter_WriteChar(writer, '-') < 0) {
goto error;
}
if (PyUnicodeWriter_WriteUTF8(writer, "latin1=\xC3\xA9", -1) < 0) {
goto error;
}
if (PyUnicodeWriter_WriteChar(writer, '-') < 0) {
goto error;
}
if (PyUnicodeWriter_WriteUTF8(writer, "euro=\xE2\x82\xAC", -1) < 0) {
goto error;
}
if (PyUnicodeWriter_WriteChar(writer, '.') < 0) {
goto error;
}
PyObject *result = PyUnicodeWriter_Finish(writer);
if (result == NULL) {
return NULL;
}
assert(PyUnicode_EqualToUTF8(result,
"ascii-latin1=\xC3\xA9-euro=\xE2\x82\xAC."));
Py_DECREF(result);
Py_RETURN_NONE;
error:
PyUnicodeWriter_Discard(writer);
return NULL;
}
static PyObject *
test_unicodewriter_invalid_utf8(PyObject *self, PyObject *Py_UNUSED(args))
{
PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
if (writer == NULL) {
return NULL;
}
assert(PyUnicodeWriter_WriteUTF8(writer, "invalid=\xFF", -1) < 0);
PyUnicodeWriter_Discard(writer);
assert(PyErr_ExceptionMatches(PyExc_UnicodeDecodeError));
PyErr_Clear();
Py_RETURN_NONE;
}
static PyObject *
test_unicodewriter_recover_error(PyObject *self, PyObject *Py_UNUSED(args))
{
// test recovering from PyUnicodeWriter_WriteUTF8() error
PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
if (writer == NULL) {
return NULL;
}
assert(PyUnicodeWriter_WriteUTF8(writer, "value=", -1) == 0);
// write fails with an invalid string
assert(PyUnicodeWriter_WriteUTF8(writer, "invalid\xFF", -1) < 0);
PyErr_Clear();
// retry write with a valid string
assert(PyUnicodeWriter_WriteUTF8(writer, "valid", -1) == 0);
PyObject *result = PyUnicodeWriter_Finish(writer);
if (result == NULL) {
return NULL;
}
assert(PyUnicode_EqualToUTF8(result, "value=valid"));
Py_DECREF(result);
Py_RETURN_NONE;
}
static PyObject *
test_unicodewriter_format(PyObject *self, PyObject *Py_UNUSED(args))
{
PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
if (writer == NULL) {
return NULL;
}
// test PyUnicodeWriter_Format()
if (PyUnicodeWriter_Format(writer, "%s %i", "Hello", 123) < 0) {
goto error;
}
// test PyUnicodeWriter_WriteChar()
if (PyUnicodeWriter_WriteChar(writer, '.') < 0) {
goto error;
}
PyObject *result = PyUnicodeWriter_Finish(writer);
if (result == NULL) {
return NULL;
}
assert(PyUnicode_EqualToUTF8(result, "Hello 123."));
Py_DECREF(result);
Py_RETURN_NONE;
error:
PyUnicodeWriter_Discard(writer);
return NULL;
}
static PyObject *
test_unicodewriter_format_recover_error(PyObject *self, PyObject *Py_UNUSED(args))
{
// test recovering from PyUnicodeWriter_Format() error
PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
if (writer == NULL) {
return NULL;
}
assert(PyUnicodeWriter_Format(writer, "%s ", "Hello") == 0);
// PyUnicodeWriter_Format() fails with an invalid format string
assert(PyUnicodeWriter_Format(writer, "%s\xff", "World") < 0);
PyErr_Clear();
// Retry PyUnicodeWriter_Format() with a valid format string
assert(PyUnicodeWriter_Format(writer, "%s.", "World") == 0);
PyObject *result = PyUnicodeWriter_Finish(writer);
if (result == NULL) {
return NULL;
}
assert(PyUnicode_EqualToUTF8(result, "Hello World."));
Py_DECREF(result);
Py_RETURN_NONE;
}
static PyMethodDef TestMethods[] = {
{"unicode_new", unicode_new, METH_VARARGS},
{"unicode_fill", unicode_fill, METH_VARARGS},
@ -229,6 +444,12 @@ static PyMethodDef TestMethods[] = {
{"unicode_asucs4copy", unicode_asucs4copy, METH_VARARGS},
{"unicode_asutf8", unicode_asutf8, METH_VARARGS},
{"unicode_copycharacters", unicode_copycharacters, METH_VARARGS},
{"test_unicodewriter", test_unicodewriter, METH_NOARGS},
{"test_unicodewriter_utf8", test_unicodewriter_utf8, METH_NOARGS},
{"test_unicodewriter_invalid_utf8", test_unicodewriter_invalid_utf8, METH_NOARGS},
{"test_unicodewriter_recover_error", test_unicodewriter_recover_error, METH_NOARGS},
{"test_unicodewriter_format", test_unicodewriter_format, METH_NOARGS},
{"test_unicodewriter_format_recover_error", test_unicodewriter_format_recover_error, METH_NOARGS},
{NULL},
};

View file

@ -2872,23 +2872,21 @@ unicode_fromformat_arg(_PyUnicodeWriter *writer,
return f;
}
PyObject *
PyUnicode_FromFormatV(const char *format, va_list vargs)
static int
unicode_from_format(_PyUnicodeWriter *writer, const char *format, va_list vargs)
{
writer->min_length += strlen(format) + 100;
writer->overallocate = 1;
va_list vargs2;
const char *f;
_PyUnicodeWriter writer;
_PyUnicodeWriter_Init(&writer);
writer.min_length = strlen(format) + 100;
writer.overallocate = 1;
// Copy varags to be able to pass a reference to a subfunction.
va_copy(vargs2, vargs);
for (f = format; *f; ) {
if (*f == '%') {
f = unicode_fromformat_arg(&writer, f, &vargs2);
f = unicode_fromformat_arg(writer, f, &vargs2);
if (f == NULL)
goto fail;
}
@ -2912,21 +2910,33 @@ PyUnicode_FromFormatV(const char *format, va_list vargs)
len = p - f;
if (*p == '\0')
writer.overallocate = 0;
writer->overallocate = 0;
if (_PyUnicodeWriter_WriteASCIIString(&writer, f, len) < 0)
if (_PyUnicodeWriter_WriteASCIIString(writer, f, len) < 0)
goto fail;
f = p;
}
}
va_end(vargs2);
return _PyUnicodeWriter_Finish(&writer);
return 0;
fail:
va_end(vargs2);
_PyUnicodeWriter_Dealloc(&writer);
return NULL;
return -1;
}
PyObject *
PyUnicode_FromFormatV(const char *format, va_list vargs)
{
_PyUnicodeWriter writer;
_PyUnicodeWriter_Init(&writer);
if (unicode_from_format(&writer, format, vargs) < 0) {
_PyUnicodeWriter_Dealloc(&writer);
return NULL;
}
return _PyUnicodeWriter_Finish(&writer);
}
PyObject *
@ -2941,6 +2951,23 @@ PyUnicode_FromFormat(const char *format, ...)
return ret;
}
int
PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...)
{
_PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
Py_ssize_t old_pos = _writer->pos;
va_list vargs;
va_start(vargs, format);
int res = unicode_from_format(_writer, format, vargs);
va_end(vargs);
if (res < 0) {
_writer->pos = old_pos;
}
return res;
}
static Py_ssize_t
unicode_get_widechar_size(PyObject *unicode)
{
@ -4927,6 +4954,7 @@ unicode_decode_utf8(const char *s, Py_ssize_t size,
}
// Used by PyUnicodeWriter_WriteUTF8() implementation
static int
unicode_decode_utf8_writer(_PyUnicodeWriter *writer,
const char *s, Py_ssize_t size,
@ -13080,6 +13108,7 @@ unicode_endswith_impl(PyObject *self, PyObject *subobj, Py_ssize_t start,
return PyBool_FromLong(result);
}
static inline void
_PyUnicodeWriter_Update(_PyUnicodeWriter *writer)
{
@ -13103,6 +13132,7 @@ _PyUnicodeWriter_Update(_PyUnicodeWriter *writer)
}
}
void
_PyUnicodeWriter_Init(_PyUnicodeWriter *writer)
{
@ -13111,12 +13141,41 @@ _PyUnicodeWriter_Init(_PyUnicodeWriter *writer)
/* ASCII is the bare minimum */
writer->min_char = 127;
/* use a value smaller than PyUnicode_1BYTE_KIND() so
/* use a kind value smaller than PyUnicode_1BYTE_KIND so
_PyUnicodeWriter_PrepareKind() will copy the buffer. */
writer->kind = 0;
assert(writer->kind <= PyUnicode_1BYTE_KIND);
assert(writer->kind == 0);
assert(writer->kind < PyUnicode_1BYTE_KIND);
}
PyUnicodeWriter*
PyUnicodeWriter_Create(Py_ssize_t length)
{
const size_t size = sizeof(_PyUnicodeWriter);
PyUnicodeWriter *pub_writer = (PyUnicodeWriter *)PyMem_Malloc(size);
if (pub_writer == NULL) {
return (PyUnicodeWriter *)PyErr_NoMemory();
}
_PyUnicodeWriter *writer = (_PyUnicodeWriter *)pub_writer;
_PyUnicodeWriter_Init(writer);
if (_PyUnicodeWriter_Prepare(writer, length, 127) < 0) {
PyUnicodeWriter_Discard(pub_writer);
return NULL;
}
writer->overallocate = 1;
return pub_writer;
}
void PyUnicodeWriter_Discard(PyUnicodeWriter *writer)
{
_PyUnicodeWriter_Dealloc((_PyUnicodeWriter*)writer);
PyMem_Free(writer);
}
// Initialize _PyUnicodeWriter with initial buffer
static inline void
_PyUnicodeWriter_InitWithBuffer(_PyUnicodeWriter *writer, PyObject *buffer)
@ -13127,6 +13186,7 @@ _PyUnicodeWriter_InitWithBuffer(_PyUnicodeWriter *writer, PyObject *buffer)
writer->min_length = writer->size;
}
int
_PyUnicodeWriter_PrepareInternal(_PyUnicodeWriter *writer,
Py_ssize_t length, Py_UCS4 maxchar)
@ -13242,9 +13302,17 @@ _PyUnicodeWriter_WriteChar(_PyUnicodeWriter *writer, Py_UCS4 ch)
return _PyUnicodeWriter_WriteCharInline(writer, ch);
}
int
PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch)
{
return _PyUnicodeWriter_WriteChar((_PyUnicodeWriter*)writer, ch);
}
int
_PyUnicodeWriter_WriteStr(_PyUnicodeWriter *writer, PyObject *str)
{
assert(PyUnicode_Check(str));
Py_UCS4 maxchar;
Py_ssize_t len;
@ -13270,6 +13338,34 @@ _PyUnicodeWriter_WriteStr(_PyUnicodeWriter *writer, PyObject *str)
return 0;
}
int
PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj)
{
PyObject *str = PyObject_Str(obj);
if (str == NULL) {
return -1;
}
int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str);
Py_DECREF(str);
return res;
}
int
PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj)
{
PyObject *repr = PyObject_Repr(obj);
if (repr == NULL) {
return -1;
}
int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, repr);
Py_DECREF(repr);
return res;
}
int
_PyUnicodeWriter_WriteSubstring(_PyUnicodeWriter *writer, PyObject *str,
Py_ssize_t start, Py_ssize_t end)
@ -13302,6 +13398,29 @@ _PyUnicodeWriter_WriteSubstring(_PyUnicodeWriter *writer, PyObject *str,
return 0;
}
int
PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str,
Py_ssize_t start, Py_ssize_t end)
{
if (!PyUnicode_Check(str)) {
PyErr_Format(PyExc_TypeError, "expect str, not %T", str);
return -1;
}
if (start < 0 || start > end) {
PyErr_Format(PyExc_ValueError, "invalid start argument");
return -1;
}
if (end > PyUnicode_GET_LENGTH(str)) {
PyErr_Format(PyExc_ValueError, "invalid end argument");
return -1;
}
return _PyUnicodeWriter_WriteSubstring((_PyUnicodeWriter*)writer, str,
start, end);
}
int
_PyUnicodeWriter_WriteASCIIString(_PyUnicodeWriter *writer,
const char *ascii, Py_ssize_t len)
@ -13362,6 +13481,25 @@ _PyUnicodeWriter_WriteASCIIString(_PyUnicodeWriter *writer,
return 0;
}
int
PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer,
const char *str,
Py_ssize_t size)
{
if (size < 0) {
size = strlen(str);
}
_PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
Py_ssize_t old_pos = _writer->pos;
int res = unicode_decode_utf8_writer(_writer, str, size,
_Py_ERROR_STRICT, NULL, NULL);
if (res < 0) {
_writer->pos = old_pos;
}
return res;
}
int
_PyUnicodeWriter_WriteLatin1String(_PyUnicodeWriter *writer,
const char *str, Py_ssize_t len)
@ -13408,6 +13546,17 @@ _PyUnicodeWriter_Finish(_PyUnicodeWriter *writer)
return unicode_result(str);
}
PyObject*
PyUnicodeWriter_Finish(PyUnicodeWriter *writer)
{
PyObject *str = _PyUnicodeWriter_Finish((_PyUnicodeWriter*)writer);
assert(((_PyUnicodeWriter*)writer)->buffer == NULL);
PyMem_Free(writer);
return str;
}
void
_PyUnicodeWriter_Dealloc(_PyUnicodeWriter *writer)
{