Skip to content

Commit c688c0f

Browse files
gh-67044: Always quote or escape \r and \n in csv.writer() (GH-115741)
1 parent 462a2fc commit c688c0f

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
@@ -265,9 +265,11 @@ def test_write_lineterminator(self):
265265
writer = csv.writer(sio, lineterminator=lineterminator)
266266
writer.writerow(['a', 'b'])
267267
writer.writerow([1, 2])
268+
writer.writerow(['\r', '\n'])
268269
self.assertEqual(sio.getvalue(),
269270
f'a,b{lineterminator}'
270-
f'1,2{lineterminator}')
271+
f'1,2{lineterminator}'
272+
f'"\r","\n"{lineterminator}')
271273

272274
def test_write_iterable(self):
273275
self._write_test(iter(['a', 1, 'p,q']), 'a,1,"p,q"')
@@ -507,22 +509,44 @@ def test_read_linenum(self):
507509
self.assertEqual(r.line_num, 3)
508510

509511
def test_roundtrip_quoteed_newlines(self):
510-
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
511-
writer = csv.writer(fileobj)
512-
rows = [['a\nb','b'],['c','x\r\nd']]
513-
writer.writerows(rows)
514-
fileobj.seek(0)
515-
for i, row in enumerate(csv.reader(fileobj)):
516-
self.assertEqual(row, rows[i])
512+
rows = [
513+
['\na', 'b\nc', 'd\n'],
514+
['\re', 'f\rg', 'h\r'],
515+
['\r\ni', 'j\r\nk', 'l\r\n'],
516+
['\n\rm', 'n\n\ro', 'p\n\r'],
517+
['\r\rq', 'r\r\rs', 't\r\r'],
518+
['\n\nu', 'v\n\nw', 'x\n\n'],
519+
]
520+
for lineterminator in '\r\n', '\n', '\r':
521+
with self.subTest(lineterminator=lineterminator):
522+
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
523+
writer = csv.writer(fileobj, lineterminator=lineterminator)
524+
writer.writerows(rows)
525+
fileobj.seek(0)
526+
for i, row in enumerate(csv.reader(fileobj)):
527+
self.assertEqual(row, rows[i])
517528

518529
def test_roundtrip_escaped_unquoted_newlines(self):
519-
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
520-
writer = csv.writer(fileobj,quoting=csv.QUOTE_NONE,escapechar="\\")
521-
rows = [['a\nb','b'],['c','x\r\nd']]
522-
writer.writerows(rows)
523-
fileobj.seek(0)
524-
for i, row in enumerate(csv.reader(fileobj,quoting=csv.QUOTE_NONE,escapechar="\\")):
525-
self.assertEqual(row,rows[i])
530+
rows = [
531+
['\na', 'b\nc', 'd\n'],
532+
['\re', 'f\rg', 'h\r'],
533+
['\r\ni', 'j\r\nk', 'l\r\n'],
534+
['\n\rm', 'n\n\ro', 'p\n\r'],
535+
['\r\rq', 'r\r\rs', 't\r\r'],
536+
['\n\nu', 'v\n\nw', 'x\n\n'],
537+
]
538+
for lineterminator in '\r\n', '\n', '\r':
539+
with self.subTest(lineterminator=lineterminator):
540+
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
541+
writer = csv.writer(fileobj, lineterminator=lineterminator,
542+
quoting=csv.QUOTE_NONE, escapechar="\\")
543+
writer.writerows(rows)
544+
fileobj.seek(0)
545+
for i, row in enumerate(csv.reader(fileobj,
546+
quoting=csv.QUOTE_NONE,
547+
escapechar="\\")):
548+
self.assertEqual(row, rows[i])
549+
526550

527551
class TestDialectRegistry(unittest.TestCase):
528552
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
@@ -1152,6 +1152,8 @@ join_append_data(WriterObj *self, int field_kind, const void *field_data,
11521152
if (c == dialect->delimiter ||
11531153
c == dialect->escapechar ||
11541154
c == dialect->quotechar ||
1155+
c == '\n' ||
1156+
c == '\r' ||
11551157
PyUnicode_FindChar(
11561158
dialect->lineterminator, c, 0,
11571159
PyUnicode_GET_LENGTH(dialect->lineterminator), 1) >= 0) {

0 commit comments

Comments
 (0)