Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c688c0f

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

File tree

Expand file treeCollapse file tree

3 files changed

+43
-15
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+43
-15
lines changed

‎Lib/test/test_csv.py

Copy file name to clipboardExpand all lines: Lib/test/test_csv.py
+39-15Lines 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):
+2Lines 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

Copy file name to clipboardExpand all lines: Modules/_csv.c
+2Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.