https://github.com/python/cpython/commit/c3fca5d4785a7e8406e864a662433644bffb4818
commit: c3fca5d4785a7e8406e864a662433644bffb4818
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2025-09-12T14:21:57+02:00
summary:
gh-129813, PEP 782: Add PyBytesWriter_Format() (#138824)
Modify PyBytes_FromFormatV() to use the public PyBytesWriter API
rather than the _PyBytesWriter private API.
files:
M Doc/c-api/bytes.rst
M Doc/whatsnew/3.15.rst
M Include/cpython/bytesobject.h
M Lib/test/test_capi/test_bytes.py
M Misc/NEWS.d/next/C_API/2025-09-12-13-05-20.gh-issue-129813.dJZpME.rst
M Modules/_testcapi/bytes.c
M Objects/bytesobject.c
diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst
index 0a3cbaa62712f8..8cc935cd06659c 100644
--- a/Doc/c-api/bytes.rst
+++ b/Doc/c-api/bytes.rst
@@ -307,6 +307,15 @@ High-level API
On success, return ``0``.
On error, set an exception and return ``-1``.
+.. c:function:: int PyBytesWriter_Format(PyBytesWriter *writer, const char
*format, ...)
+
+ Similar to :c:func:`PyBytes_FromFormat`, but write the output directly at
+ the writer end. Grow the writer internal buffer on demand. Then add the
+ written size to the writer size.
+
+ On success, return ``0``.
+ On error, set an exception and return ``-1``.
+
Getters
^^^^^^^
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 33c6659ea01775..1d029e3914baf5 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -714,6 +714,7 @@ New features
* :c:func:`PyBytesWriter_FinishWithPointer`
* :c:func:`PyBytesWriter_FinishWithSize`
* :c:func:`PyBytesWriter_Finish`
+ * :c:func:`PyBytesWriter_Format`
* :c:func:`PyBytesWriter_GetData`
* :c:func:`PyBytesWriter_GetSize`
* :c:func:`PyBytesWriter_GrowAndUpdatePointer`
diff --git a/Include/cpython/bytesobject.h b/Include/cpython/bytesobject.h
index 23a652cedf32cd..85bc2b827df8fb 100644
--- a/Include/cpython/bytesobject.h
+++ b/Include/cpython/bytesobject.h
@@ -68,6 +68,10 @@ PyAPI_FUNC(int) PyBytesWriter_WriteBytes(
PyBytesWriter *writer,
const void *bytes,
Py_ssize_t size);
+PyAPI_FUNC(int) PyBytesWriter_Format(
+ PyBytesWriter *writer,
+ const char *format,
+ ...);
PyAPI_FUNC(int) PyBytesWriter_Resize(
PyBytesWriter *writer,
diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py
index cc6c932c7d9072..410ebab729c2cf 100644
--- a/Lib/test/test_capi/test_bytes.py
+++ b/Lib/test/test_capi/test_bytes.py
@@ -361,12 +361,26 @@ def test_resize(self):
writer.resize(len(b'number=123456'), b'456')
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
+ def test_format_i(self):
+ # Test PyBytesWriter_Format()
+ writer = self.create_writer()
+ writer.format_i(b'x=%i', 123456)
+ self.assertEqual(writer.finish(), self.result_type(b'x=123456'))
+
+ writer = self.create_writer()
+ writer.format_i(b'x=%i, ', 123)
+ writer.format_i(b'y=%i', 456)
+ self.assertEqual(writer.finish(), self.result_type(b'x=123, y=456'))
+
def test_example_abc(self):
self.assertEqual(_testcapi.byteswriter_abc(), b'abc')
def test_example_resize(self):
self.assertEqual(_testcapi.byteswriter_resize(), b'Hello World')
+ def test_example_highlevel(self):
+ self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
+
class ByteArrayWriterTest(BytesWriterTest):
result_type = bytearray
@@ -374,5 +388,6 @@ class ByteArrayWriterTest(BytesWriterTest):
def create_writer(self, alloc=0, string=b''):
return _testcapi.PyBytesWriter(alloc, string, 1)
+
if __name__ == "__main__":
unittest.main()
diff --git
a/Misc/NEWS.d/next/C_API/2025-09-12-13-05-20.gh-issue-129813.dJZpME.rst
b/Misc/NEWS.d/next/C_API/2025-09-12-13-05-20.gh-issue-129813.dJZpME.rst
index 30d07279918045..e4abfb6f6ed410 100644
--- a/Misc/NEWS.d/next/C_API/2025-09-12-13-05-20.gh-issue-129813.dJZpME.rst
+++ b/Misc/NEWS.d/next/C_API/2025-09-12-13-05-20.gh-issue-129813.dJZpME.rst
@@ -5,6 +5,7 @@ Implement :pep:`782`, the :c:type:`PyBytesWriter` API. Add
functions:
* :c:func:`PyBytesWriter_FinishWithPointer`
* :c:func:`PyBytesWriter_FinishWithSize`
* :c:func:`PyBytesWriter_Finish`
+* :c:func:`PyBytesWriter_Format`
* :c:func:`PyBytesWriter_GetData`
* :c:func:`PyBytesWriter_GetSize`
* :c:func:`PyBytesWriter_GrowAndUpdatePointer`
diff --git a/Modules/_testcapi/bytes.c b/Modules/_testcapi/bytes.c
index 3530b6a4a42b44..388e65456c3a8b 100644
--- a/Modules/_testcapi/bytes.c
+++ b/Modules/_testcapi/bytes.c
@@ -163,6 +163,27 @@ writer_write_bytes(PyObject *self_raw, PyObject *args)
}
+static PyObject*
+writer_format_i(PyObject *self_raw, PyObject *args)
+{
+ WriterObject *self = (WriterObject *)self_raw;
+ if (writer_check(self) < 0) {
+ return NULL;
+ }
+
+ char *format;
+ int value;
+ if (!PyArg_ParseTuple(args, "yi", &format, &value)) {
+ return NULL;
+ }
+
+ if (PyBytesWriter_Format(self->writer, format, value) < 0) {
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+
static PyObject*
writer_resize(PyObject *self_raw, PyObject *args)
{
@@ -241,6 +262,7 @@ writer_finish_with_size(PyObject *self_raw, PyObject *args)
static PyMethodDef writer_methods[] = {
{"write_bytes", _PyCFunction_CAST(writer_write_bytes), METH_VARARGS},
+ {"format_i", _PyCFunction_CAST(writer_format_i), METH_VARARGS},
{"resize", _PyCFunction_CAST(writer_resize), METH_VARARGS},
{"get_size", _PyCFunction_CAST(writer_get_size), METH_NOARGS},
{"finish", _PyCFunction_CAST(writer_finish), METH_NOARGS},
@@ -309,11 +331,33 @@ byteswriter_resize(PyObject *Py_UNUSED(module), PyObject
*Py_UNUSED(args))
}
+static PyObject *
+byteswriter_highlevel(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
+{
+ PyBytesWriter *writer = PyBytesWriter_Create(0);
+ if (writer == NULL) {
+ goto error;
+ }
+ if (PyBytesWriter_WriteBytes(writer, "Hello", -1) < 0) {
+ goto error;
+ }
+ if (PyBytesWriter_Format(writer, " %s!", "World") < 0) {
+ goto error;
+ }
+ return PyBytesWriter_Finish(writer);
+
+error:
+ PyBytesWriter_Discard(writer);
+ return NULL;
+}
+
+
static PyMethodDef test_methods[] = {
{"bytes_resize", bytes_resize, METH_VARARGS},
{"bytes_join", bytes_join, METH_VARARGS},
{"byteswriter_abc", byteswriter_abc, METH_NOARGS},
{"byteswriter_resize", byteswriter_resize, METH_NOARGS},
+ {"byteswriter_highlevel", byteswriter_highlevel, METH_NOARGS},
{NULL},
};
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index aa099af8cf17f3..fc9e1bef80f037 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -196,10 +196,11 @@ PyBytes_FromString(const char *str)
return (PyObject *) op;
}
-PyObject *
-PyBytes_FromFormatV(const char *format, va_list vargs)
+
+static char*
+bytes_fromformat(PyBytesWriter *writer, Py_ssize_t writer_pos,
+ const char *format, va_list vargs)
{
- char *s;
const char *f;
const char *p;
Py_ssize_t prec;
@@ -213,21 +214,20 @@ PyBytes_FromFormatV(const char *format, va_list vargs)
Longest 64-bit pointer representation:
"0xffffffffffffffff\0" (19 bytes). */
char buffer[21];
- _PyBytesWriter writer;
- _PyBytesWriter_Init(&writer);
+ char *s = (char*)PyBytesWriter_GetData(writer) + writer_pos;
- s = _PyBytesWriter_Alloc(&writer, strlen(format));
- if (s == NULL)
- return NULL;
- writer.overallocate = 1;
-
-#define WRITE_BYTES(str) \
+#define WRITE_BYTES_LEN(str, len_expr) \
do { \
- s = _PyBytesWriter_WriteBytes(&writer, s, (str), strlen(str)); \
- if (s == NULL) \
+ size_t len = (len_expr); \
+ s = PyBytesWriter_GrowAndUpdatePointer(writer, len, s); \
+ if (s == NULL) { \
goto error; \
+ } \
+ memcpy(s, (str), len); \
+ s += len; \
} while (0)
+#define WRITE_BYTES(str) WRITE_BYTES_LEN(str, strlen(str))
for (f = format; *f; f++) {
if (*f != '%') {
@@ -268,10 +268,6 @@ PyBytes_FromFormatV(const char *format, va_list vargs)
++f;
}
- /* subtract bytes preallocated for the format string
- (ex: 2 for "%s") */
- writer.min_size -= (f - p + 1);
-
switch (*f) {
case 'c':
{
@@ -282,7 +278,6 @@ PyBytes_FromFormatV(const char *format, va_list vargs)
"expects an integer in range [0; 255]");
goto error;
}
- writer.min_size++;
*s++ = (unsigned char)c;
break;
}
@@ -341,9 +336,7 @@ PyBytes_FromFormatV(const char *format, va_list vargs)
i++;
}
}
- s = _PyBytesWriter_WriteBytes(&writer, s, p, i);
- if (s == NULL)
- goto error;
+ WRITE_BYTES_LEN(p, i);
break;
}
@@ -362,31 +355,45 @@ PyBytes_FromFormatV(const char *format, va_list vargs)
break;
case '%':
- writer.min_size++;
*s++ = '%';
break;
default:
- if (*f == 0) {
- /* fix min_size if we reached the end of the format string */
- writer.min_size++;
- }
-
/* invalid format string: copy unformatted string and exit */
WRITE_BYTES(p);
- return _PyBytesWriter_Finish(&writer, s);
+ return s;
}
}
#undef WRITE_BYTES
+#undef WRITE_BYTES_LEN
- return _PyBytesWriter_Finish(&writer, s);
+ return s;
error:
- _PyBytesWriter_Dealloc(&writer);
return NULL;
}
+
+PyObject *
+PyBytes_FromFormatV(const char *format, va_list vargs)
+{
+ Py_ssize_t alloc = strlen(format);
+ PyBytesWriter *writer = PyBytesWriter_Create(alloc);
+ if (writer == NULL) {
+ return NULL;
+ }
+
+ char *s = bytes_fromformat(writer, 0, format, vargs);
+ if (s == NULL) {
+ PyBytesWriter_Discard(writer);
+ return NULL;
+ }
+
+ return PyBytesWriter_FinishWithPointer(writer, s);
+}
+
+
PyObject *
PyBytes_FromFormat(const char *format, ...)
{
@@ -399,6 +406,7 @@ PyBytes_FromFormat(const char *format, ...)
return ret;
}
+
/* Helpers for formatstring */
Py_LOCAL_INLINE(PyObject *)
@@ -4048,3 +4056,21 @@ PyBytesWriter_WriteBytes(PyBytesWriter *writer,
memcpy(buf + pos, bytes, size);
return 0;
}
+
+
+int
+PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...)
+{
+ Py_ssize_t pos = writer->size;
+ if (PyBytesWriter_Grow(writer, strlen(format)) < 0) {
+ return -1;
+ }
+
+ va_list vargs;
+ va_start(vargs, format);
+ char *buf = bytes_fromformat(writer, pos, format, vargs);
+ va_end(vargs);
+
+ Py_ssize_t size = buf - byteswriter_data(writer);
+ return PyBytesWriter_Resize(writer, size);
+}
_______________________________________________
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]