Skip to content

Commit a4a28e5

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

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
@@ -237,9 +237,11 @@ def test_write_lineterminator(self):
237237
writer = csv.writer(sio, lineterminator=lineterminator)
238238
writer.writerow(['a', 'b'])
239239
writer.writerow([1, 2])
240+
writer.writerow(['\r', '\n'])
240241
self.assertEqual(sio.getvalue(),
241242
f'a,b{lineterminator}'
242-
f'1,2{lineterminator}')
243+
f'1,2{lineterminator}'
244+
f'"\r","\n"{lineterminator}')
243245

244246
def test_write_iterable(self):
245247
self._write_test(iter(['a', 1, 'p,q']), 'a,1,"p,q"')
@@ -455,22 +457,44 @@ def test_read_linenum(self):
455457
self.assertEqual(r.line_num, 3)
456458

457459
def test_roundtrip_quoteed_newlines(self):
458-
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
459-
writer = csv.writer(fileobj)
460-
rows = [['a\nb','b'],['c','x\r\nd']]
461-
writer.writerows(rows)
462-
fileobj.seek(0)
463-
for i, row in enumerate(csv.reader(fileobj)):
464-
self.assertEqual(row, rows[i])
460+
rows = [
461+
['\na', 'b\nc', 'd\n'],
462+
['\re', 'f\rg', 'h\r'],
463+
['\r\ni', 'j\r\nk', 'l\r\n'],
464+
['\n\rm', 'n\n\ro', 'p\n\r'],
465+
['\r\rq', 'r\r\rs', 't\r\r'],
466+
['\n\nu', 'v\n\nw', 'x\n\n'],
467+
]
468+
for lineterminator in '\r\n', '\n', '\r':
469+
with self.subTest(lineterminator=lineterminator):
470+
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
471+
writer = csv.writer(fileobj, lineterminator=lineterminator)
472+
writer.writerows(rows)
473+
fileobj.seek(0)
474+
for i, row in enumerate(csv.reader(fileobj)):
475+
self.assertEqual(row, rows[i])
465476

466477
def test_roundtrip_escaped_unquoted_newlines(self):
467-
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
468-
writer = csv.writer(fileobj,quoting=csv.QUOTE_NONE,escapechar="\\")
469-
rows = [['a\nb','b'],['c','x\r\nd']]
470-
writer.writerows(rows)
471-
fileobj.seek(0)
472-
for i, row in enumerate(csv.reader(fileobj,quoting=csv.QUOTE_NONE,escapechar="\\")):
473-
self.assertEqual(row,rows[i])
478+
rows = [
479+
['\na', 'b\nc', 'd\n'],
480+
['\re', 'f\rg', 'h\r'],
481+
['\r\ni', 'j\r\nk', 'l\r\n'],
482+
['\n\rm', 'n\n\ro', 'p\n\r'],
483+
['\r\rq', 'r\r\rs', 't\r\r'],
484+
['\n\nu', 'v\n\nw', 'x\n\n'],
485+
]
486+
for lineterminator in '\r\n', '\n', '\r':
487+
with self.subTest(lineterminator=lineterminator):
488+
with TemporaryFile("w+", encoding="utf-8", newline='') as fileobj:
489+
writer = csv.writer(fileobj, lineterminator=lineterminator,
490+
quoting=csv.QUOTE_NONE, escapechar="\\")
491+
writer.writerows(rows)
492+
fileobj.seek(0)
493+
for i, row in enumerate(csv.reader(fileobj,
494+
quoting=csv.QUOTE_NONE,
495+
escapechar="\\")):
496+
self.assertEqual(row, rows[i])
497+
474498

475499
class TestDialectRegistry(unittest.TestCase):
476500
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, unsigned int field_kind, const void *field_dat
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)