From 30a69dbfe3de0633fa0e459610e7f8b818348bba Mon Sep 17 00:00:00 2001 From: Jamie Cockburn Date: Mon, 3 Nov 2025 10:28:45 +0000 Subject: [PATCH] #70 - allow streaming iterables of strings --- README.md | 3 +-- pyproject.toml | 2 +- src/json_stream/iterators.py | 16 ++++++++++++---- src/json_stream/tests/test_iterators.py | 13 +++++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 69ec73d..34a3eba 100644 --- a/README.md +++ b/README.md @@ -391,8 +391,7 @@ with httpx.Client() as client, client.stream('GET', 'http://example.com/data.jso ### Stream an iterable -`json-stream`'s parsing functions can take any iterable object that produces encoded JSON as -`byte` objects. +`json-stream`'s parsing functions can take any iterable that produces encoded JSON chunks. The chunks can be `byte`s or `str`s. ```python import json_stream diff --git a/pyproject.toml b/pyproject.toml index 6dea1ab..9818dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "json-stream" -version = "2.4.0" +version = "2.4.1" authors = [ {name = "Jamie Cockburn", email="jamie_cockburn@hotmail.co.uk"}, ] diff --git a/src/json_stream/iterators.py b/src/json_stream/iterators.py index 241ab6d..7322adc 100644 --- a/src/json_stream/iterators.py +++ b/src/json_stream/iterators.py @@ -4,13 +4,21 @@ class IterableStream(io.RawIOBase): def __init__(self, iterable): self.iterator = iter(iterable) - self.remainder = None + self.remainder = None # type: bytes | None + + def _normalize_chunk(self, chunk): + # Ensure chunk is bytes for writing into a binary buffer + if isinstance(chunk, str): + return chunk.encode() + return chunk def readinto(self, buffer): try: - chunk = self.remainder or next(self.iterator) - length = min(len(buffer), len(chunk)) - buffer[:length], self.remainder = chunk[:length], chunk[length:] + # Wrap `buffer: WriteableBuffer` in memoryview to ensure len() and slicing + mv = memoryview(buffer) + chunk = self.remainder or self._normalize_chunk(next(self.iterator)) + length = min(len(mv), len(chunk)) + mv[:length], self.remainder = chunk[:length], chunk[length:] return length except StopIteration: return 0 # indicate EOF diff --git a/src/json_stream/tests/test_iterators.py b/src/json_stream/tests/test_iterators.py index 3c1e6b6..2b88b90 100644 --- a/src/json_stream/tests/test_iterators.py +++ b/src/json_stream/tests/test_iterators.py @@ -15,3 +15,16 @@ def test_read(self): # stream it and check the result stream = IterableStream(data) self.assertEqual(stream.read(), b"".join(data)) + + def test_read_str_chunks(self): + # create some chunks of text data + data_str = ( + "a" * io.DEFAULT_BUFFER_SIZE, + "b" * (io.DEFAULT_BUFFER_SIZE + 1), + "c" * (io.DEFAULT_BUFFER_SIZE - 1), + ) + expected = ("".join(data_str)).encode() + + # stream it and check the result + stream = IterableStream(data_str) + self.assertEqual(stream.read(), expected)