8000 gh-117722: Fix Stream.readuntil with non-bytes buffer objects (#117723) · python/cpython@01a51f9 · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit 01a51f9

Browse files
authored
gh-117722: Fix Stream.readuntil with non-bytes buffer objects (#117723)
gh-16429 introduced support for an iterable of separators in Stream.readuntil. Since bytes-like types are themselves iterable, this can introduce ambiguities in deciding whether the argument is an iterator of separators or a singleton separator. In gh-16429, only 'bytes' was considered a singleton, but this will break code that passes other buffer object types. Fix it by only supporting tuples rather than arbitrary iterables. Closes gh-117722.
1 parent 898f6de commit 01a51f9

File tree

5 files changed

+27
-14
lines changed

5 files changed

+27
-14
lines changed

Doc/library/asyncio-stream.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ StreamReader
260260
buffer is reset. The :attr:`IncompleteReadError.partial` attribute
261261
may contain a portion of the separator.
262262

263-
The *separator* may also be an :term:`iterable` of separators. In this
263+
The *separator* may also be a tuple of separators. In this
264264
case the return value will be the shortest possible that has any
265265
separator as the suffix. For the purposes of :exc:`LimitOverrunError`,
266266
the shortest possible separator is considered to be the one that
@@ -270,7 +270,7 @@ StreamReader
270270

271271
.. versionchanged:: 3.13
272272

273-
The *separator* parameter may now be an :term:`iterable` of
273+
The *separator* parameter may now be a :class:`tuple` of
274274
separators.
275275

276276
.. method:: at_eof()

Doc/whatsnew/3.13.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,10 @@ asyncio
324324
:exc:`asyncio.QueueShutDown`) for queue termination.
325325
(Contributed by Laurie Opperman and Yves Duprat in :gh:`104228`.)
326326

327+
* Accept a tuple of separators in :meth:`asyncio.StreamReader.readuntil`,
328+
stopping when one of them is encountered.
329+
(Contributed by Bruce Merry in :gh:`81322`.)
330+
327331
base64
328332
------
329333

Lib/asyncio/streams.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -591,17 +591,17 @@ async def readuntil(self, separator=b'\n'):
591591
LimitOverrunError exception will be raised, and the data
592592
will be left in the internal buffer, so it can be read again.
593593
594-
The ``separator`` may also be an iterable of separators. In this
594+
The ``separator`` may also be a tuple of separators. In this
595595
case the return value will be the shortest possible that has any
596596
separator as the suffix. For the purposes of LimitOverrunError,
597597
the shortest possible separator is considered to be the one that
598598
matched.
599599
"""
600-
if isinstance(separator, bytes):
601-
separator = [separator]
602-
else:
603-
# Makes sure shortest matches wins, and supports arbitrary iterables
600+
if isinstance(separator, tuple):
601+
# Makes sure shortest matches wins
604602
separator = sorted(separator, key=len)
603+
else:
604+
separator = [separator]
605605
if not separator:
606606
raise ValueError('Separator should contain at least one element')
607607
min_seplen = len(separator[0])

Lib/test/test_asyncio/test_streams.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -384,9 +384,9 @@ def test_readuntil_separator(self):
384384
with self.assertRaisesRegex(ValueError, 'Separator should be'):
385385
self.loop.run_until_complete(stream.readuntil(separator=b''))
386386
with self.assertRaisesRegex(ValueError, 'Separator should be'):
387-
self.loop.run_until_complete(stream.readuntil(separator=[b'']))
387+
self.loop.run_until_complete(stream.readuntil(separator=(b'',)))
388388
with self.assertRaisesRegex(ValueError, 'Separator should contain'):
389-
self.loop.run_until_complete(stream.readuntil(separator=[]))
389+
self.loop.run_until_complete(stream.readuntil(separator=()))
390390

391391
def test_readuntil_multi_chunks(self):
392392
stream = asyncio.StreamReader(loop=self.loop)
@@ -475,15 +475,15 @@ def test_readuntil_multi_separator(self):
475475

476476
# Simple case
477477
stream.feed_data(b'line 1\nline 2\r')
478-
data = self.loop.run_until_complete(stream.readuntil([b'\r', b'\n']))
478+
data = self.loop.run_until_complete(stream.readuntil((b'\r' 57A6 , b'\n')))
479479
self.assertEqual(b'line 1\n', data)
480-
data = self.loop.run_until_complete(stream.readuntil([b'\r', b'\n']))
480+
data = self.loop.run_until_complete(stream.readuntil((b'\r', b'\n')))
481481
self.assertEqual(b'line 2\r', data)
482482
self.assertEqual(b'', stream._buffer)
483483

484484
# First end position matches, even if that's a longer match
485485
stream.feed_data(b'ABCDEFG')
486-
data = self.loop.run_until_complete(stream.readuntil([b'DEF', b'BCDE']))
486+
data = self.loop.run_until_complete(stream.readuntil((b'DEF', b'BCDE')))
487487
self.assertEqual(b'ABCDE', data)
488488
self.assertEqual(b'FG', stream._buffer)
489489

@@ -493,7 +493,7 @@ def test_readuntil_multi_separator_limit(self):
493493

494494
with self.assertRaisesRegex(asyncio.LimitOverrunError,
495495
'is found') as cm:
496-
self.loop.run_until_complete(stream.readuntil([b'A', b'ome dataA']))
496+
self.loop.run_until_complete(stream.readuntil((b'A', b'ome dataA')))
497497

498498
self.assertEqual(b'some dataA', stream._buffer)
499499

@@ -504,14 +504,21 @@ def test_readuntil_multi_separator_negative_offset(self):
504504
stream = asyncio.StreamReader(loop=self.loop)
505505
stream.feed_data(b'data')
506506

507-
readuntil_task = self.loop.create_task(stream.readuntil([b'A', b'long sep']))
507+
readuntil_task = self.loop.create_task(stream.readuntil((b'A', b'long sep')))
508508
self.loop.call_soon(stream.feed_data, b'Z')
509509
self.loop.call_soon(stream.feed_data, b'Aaaa')
510510

511511
data = self.loop.run_until_complete(readuntil_task)
512512
self.assertEqual(b'dataZA', data)
513513
self.assertEqual(b'aaa', stream._buffer)
514514

515+
def test_readuntil_bytearray(self):
516+
stream = asyncio.StreamReader(loop=self.loop)
517+
stream.feed_data(b'some data\r\n')
518+
data = self.loop.run_until_complete(stream.readuntil(bytearray(b'\r\n')))
519+
self.assertEqual(b'some data\r\n', data)
520+
self.assertEqual(b'', stream._buffer)
521+
515522
def test_readexactly_zero_or_less(self):
516523
# Read exact number of bytes (zero or less).
517524
stream = asyncio.StreamReader(loop=self.loop)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Change the new multi-separator support in :meth:`asyncio.Stream.readuntil`
2+
to only accept tuples of separators rather than arbitrary iterables.

0 commit comments

Comments
 (0)
0