Skip to content

Commit adb4140

Browse files
authored
gh-129813, PEP 782: Add PyBytesWriter C API (#138822)
1 parent 3d521a6 commit adb4140

File tree

10 files changed

+873
-0
lines changed

10 files changed

+873
-0
lines changed

Doc/c-api/bytes.rst

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,153 @@ called with a non-bytes parameter.
219219
reallocation fails, the original bytes object at *\*bytes* is deallocated,
220220
*\*bytes* is set to ``NULL``, :exc:`MemoryError` is set, and ``-1`` is
221221
returned.
222+
223+
PyBytesWriter
224+
-------------
225+
226+
The :c:type:`PyBytesWriter` API can be used to create a Python :class:`bytes`
227+
object.
228+
229+
.. versionadded:: next
230+
231+
.. c:type:: PyBytesWriter
232+
233+
A bytes writer instance.
234+
235+
The API is **not thread safe**: a writer should only be used by a single
236+
thread at the same time.
237+
238+
The instance must be destroyed by :c:func:`PyBytesWriter_Finish` on
239+
success, or :c:func:`PyBytesWriter_Discard` on error.
240+
241+
242+
Create, Finish, Discard
243+
^^^^^^^^^^^^^^^^^^^^^^^
244+
245+
.. c:function:: PyBytesWriter* PyBytesWriter_Create(Py_ssize_t size)
246+
247+
Create a :c:type:`PyBytesWriter` to write *size* bytes.
248+
249+
If *size* is greater than zero, allocate *size* bytes, and set the
250+
writer size to *size*. The caller is responsible to write *size*
251+
bytes using :c:func:`PyBytesWriter_GetData`.
252+
253+
On error, set an exception and return NULL.
254+
255+
*size* must be positive or zero.
256+
257+
.. c:function:: PyObject* PyBytesWriter_Finish(PyBytesWriter *writer)
258+
259+
Finish a :c:type:`PyBytesWriter` created by
260+
:c:func:`PyBytesWriter_Create`.
261+
262+
On success, return a Python :class:`bytes` object.
263+
On error, set an exception and return ``NULL``.
264+
265+
The writer instance is invalid after the call in any case.
266+
No API can be called on the writer after :c:func:`PyBytesWriter_Finish`.
267+
268+
.. c:function:: PyObject* PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size)
269+
270+
Similar to :c:func:`PyBytesWriter_Finish`, but resize the writer
271+
to *size* bytes before creating the :class:`bytes` object.
272+
273+
.. c:function:: PyObject* PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf)
274+
275+
Similar to :c:func:`PyBytesWriter_Finish`, but resize the writer
276+
using *buf* pointer before creating the :class:`bytes` object.
277+
278+
Set an exception and return ``NULL`` if *buf* pointer is outside the
279+
internal buffer bounds.
280+
281+
Function pseudo-code::
282+
283+
Py_ssize_t size = (char*)buf - (char*)PyBytesWriter_GetData(writer);
284+
return PyBytesWriter_FinishWithSize(writer, size);
285+
286+
.. c:function:: void PyBytesWriter_Discard(PyBytesWriter *writer)
287+
288+
Discard a :c:type:`PyBytesWriter` created by :c:func:`PyBytesWriter_Create`.
289+
290+
Do nothing if *writer* is ``NULL``.
291+
292+
The writer instance is invalid after the call.
293+
No API can be called on the writer after :c:func:`PyBytesWriter_Discard`.
294+
295+
High-level API
296+
^^^^^^^^^^^^^^
297+
298+
.. c:function:: int PyBytesWriter_WriteBytes(PyBytesWriter *writer, const void *bytes, Py_ssize_t size)
299+
300+
Grow the *writer* internal buffer by *size* bytes,
301+
write *size* bytes of *bytes* at the *writer* end,
302+
and add *size* to the *writer* size.
303+
304+
If *size* is equal to ``-1``, call ``strlen(bytes)`` to get the
305+
string length.
306+
307+
On success, return ``0``.
308+
On error, set an exception and return ``-1``.
309+
310+
311+
Getters
312+
^^^^^^^
313+
314+
.. c:function:: Py_ssize_t PyBytesWriter_GetSize(PyBytesWriter *writer)
315+
316+
Get the writer size.
317+
318+
.. c:function:: void* PyBytesWriter_GetData(PyBytesWriter *writer)
319+
320+
Get the writer data: start of the internal buffer.
321+
322+
The pointer is valid until :c:func:`PyBytesWriter_Finish` or
323+
:c:func:`PyBytesWriter_Discard` is called on *writer*.
324+
325+
326+
Low-level API
327+
^^^^^^^^^^^^^
328+
329+
.. c:function:: int PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size)
330+
331+
Resize the writer to *size* bytes. It can be used to enlarge or to
332+
shrink the writer.
333+
334+
Newly allocated bytes are left uninitialized.
335+
336+
On success, return ``0``.
337+
On error, set an exception and return ``-1``.
338+
339+
*size* must be positive or zero.
340+
341+
.. c:function:: int PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t grow)
342+
343+
Resize the writer by adding *grow* bytes to the current writer size.
344+
345+
Newly allocated bytes are left uninitialized.
346+
347+
On success, return ``0``.
348+
On error, set an exception and return ``-1``.
349+
350+
*size* can be negative to shrink the writer.
351+
352+
.. c:function:: void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf)
353+
354+
Similar to :c:func:`PyBytesWriter_Grow`, but update also the *buf*
355+
pointer.
356+
357+
The *buf* pointer is moved if the internal buffer is moved in memory.
358+
The *buf* relative position within the internal buffer is left
359+
unchanged.
360+
361+
On error, set an exception and return ``NULL``.
362+
363+
*buf* must not be ``NULL``.
364+
365+
Function pseudo-code::
366+
367+
Py_ssize_t pos = (char*)buf - (char*)PyBytesWriter_GetData(writer);
368+
if (PyBytesWriter_Grow(writer, size) < 0) {
369+
return NULL;
370+
}
371+
return (char*)PyBytesWriter_GetData(writer) + pos;

