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
48 changes: 33 additions & 15 deletions Lib/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,22 +563,40 @@ def _detect_doublequote(self, lines, delimiter, quotechar, escapechar):
def _detect_skipinitialspace(self, lines, delimiter, quotechar,
escapechar, doublequote):
"""
True only if every field following a delimiter starts with
a space.
Detect whether the spaces following a delimiter are a part of
the format or of the data.
"""
skipinitialspace = False
try:
for row in self._make_reader(lines, delimiter, quotechar,
escapechar,
doublequote=doublequote,
skipinitialspace=False):
for field in row[1:]:
if not field.startswith(' '):
return False
skipinitialspace = True
except Error:
pass
return skipinitialspace
results = []
for skipinitialspace in False, True:
rows = []
try:
rows.extend(self._make_reader(
lines, delimiter, quotechar, escapechar,
doublequote=doublequote,
skipinitialspace=skipinitialspace))
except Error:
# Keep the rows parsed before the error.
pass
results.append([row for row in rows if row])
if results[0] == results[1]:
return False # No evidence.
counts = [[len(row) for row in rows] for rows in results]
if counts[0] != counts[1]:
# Prefer the more consistent row widths.
return len(set(counts[1])) <= len(set(counts[0]))
# Only some spaces are stripped. A field differs only if
# a space was skipped at its start, which tells the padding
# apart from the spaces inside quoted or escaped fields.
if not all(kept_field != skipped_field
for kept_row, skipped_row in zip(*results)
for kept_field, skipped_field in zip(kept_row[1:],
skipped_row[1:])):
return False
# The first field of a row is commonly not padded ('a, b, c'),
# so the first fields need only agree with each other.
first = [kept_row[0] != skipped_row[0]
for kept_row, skipped_row in zip(*results)]
return all(first) or not any(first)

def has_header(self, sample):
# Creates a dictionary of types of data in each column. If any
Expand Down
54 changes: 54 additions & 0 deletions Lib/test/test_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,60 @@ def test_sniff_skipinitialspace_quoted(self):
self.assertEqual(dialect.quotechar, "'")
self.assertIs(dialect.skipinitialspace, True)

def test_sniff_skipinitialspace_quoted_fields(self):
# A quote is only a quote at the very start of a field, so not
# skipping the space splits the quoted field.
sniffer = csv.Sniffer()
sample = 'a, "b,c"\nd,e\nf,g\n'
dialect = sniffer.sniff(sample)
self.assertEqual(dialect.delimiter, ',')
self.assertEqual(dialect.quotechar, '"')
self.assertIs(dialect.skipinitialspace, True)
self.assertEqual(next(csv.reader(StringIO(sample), dialect)),
['a', 'b,c'])

# But without a delimiter inside the quotes nothing is split,
# so only the padding counts.
sample = 'a, "b"\nd,e\nf,g\n'
dialect = sniffer.sniff(sample)
self.assertEqual(dialect.delimiter, ',')
self.assertEqual(dialect.quotechar, '"')
self.assertIs(dialect.skipinitialspace, False)
self.assertEqual(next(csv.reader(StringIO(sample), dialect)),
['a', ' "b"'])

def test_sniff_skipinitialspace_data(self):
# The spaces are a part of the data if only some fields
# are padded.
sniffer = csv.Sniffer()
sample = 'a, b\nc,d\ne, f\n'
dialect = sniffer.sniff(sample)
self.assertEqual(dialect.delimiter, ',')
self.assertIs(dialect.skipinitialspace, False)
self.assertEqual(next(csv.reader(StringIO(sample), dialect)),
['a', ' b'])

def test_sniff_skipinitialspace_not_skipped(self):
# A quoted or escaped space is not skipped, so it is not
# an evidence of the padding.
sniffer = csv.Sniffer()
sample = 'a," b"\nc, d\n'
dialect = sniffer.sniff(sample)
self.assertEqual(dialect.delimiter, ',')
self.assertEqual(dialect.quotechar, '"')
self.assertIs(dialect.skipinitialspace, False)
self.assertEqual(list(csv.reader(StringIO(sample), dialect)),
[['a', ' b'], ['c', ' d']])

# The escaped delimiter forces the escapechar detection.
sample = 'a,\\ b\\,c\nd, e\n'
dialect = sniffer.sniff(sample)
self.assertEqual(dialect.delimiter, ',')
self.assertEqual(dialect.escapechar, '\\')
self.assertIs(dialect.skipinitialspace, False)
self.assertEqual(list(csv.reader(StringIO(sample), dialect)),
[['a', ' b,c'], ['d', ' e']])

def test_sniff_regex_backtracking(self):
# gh-109638: this artificial sample used to take minutes.
sniffer = csv.Sniffer()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve detection of ``skipinitialspace`` in :meth:`csv.Sniffer.sniff`.
Loading