From 04c67240ac754b28bec0c3fa2cd308f443ef7335 Mon Sep 17 00:00:00 2001 From: Harsh Thakkar Date: Thu, 24 Sep 2026 05:54:53 +0530 Subject: [PATCH] fix: auto-detect single-column CSV files Format detection passed the first bytes to csv.Sniffer().sniff(), which raises csv.Error('Could not determine delimiter') for files with no delimiter at all, so a bare single-column list like 'id\n1\n2' could never be loaded through rows_from_file. Fall back to the default CSV dialect when sniffing fails so DictReader treats the file as a single column. --- sqlite_utils/utils.py | 18 ++++++++++++++---- tests/test_utils.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/sqlite_utils/utils.py b/sqlite_utils/utils.py index 91fd24ddd..ab1db011a 100644 --- a/sqlite_utils/utils.py +++ b/sqlite_utils/utils.py @@ -389,14 +389,24 @@ class Format(enum.Enum): with buffered: return rows_from_file(buffered, format=Format.JSON) else: - dialect = csv.Sniffer().sniff( - first_bytes.decode(encoding or "utf-8-sig", "ignore") - ) + try: + dialect = csv.Sniffer().sniff( + first_bytes.decode(encoding or "utf-8-sig", "ignore") + ) + except csv.Error: + # Single-column files have no delimiter to sniff (e.g. a bare + # "id\n1\n2\n" list). Fall back to the default CSV dialect so + # DictReader still loads them as a single column. + dialect = None rows, _ = rows_from_file( buffered, format=Format.CSV, dialect=dialect, encoding=encoding ) # Make sure we return the format we detected - detected_format = Format.TSV if dialect.delimiter == "\t" else Format.CSV + detected_format = ( + Format.TSV + if dialect is not None and dialect.delimiter == "\t" + else Format.CSV + ) return ( _extra_key_strategy( cast(Iterable[dict[str | None, object]], rows), diff --git a/tests/test_utils.py b/tests/test_utils.py index 360a4436a..16b81a67c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -55,6 +55,21 @@ def test_hash_record(): ) +@pytest.mark.parametrize( + "content,expected", + ( + (b"apple\nbanana\ncherry\n", [{"apple": "banana"}, {"apple": "cherry"}]), + (b"id\n1\n2\n", [{"id": "1"}, {"id": "2"}]), + ), +) +def test_rows_from_file_detects_single_column_csv(content, expected): + # A single-column file has no delimiter for csv.Sniffer to detect; it + # used to raise csv.Error("Could not determine delimiter"). + rows, format_ = utils.rows_from_file(io.BytesIO(content)) + assert format_ == utils.Format.CSV + assert list(rows) == expected + + def test_maximize_csv_field_size_limit(): # Reset to default in case other tests have changed it csv.field_size_limit(utils.ORIGINAL_CSV_FIELD_SIZE_LIMIT)