Doc/whatsnew/3.15.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,22 @@ New features
707707
and :c:data:`Py_mod_abi`.
708708
(Contributed by Petr Viktorin in :gh:`137210`.)
709709

710+
* Implement :pep:`782`, the :c:type:`PyBytesWriter` API. Add functions:
711+
712+
* :c:func:`PyBytesWriter_Create`
713+
* :c:func:`PyBytesWriter_Discard`
714+
* :c:func:`PyBytesWriter_FinishWithPointer`
715+
* :c:func:`PyBytesWriter_FinishWithSize`
716+
* :c:func:`PyBytesWriter_Finish`
717+
* :c:func:`PyBytesWriter_GetData`
718+
* :c:func:`PyBytesWriter_GetSize`
719+
* :c:func:`PyBytesWriter_GrowAndUpdatePointer`
720+
* :c:func:`PyBytesWriter_Grow`
721+
* :c:func:`PyBytesWriter_Resize`
722+
* :c:func:`PyBytesWriter_WriteBytes`
723+
724+
(Contributed by Victor Stinner in :gh:`129813`.)
725+
710726

711727
Porting to Python 3.15
712728
----------------------

Include/cpython/bytesobject.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,42 @@ _PyBytes_Join(PyObject *sep, PyObject *iterable)
4040
{
4141
return PyBytes_Join(sep, iterable);
4242
}
43+
44+
45+
// --- PyBytesWriter API -----------------------------------------------------
46+
47+
typedef struct PyBytesWriter PyBytesWriter;
48+
49+
PyAPI_FUNC(PyBytesWriter *) PyBytesWriter_Create(
50+
Py_ssize_t size);
51+
PyAPI_FUNC(void) PyBytesWriter_Discard(
52+
PyBytesWriter *writer);
53+
PyAPI_FUNC(PyObject*) PyBytesWriter_Finish(
54+
PyBytesWriter *writer);
55+
PyAPI_FUNC(PyObject*) PyBytesWriter_FinishWithSize(
56+
PyBytesWriter *writer,
57+
Py_ssize_t size);
58+
PyAPI_FUNC(PyObject*) PyBytesWriter_FinishWithPointer(
59+
PyBytesWriter *writer,
60+
void *buf);
61+
62+
PyAPI_FUNC(void*) PyBytesWriter_GetData(
63+
PyBytesWriter *writer);
64+
PyAPI_FUNC(Py_ssize_t) PyBytesWriter_GetSize(
65+
PyBytesWriter *writer);
66+
67+
PyAPI_FUNC(int) PyBytesWriter_WriteBytes(
68+
PyBytesWriter *writer,
69+
const void *bytes,
70+
Py_ssize_t size);
71+
72+
PyAPI_FUNC(int) PyBytesWriter_Resize(
73+
PyBytesWriter *writer,
74+
Py_ssize_t size);
75+
PyAPI_FUNC(int) PyBytesWriter_Grow(
76+
PyBytesWriter *writer,
77+
Py_ssize_t size);
78+
PyAPI_FUNC(void*) PyBytesWriter_GrowAndUpdatePointer(
79+
PyBytesWriter *writer,
80+
Py_ssize_t size,
81+
void *buf);

Include/internal/pycore_bytesobject.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ PyAPI_FUNC(void*) _PyBytesWriter_WriteBytes(_PyBytesWriter *writer,
143143
const void *bytes,
144144
Py_ssize_t size);
145145

146+
// Export for '_testcapi' shared extension.
147+
PyAPI_FUNC(PyBytesWriter*) _PyBytesWriter_CreateByteArray(
148+
Py_ssize_t size);
149+
146150
#ifdef __cplusplus
147151
}
148152
#endif

