Skip to content
Open
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
43 changes: 42 additions & 1 deletion src/tablib/formats/_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,53 @@ def export_set(cls, dataset, **kwargs):
stream = cls.export_stream_set(dataset, **kwargs)
return stream.getvalue()

#: Delimiters considered when detecting the dialect of a CSV stream.
CANDIDATE_DELIMITERS = (',', ';', ' ', '|', ':')

@classmethod
def sniff_delimiter(cls, in_stream):
"""Detect the delimiter used by a CSV stream.

Only a small set of common delimiters is considered, and the
default comma is used whenever the dialect cannot be determined
or only one column is present.
"""
try:
position = in_stream.tell()
except (AttributeError, OSError):
position = None

try:
sample = in_stream.read(2048)
except Exception:
return cls.DEFAULT_DELIMITER

if position is not None:
in_stream.seek(position)

for candidate in cls.CANDIDATE_DELIMITERS:
if candidate not in sample:
continue
try:
dialect = csv.Sniffer().sniff(sample, delimiters=candidate)
except Exception:
continue
if dialect.delimiter == candidate:
return candidate

return cls.DEFAULT_DELIMITER

@classmethod
def import_set(cls, dset, in_stream, headers=True, skip_lines=0, **kwargs):
def import_set(
cls, dset, in_stream, headers=True, skip_lines=0, **kwargs
):
"""Returns dataset from CSV stream."""

dset.wipe()

if 'delimiter' not in kwargs and not skip_lines:
kwargs['delimiter'] = cls.sniff_delimiter(in_stream)

kwargs.setdefault('delimiter', cls.DEFAULT_DELIMITER)

rows = csv.reader(in_stream, **kwargs)
Expand Down
69 changes: 67 additions & 2 deletions tests/test_tablib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,14 +1165,79 @@ def test_csv_formatter_support_kwargs(self):
_csv = data.export('csv', **kwargs)
self.assertEqual(expected, _csv)

# the import works but consider default delimiter=','
# the delimiter is now detected, so no explicit delimiter is needed
d1 = tablib.import_set(_csv, format="csv")
self.assertEqual(1, len(d1.headers))
self.assertEqual(3, len(d1.headers))

d2 = tablib.import_set(_csv, format="csv", **kwargs)
self.assertEqual(3, len(d2.headers))


def test_csv_import_set_sniffs_colon_delimiter(self):
"""Colon-separated CSV is detected without an explicit delimiter."""
csv_text = (
'test:test2\n'
'value1:value2\n'
)
data.csv = csv_text
self.assertEqual(('test', 'test2'), tuple(data.headers))
self.assertEqual(('value1', 'value2'), tuple(data[0]))

def test_csv_import_set_sniffs_semicolon_delimiter(self):
"""Semicolon-separated CSV is detected without an explicit
delimiter."""
csv_text = (
'a;b;c\n'
'1;2;3\n'
'4;5;6\n'
)
data.csv = csv_text
self.assertEqual(('a', 'b', 'c'), tuple(data.headers))
self.assertEqual(('1', '2', '3'), tuple(data[0]))
self.assertEqual(('4', '5', '6'), tuple(data[1]))

def test_csv_import_set_sniffs_pipe_delimiter(self):
"""Pipe-separated CSV is detected without an explicit delimiter."""
csv_text = (
'a|b|c\n'
'1|2|3\n'
)
data.csv = csv_text
self.assertEqual(('a', 'b', 'c'), tuple(data.headers))
self.assertEqual(('1', '2', '3'), tuple(data[0]))

def test_csv_import_set_sniffs_tab_delimiter(self):
"""Tab-separated CSV is detected without an explicit delimiter."""
csv_text = (
'a\tb\tc\n'
'1\t2\t3\n'
)
data.csv = csv_text
self.assertEqual(('a', 'b', 'c'), tuple(data.headers))
self.assertEqual(('1', '2', '3'), tuple(data[0]))

def test_csv_import_set_single_column_falls_back_to_comma(self):
"""A single-column CSV has no delimiter to detect, so it falls back
to the default without raising."""
csv_text = (
'name\n'
'Alice\n'
'Bob\n'
)
data.csv = csv_text
self.assertEqual(('name',), tuple(data.headers))
self.assertEqual(('Alice',), tuple(data[0]))

def test_csv_import_set_explicit_delimiter_still_wins(self):
"""An explicitly supplied delimiter takes precedence over
detection."""
csv_text = (
'a;b;c\n'
'1;2;3\n'
)
data.set_csv(csv_text, delimiter='|')
self.assertEqual(('a;b;c',), tuple(data.headers))

class TSVTests(BaseTestCase):
def test_tsv_import_set(self):
"""Generate and import TSV set serialization."""
Expand Down