Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions nsv/reader.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
class Reader:
def __init__(self, file_obj):
self._file_obj = file_obj
self._line_parts = []
self._row_buffer = []

def __iter__(self):
return self

def __next__(self):
acc = []
for line in self._file_obj:
if line[-1] != '\n': # missing newline = EOF mid-line
self._line_parts.append(line)
continue
if self._line_parts:
self._line_parts.append(line)
line = ''.join(self._line_parts)
self._line_parts = []
if line == '\n':
return acc
if line[-1] == '\n': # so as not to chop if missing newline at EOF
line = line[:-1]
acc.append(Reader.unescape(line)) # bruh
# at the end of the file
if acc:
return acc
else: # an empty row would self-report in the cycle body
raise StopIteration
row, self._row_buffer = self._row_buffer, []
return row
self._row_buffer.append(Reader.unescape(line[:-1])) # bruh
raise StopIteration

@staticmethod
def unescape(s: str) -> str:
Expand Down
37 changes: 34 additions & 3 deletions tests/test_incremental.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
import io
import os
import tempfile
import nsv
Expand All @@ -17,13 +18,43 @@ def test_incremental_reading(self):
second = next(reader)
self.assertEqual(second, ["d", "e", "f"])

# third = next(reader)
# self.assertEqual(third, ["last1", "last2"])

# Should be at end of the file
with self.assertRaises(StopIteration):
next(reader)

def test_incomplete_trailing_row_buffered(self):
"""A resumable Reader buffers an incomplete trailing row instead of emitting it."""
reader = nsv.Reader(io.StringIO('a\nb\n\nc\nd'))
self.assertEqual(next(reader), ['a', 'b'])
with self.assertRaises(StopIteration):
next(reader)

reader = nsv.Reader(io.StringIO('a\nb\n\nc\nd\n'))
self.assertEqual(next(reader), ['a', 'b'])
with self.assertRaises(StopIteration):
next(reader)

def test_reading_resumes_after_eof(self):
"""Reading continues across EOF once more data is appended (tailing)."""
with tempfile.TemporaryDirectory() as output_dir:
file_path = os.path.join(output_dir, 'tail.nsv')
with open(file_path, 'w') as f:
f.write('a\nb')
f.flush()

with open(file_path, 'r') as rf:
reader = nsv.Reader(rf)
with self.assertRaises(StopIteration):
next(reader)

f.write('c\nd\n\n')
f.flush()
self.assertEqual(next(reader), ['a', 'bc', 'd'])

def test_load_emits_incomplete_trailing_row(self):
"""Non-resumable loads keeps emitting the incomplete tail."""
self.assertEqual(nsv.loads('a\nb\n\nc\nd'), [['a', 'b'], ['c', 'd']])

def test_incremental_writing(self):
"""Test writing elements incrementally."""
data = [["field1", "field2"], ["value1", "value2"], ["last1", "last2"]]
Expand Down
Loading