Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b850389

Browse files
committed
Issue #21057: TextIOWrapper now allows the underlying binary stream's read() or read1() method to return an arbitrary bytes-like object (such as a memoryview).
Patch by Nikolaus Rath.
1 parent 92c4d45 commit b850389

3 files changed

Lines changed: 52 additions & 14 deletions

File tree

Lib/test/test_io.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2681,6 +2681,34 @@ def test_create_at_shutdown_with_encoding(self):
26812681
self.assertFalse(err)
26822682
self.assertEqual("ok", out.decode().strip())
26832683

2684+
def test_read_byteslike(self):
2685+
r = MemviewBytesIO(b'Just some random string\n')
2686+
t = self.TextIOWrapper(r, 'utf-8')
2687+
2688+
# TextIOwrapper will not read the full string, because
2689+
# we truncate it to a multiple of the native int size
2690+
# so that we can construct a more complex memoryview.
2691+
bytes_val = _to_memoryview(r.getvalue()).tobytes()
2692+
2693+
self.assertEqual(t.read(200), bytes_val.decode('utf-8'))
2694+
2695+
class MemviewBytesIO(io.BytesIO):
2696+
'''A BytesIO object whose read method returns memoryviews
2697+
rather than bytes'''
2698+
2699+
def read1(self, len_):
2700+
return _to_memoryview(super().read1(len_))
2701+
2702+
def read(self, len_):
2703+
return _to_memoryview(super().read(len_))
2704+
2705+
def _to_memoryview(buf):
2706+
'''Convert bytes-object *buf* to a non-trivial memoryview'''
2707+
2708+
arr = array.array('i')
2709+
idx = len(buf) - len(buf) % arr.itemsize
2710+
arr.frombytes(buf[:idx])
2711+
return memoryview(arr)
26842712

26852713
class CTextIOWrapperTest(TextIOWrapperTest):
26862714
io = io

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ Core and Builtins
6060
Library
6161
-------
6262

63+
- Issue #21057: TextIOWrapper now allows the underlying binary stream's
64+
read() or read1() method to return an arbitrary bytes-like object
65+
(such as a memoryview). Patch by Nikolaus Rath.
66+
6367
- Issue #20951: SSLSocket.send() now raises either SSLWantReadError or
6468
SSLWantWriteError on a non-blocking socket if the operation would block.
6569
Previously, it would return 0. Patch by Nikolaus Rath.

Modules/_io/textio.c

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,7 @@ textiowrapper_read_chunk(textio *self, Py_ssize_t size_hint)
14391439
PyObject *dec_buffer = NULL;
14401440
PyObject *dec_flags = NULL;
14411441
PyObject *input_chunk = NULL;
1442+
Py_buffer input_chunk_buf;
14421443
PyObject *decoded_chars, *chunk_size;
14431444
Py_ssize_t nbytes, nchars;
14441445
int eof;
@@ -1470,6 +1471,15 @@ textiowrapper_read_chunk(textio *self, Py_ssize_t size_hint)
14701471
Py_DECREF(state);
14711472
return -1;
14721473
}
1474+
1475+
if (!PyBytes_Check(dec_buffer)) {
1476+
PyErr_Format(PyExc_TypeError,
1477+
"decoder getstate() should have returned a bytes "
1478+
"object, not '%.200s'",
1479+
Py_TYPE(dec_buffer)->tp_name);
1480+
Py_DECREF(state);
1481+
return -1;
1482+
}
14731483
Py_INCREF(dec_buffer);
14741484
Py_INCREF(dec_flags);
14751485
Py_DECREF(state);
@@ -1482,23 +1492,24 @@ textiowrapper_read_chunk(textio *self, Py_ssize_t size_hint)
14821492
chunk_size = PyLong_FromSsize_t(Py_MAX(self->chunk_size, size_hint));
14831493
if (chunk_size == NULL)
14841494
goto fail;
1495+
14851496
input_chunk = PyObject_CallMethodObjArgs(self->buffer,
14861497
(self->has_read1 ? _PyIO_str_read1: _PyIO_str_read),
14871498
chunk_size, NULL);
14881499
Py_DECREF(chunk_size);
14891500
if (input_chunk == NULL)
14901501
goto fail;
1491-
if (!PyBytes_Check(input_chunk)) {
1502+
1503+
if (PyObject_GetBuffer(input_chunk, &input_chunk_buf, 0) != 0) {
14921504
PyErr_Format(PyExc_TypeError,
1493-
"underlying %s() should have returned a bytes object, "
1505+
"underlying %s() should have returned a bytes-like object, "
14941506
"not '%.200s'", (self->has_read1 ? "read1": "read"),
14951507
Py_TYPE(input_chunk)->tp_name);
14961508
goto fail;
14971509
}
14981510

1499-
nbytes = PyBytes_Size(input_chunk);
1511+
nbytes = input_chunk_buf.len;
15001512
eof = (nbytes == 0);
1501-
15021513
if (Py_TYPE(self->decoder) == &PyIncrementalNewlineDecoder_Type) {
15031514
decoded_chars = _PyIncrementalNewlineDecoder_decode(
15041515
self->decoder, input_chunk, eof);
@@ -1507,6 +1518,7 @@ textiowrapper_read_chunk(textio *self, Py_ssize_t size_hint)
15071518
decoded_chars = PyObject_CallMethodObjArgs(self->decoder,
15081519
_PyIO_str_decode, input_chunk, eof ? Py_True : Py_False, NULL);
15091520
}
1521+
PyBuffer_Release(&input_chunk_buf);
15101522

15111523
if (check_decoded(decoded_chars) < 0)
15121524
goto fail;
@@ -1523,18 +1535,12 @@ textiowrapper_read_chunk(textio *self, Py_ssize_t size_hint)
15231535
/* At the snapshot point, len(dec_buffer) bytes before the read, the
15241536
* next input to be decoded is dec_buffer + input_chunk.
15251537
*/
1526-
PyObject *next_input = PyNumber_Add(dec_buffer, input_chunk);
1527-
if (next_input == NULL)
1528-
goto fail;
1529-
if (!PyBytes_Check(next_input)) {
1530-
PyErr_Format(PyExc_TypeError,
1531-
"decoder getstate() should have returned a bytes "
1532-
"object, not '%.200s'",
1533-
Py_TYPE(next_input)->tp_name);
1534-
Py_DECREF(next_input);
1538+
PyObject *next_input = dec_buffer;
1539+
PyBytes_Concat(&next_input, input_chunk);
1540+
if (next_input == NULL) {
1541+
dec_buffer = NULL; /* Reference lost to PyBytes_Concat */
15351542
goto fail;
15361543
}
1537-
Py_DECREF(dec_buffer);
15381544
Py_CLEAR(self->snapshot);
15391545
self->snapshot = Py_BuildValue("NN", dec_flags, next_input);
15401546
}

0 commit comments

Comments
 (0)