Skip to content

Commit 4ac657a

Browse files
[3.12] gh-67044: Always quote or escape \r and \n in csv.writer() (GH-115741) (GH-115866)
(cherry picked from commit c688c0f) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 10907bd commit 4ac657a

File tree

3 files changed

+43
-15
lines changed

3 files changed

+43
-15
lines changed

Lib/test/test_csv.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,11 @@ def test_write_lineterminator(self):
240240
writer = csv.writer(sio, lineterminator=lineterminator)
241241
writer.writerow(['a', 'b'])
242242
writer.writerow([1, 2])
243+
writer.writerow(['\r', '\n'])
243244
self.assertEqual(sio.getvalue(),
244245
f'a,b{lineterminator}'
245-
f'1,2{lineterminator}')
246+
f'1,2{lineterminator}'
247+
f'"\r","\n"{lineterminator}')
246248

247249
def test_write_iterable(self):
248250
self._write_test(iter(['a', 1, 'p,q']), 'a,1,"p,q"')
@@ -469,22 +471,44 @@ def test_read_linenum(self):
469471
self.assertEqual(r.line_num, 3)
470472

471473
def test_roundtrip_quoteed_newlines(self):
472-
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
473-
writer = csv.writer(fileobj)
474-
rows = [['a\nb','b'],['c','x\r\nd']]
475-
writer.writerows(rows)
476-
fileobj.seek(0)
477-
for i, row in enumerate(csv.reader(fileobj)):
478-
self.assertEqual(row, rows[i])
474+
rows = [
475+
['\na', 'b\nc', 'd\n'],
476+
['\re', 'f\rg', 'h\r'],
477+
['\r\ni', 'j\r\nk', 'l\r\n'],
478+
['\n\rm', 'n\n\ro', 'p\n\r'],
479+
['\r\rq', 'r\r\rs', 't\r\r'],
480+
['\n\nu', 'v\n\nw', 'x\n\n'],
481+
]
482+
for lineterminator in '\r\n', '\n', '\r':
483+
with self.subTest(lineterminator=lineterminator):
484+
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
485+
writer = csv.writer(fileobj, lineterminator=lineterminator)
486+
writer.writerows(rows)
487+
fileobj.seek(0)
488+
for i, row in enumerate(csv.reader(fileobj)):
489+
self.assertEqual(row, rows[i])
479490

480491
def test_roundtrip_escaped_unquoted_newlines(self):
481-
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
482-
writer = csv.writer(fileobj,quoting=csv.QUOTE_NONE,escapechar="\\")
483-
rows = [['a\nb','b'],['c','x\r\nd']]
484-
writer.writerows(rows)
485-
fileobj.seek(0)
486-
for i, row in enumerate(csv.reader(fileobj,quoting=csv.QUOTE_NONE,escapechar="\\")):
487-
self.assertEqual(row,rows[i])
492+
rows = [
493+
['\na', 'b\nc', 'd\n'],
494+
['\re', 'f\rg', 'h\r'],
495+
['\r\ni', 'j\r\nk', 'l\r\n'],
496+
['\n\rm', 'n\n\ro', 'p\n\r'],
497+
['\r\rq', 'r\r\rs', 't\r\r'],
498+
['\n\nu', 'v\n\nw', 'x\n\n'],
499+
]
500+
for lineterminator in '\r\n', '\n', '\r':
501+
with self.subTest(lineterminator=lineterminator):
502+
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
503+
writer = csv.writer(fileobj, lineterminator=lineterminator,
504+
quoting=csv.QUOTE_NONE, escapechar="\\")
505+
writer.writerows(rows)
506+
fileobj.seek(0)
507+
for i, row in enumerate(csv.reader(fileobj,
508+
quoting=csv.QUOTE_NONE,
509+
escapechar="\\")):
510+
self.assertEqual(row, rows[i])
511+
488512

489513
class TestDialectRegistry(unittest.TestCase):
490514
def test_registry_badargs(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`csv.writer` now always quotes or escapes ``'\r'`` and ``'\n'``,
2+
regardless of *lineterminator* value.

Modules/_csv.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,8 @@ join_append_data(WriterObj *self, int field_kind, const void *field_data,
11091109
if (c == dialect->delimiter ||
11101110
c == dialect->escapechar ||
11111111
c == dialect->quotechar ||
1112+
c == '\n' ||
1113+
c == '\r' ||
11121114
PyUnicode_FindChar(
11131115
dialect->lineterminator, c, 0,
11141116
PyUnicode_GET_LENGTH(dialect->lineterminator), 1) >= 0) {

0 commit comments

Comments
 (0)