From 1e601702fbce3da53d369a7f6fcf18587b164aad Mon Sep 17 00:00:00 2001 From: Shahadat Islam Koyel <179203101+ShahadatTest@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:39:19 +0600 Subject: [PATCH] Collect complete reverse-reading blocks after short reads --- boltons/jsonutils.py | 9 +++++++++ tests/test_reverse_short_reads.py | 33 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/test_reverse_short_reads.py diff --git a/boltons/jsonutils.py b/boltons/jsonutils.py index f9f91573..0794ba11 100644 --- a/boltons/jsonutils.py +++ b/boltons/jsonutils.py @@ -100,6 +100,15 @@ def reverse_iter_lines(file_obj, blocksize=DEFAULT_BLOCKSIZE, preseek=True, enco cur_pos -= read_size file_obj.seek(cur_pos, os.SEEK_SET) cur = file_obj.read(read_size) + if len(cur) < read_size: + parts, remaining = [cur], read_size - len(cur) + while remaining: + part = file_obj.read(remaining) + if not part: + break + parts.append(part) + remaining -= len(part) + cur = empty.join(parts) buff = cur + buff lines = buff.splitlines() diff --git a/tests/test_reverse_short_reads.py b/tests/test_reverse_short_reads.py new file mode 100644 index 00000000..aed3dbcd --- /dev/null +++ b/tests/test_reverse_short_reads.py @@ -0,0 +1,33 @@ +import io + +import pytest + +from boltons.jsonutils import reverse_iter_lines + + +class ShortReader: + def __init__(self, stream): + self.stream = stream + + def read(self, size=-1): + return self.stream.read(min(size, 2) if size >= 0 else 2) + + def seek(self, *args): + return self.stream.seek(*args) + + def tell(self): + return self.stream.tell() + + +@pytest.mark.parametrize('binary', [False, True]) +@pytest.mark.parametrize('blocksize', [4, 9]) +@pytest.mark.parametrize('trailing', [False, True]) +def test_reverse_lines_collects_short_reads(binary, blocksize, trailing): + data = 'first\nsecond\nthird' + ('\n' if trailing else '') + expected = ([''] if trailing else []) + ['third', 'second', 'first'] + if binary: + stream = io.BytesIO(data.encode('ascii')) + expected = [line.encode('ascii') for line in expected] + else: + stream = io.StringIO(data) + assert list(reverse_iter_lines(ShortReader(stream), blocksize=blocksize)) == expected