Include/internal/pycore_freelist_state.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ extern "C" {
2727
# define Py_futureiters_MAXFREELIST 255
2828
# define Py_object_stack_chunks_MAXFREELIST 4
2929
# define Py_unicode_writers_MAXFREELIST 1
30+
# define Py_bytes_writers_MAXFREELIST 1
3031
# define Py_pycfunctionobject_MAXFREELIST 16
3132
# define Py_pycmethodobject_MAXFREELIST 16
3233
# define Py_pymethodobjects_MAXFREELIST 20
@@ -61,6 +62,7 @@ struct _Py_freelists {
6162
struct _Py_freelist futureiters;
6263
struct _Py_freelist object_stack_chunks;
6364
struct _Py_freelist unicode_writers;
65+
struct _Py_freelist bytes_writers;
6466
struct _Py_freelist pycfunctionobject;
6567
struct _Py_freelist pycmethodobject;
6668
struct _Py_freelist pymethodobjects;

Lib/test/test_capi/test_bytes.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,5 +299,80 @@ def test_join(self):
299299
bytes_join(b'', NULL)
300300

301301

302+
class BytesWriterTest(unittest.TestCase):
303+
result_type = bytes
304+
305+
def create_writer(self, alloc=0, string=b''):
306+
return _testcapi.PyBytesWriter(alloc, string, 0)
307+
308+
def test_create(self):
309+
# Test PyBytesWriter_Create()
310+
writer = self.create_writer()
311+
self.assertEqual(writer.get_size(), 0)
312+
self.assertEqual(writer.finish(), self.result_type(b''))
313+
314+
writer = self.create_writer(3, b'abc')
315+
self.assertEqual(writer.get_size(), 3)
316+
self.assertEqual(writer.finish(), self.result_type(b'abc'))
317+
318+
writer = self.create_writer(10, b'abc')
319+
self.assertEqual(writer.get_size(), 10)
320+
self.assertEqual(writer.finish_with_size(3), self.result_type(b'abc'))
321+
322+
def test_write_bytes(self):
323+
# Test PyBytesWriter_WriteBytes()
324+
writer = self.create_writer()
325+
writer.write_bytes(b'Hello World!', -1)
326+
self.assertEqual(writer.finish(), self.result_type(b'Hello World!'))
327+
328+
writer = self.create_writer()
329+
writer.write_bytes(b'Hello ', -1)
330+
writer.write_bytes(b'World! <truncated>', 6)
331+
self.assertEqual(writer.finish(), self.result_type(b'Hello World!'))
332+
333+
def test_resize(self):
334+
# Test PyBytesWriter_Resize()
335+
writer = self.create_writer()
336+
writer.resize(len(b'number=123456'), b'number=123456')
337+
writer.resize(len(b'number=123456'), b'')
338+
self.assertEqual(writer.get_size(), len(b'number=123456'))
339+
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
340+
341+
writer = self.create_writer()
342+
writer.resize(0, b'')
343+
writer.resize(len(b'number=123456'), b'number=123456')
344+
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
345+
346+
writer = self.create_writer()
347+
writer.resize(len(b'number='), b'number=')
348+
writer.resize(len(b'number=123456'), b'123456')
349+
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
350+
351+
writer = self.create_writer()
352+
writer.resize(len(b'number='), b'number=')
353+
writer.resize(len(b'number='), b'')
354+
writer.resize(len(b'number=123456'), b'123456')
355+
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
356+
357+
writer = self.create_writer()
358+
writer.resize(len(b'number'), b'number')
359+
writer.resize(len(b'number='), b'=')
360+
writer.resize(len(b'number=123'), b'123')
361+
writer.resize(len(b'number=123456'), b'456')
362+
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
363+
364+
def test_example_abc(self):
365+
self.assertEqual(_testcapi.byteswriter_abc(), b'abc')
366+
367+
def test_example_resize(self):
368+
self.assertEqual(_testcapi.byteswriter_resize(), b'Hello World')
369+
370+
371+
class ByteArrayWriterTest(BytesWriterTest):
372+
result_type = bytearray
373+
374+
def create_writer(self, alloc=0, string=b''):
375+
return _testcapi.PyBytesWriter(alloc, string, 1)
376+
302377
if __name__ == "__main__":
303378
unittest.main()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Implement :pep:`782`, the :c:type:`PyBytesWriter` API. Add functions:
2+
3+
* :c:func:`PyBytesWriter_Create`
4+
* :c:func:`PyBytesWriter_Discard`
5+
* :c:func:`PyBytesWriter_FinishWithPointer`
6+
* :c:func:`PyBytesWriter_FinishWithSize`
7+
* :c:func:`PyBytesWriter_Finish`
8+
* :c:func:`PyBytesWriter_GetData`
9+
* :c:func:`PyBytesWriter_GetSize`
10+
* :c:func:`PyBytesWriter_GrowAndUpdatePointer`
11+
* :c:func:`PyBytesWriter_Grow`
12+
* :c:func:`PyBytesWriter_Resize`
13+
* :c:func:`PyBytesWriter_WriteBytes`
14+
15+
Patch by Victor Stinner.

0 commit comments

Comments
 (0)