From 26e271bffe31893c22db2a8dbf2ab8fd327c2d73 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 21:10:38 +0000 Subject: [PATCH 1/4] Add GitHub Actions CI and comprehensive Writer tests - Set up GitHub Actions workflow to run tests on Python 3.8-3.12 - Added comprehensive test suite for Writer.escape() covering edge cases with backslashes, newlines, and their combinations - All 23 tests passing (10 existing + 13 new) --- .github/workflows/tests.yml | 30 ++++++++++++++++ tests/test_writer_escaping.py | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 tests/test_writer_escaping.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..0f93b77 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: Tests + +on: + push: + branches: [ main, claude/* ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Run tests + run: python -m unittest discover -s tests -p 'test*.py' -v diff --git a/tests/test_writer_escaping.py b/tests/test_writer_escaping.py new file mode 100644 index 0000000..a356122 --- /dev/null +++ b/tests/test_writer_escaping.py @@ -0,0 +1,66 @@ +import unittest +from nsv.writer import Writer +from nsv import dumps, loads + + +class TestWriterEscaping(unittest.TestCase): + def test_empty_string(self): + self.assertEqual(Writer.escape(""), "\\") + + def test_no_special_chars(self): + self.assertEqual(Writer.escape("hello"), "hello") + + def test_single_backslash(self): + self.assertEqual(Writer.escape("\\"), "\\\\") + + def test_single_newline(self): + self.assertEqual(Writer.escape("\n"), "\\n") + + def test_backslash_in_middle(self): + self.assertEqual(Writer.escape("a\\b"), "a\\\\b") + + def test_newline_in_middle(self): + self.assertEqual(Writer.escape("a\nb"), "a\\nb") + + def test_backslash_then_letter_n(self): + self.assertEqual(Writer.escape("\\n"), "\\\\n") + + def test_backslash_then_newline(self): + self.assertEqual(Writer.escape("\\\n"), "\\\\\\n") + + def test_multiple_backslashes(self): + self.assertEqual(Writer.escape("\\\\"), "\\\\\\\\") + self.assertEqual(Writer.escape("\\\\\\"), "\\\\\\\\\\\\") + + def test_multiple_newlines(self): + self.assertEqual(Writer.escape("\n\n"), "\\n\\n") + self.assertEqual(Writer.escape("\n\n\n"), "\\n\\n\\n") + + def test_alternating_backslash_newline(self): + self.assertEqual(Writer.escape("\\\n\\\n"), "\\\\\\n\\\\\\n") + + def test_complex_combinations(self): + self.assertEqual(Writer.escape("\\a\nb\\"), "\\\\a\\nb\\\\") + self.assertEqual(Writer.escape("\n\\\n\\"), "\\n\\\\\\n\\\\") + + def test_roundtrip_with_backslash_and_newline(self): + test_cases = [ + "", + "\\", + "\n", + "\\n", + "\\\n", + "a\\b\nc", + "\\\\\\", + "\n\n\n", + "test\\ \n end", + ] + for original in test_cases: + with self.subTest(original=repr(original)): + encoded = dumps([[original]]) + decoded = loads(encoded) + self.assertEqual(decoded[0][0], original) + + +if __name__ == '__main__': + unittest.main() From 25ca4c070c56ffb42f2455ec09aa7a17f88809b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 21:28:40 +0000 Subject: [PATCH 2/4] Fix Python 3.8 compatibility and integrate with existing test infrastructure - Use List[List[str]] instead of list[list[str]] for Python 3.8 support - Add escape edge cases to existing SAMPLES_DATA instead of separate test file - Follow existing test pattern with sample files in tests/samples/ - Remove redundant test_writer_escaping.py All 10 tests passing on Python 3.8+ --- nsv/core.py | 6 +-- tests/samples/escape_edge_cases.nsv | 7 +++ tests/test_utils.py | 4 ++ tests/test_writer_escaping.py | 66 ----------------------------- 4 files changed, 14 insertions(+), 69 deletions(-) create mode 100644 tests/samples/escape_edge_cases.nsv delete mode 100644 tests/test_writer_escaping.py diff --git a/nsv/core.py b/nsv/core.py index 9befe4e..72a4be2 100644 --- a/nsv/core.py +++ b/nsv/core.py @@ -1,13 +1,13 @@ -from typing import Iterable +from typing import Iterable, List from .reader import Reader from .writer import Writer -def load(file_obj) -> list[list[str]]: +def load(file_obj) -> List[List[str]]: """Load NSV data from a file-like object.""" return list(Reader(file_obj)) -def loads(s: str) -> list[list[str]]: +def loads(s: str) -> List[List[str]]: """Load NSV data from a string.""" data = [] row = [] diff --git a/tests/samples/escape_edge_cases.nsv b/tests/samples/escape_edge_cases.nsv new file mode 100644 index 0000000..12cff58 --- /dev/null +++ b/tests/samples/escape_edge_cases.nsv @@ -0,0 +1,7 @@ +\\n +\\\n +\\\\n + +\\\\ +\n\n + diff --git a/tests/test_utils.py b/tests/test_utils.py index 79b087a..5c6f107 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -29,6 +29,10 @@ [], ], 'multiline_encoded': [["line1\nline2", "r1c2", "r1c3"], ["anotherline1\nline2\nline3", "r2c2"]], + 'escape_edge_cases': [ + ["\\n", "\\\n", "\\\\n"], + ["\\\\", "\n\n"], + ], } diff --git a/tests/test_writer_escaping.py b/tests/test_writer_escaping.py deleted file mode 100644 index a356122..0000000 --- a/tests/test_writer_escaping.py +++ /dev/null @@ -1,66 +0,0 @@ -import unittest -from nsv.writer import Writer -from nsv import dumps, loads - - -class TestWriterEscaping(unittest.TestCase): - def test_empty_string(self): - self.assertEqual(Writer.escape(""), "\\") - - def test_no_special_chars(self): - self.assertEqual(Writer.escape("hello"), "hello") - - def test_single_backslash(self): - self.assertEqual(Writer.escape("\\"), "\\\\") - - def test_single_newline(self): - self.assertEqual(Writer.escape("\n"), "\\n") - - def test_backslash_in_middle(self): - self.assertEqual(Writer.escape("a\\b"), "a\\\\b") - - def test_newline_in_middle(self): - self.assertEqual(Writer.escape("a\nb"), "a\\nb") - - def test_backslash_then_letter_n(self): - self.assertEqual(Writer.escape("\\n"), "\\\\n") - - def test_backslash_then_newline(self): - self.assertEqual(Writer.escape("\\\n"), "\\\\\\n") - - def test_multiple_backslashes(self): - self.assertEqual(Writer.escape("\\\\"), "\\\\\\\\") - self.assertEqual(Writer.escape("\\\\\\"), "\\\\\\\\\\\\") - - def test_multiple_newlines(self): - self.assertEqual(Writer.escape("\n\n"), "\\n\\n") - self.assertEqual(Writer.escape("\n\n\n"), "\\n\\n\\n") - - def test_alternating_backslash_newline(self): - self.assertEqual(Writer.escape("\\\n\\\n"), "\\\\\\n\\\\\\n") - - def test_complex_combinations(self): - self.assertEqual(Writer.escape("\\a\nb\\"), "\\\\a\\nb\\\\") - self.assertEqual(Writer.escape("\n\\\n\\"), "\\n\\\\\\n\\\\") - - def test_roundtrip_with_backslash_and_newline(self): - test_cases = [ - "", - "\\", - "\n", - "\\n", - "\\\n", - "a\\b\nc", - "\\\\\\", - "\n\n\n", - "test\\ \n end", - ] - for original in test_cases: - with self.subTest(original=repr(original)): - encoded = dumps([[original]]) - decoded = loads(encoded) - self.assertEqual(decoded[0][0], original) - - -if __name__ == '__main__': - unittest.main() From 964f7af0258f0076cc0d43f50098086901c36a38 Mon Sep 17 00:00:00 2001 From: namingbe Date: Sat, 15 Nov 2025 22:38:06 +0100 Subject: [PATCH 3/4] add a dangling backslash test --- tests/samples/trailing_backslash.nsv | 13 +++++++++++++ tests/test_edge_cases.py | 11 +++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/samples/trailing_backslash.nsv diff --git a/tests/samples/trailing_backslash.nsv b/tests/samples/trailing_backslash.nsv new file mode 100644 index 0000000..1acad5c --- /dev/null +++ b/tests/samples/trailing_backslash.nsv @@ -0,0 +1,13 @@ +yo\ +shouln'ta\ +be\ +doing\ +this\ + +\ +or +\ +should +\ +ya + diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 775bc8d..64ce60d 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -20,6 +20,17 @@ def test_special_characters(self): rows = nsv.load(f) self.assertEqual(SAMPLES_DATA['special_chars'], rows) + def test_trailing_backslash(self): + """Test handling of special characters in field values.""" + expected = [ + ['yo', 'shouln\'ta', 'be', 'doing', 'this'], + ['', 'or', '', 'should', '', 'ya'], + ] + file_path = os.path.join(SAMPLES_DIR, 'trailing_backslash.nsv') + with open(file_path, 'r') as f: + rows = nsv.load(f) + self.assertEqual(expected, rows) + # def test_numeric_values(self): # """Test handling of numeric values.""" # data = [ From ecf06d368de7782eb5d3a4d634cc352915d39e42 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 21:45:55 +0000 Subject: [PATCH 4/4] Fix README: run tests from project root, not tests/ directory --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 343aed9..51670d3 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,20 @@ with open('output.nsv', 'w') as f: writer.write_row(['row2cell1', 'row2cell2', 'row2cell3']) ``` -## Running Tests +## Development + +### Running Tests + +**Important**: Always run tests from the project root to test local code changes (not the installed package): ```bash -cd tests -python -m unittest +python -m unittest discover -s tests -p 'test*.py' -v +``` + +Alternatively, install in editable mode: + +```bash +pip install -e . ``` Must cover