Skip to content
Merged
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
23 changes: 21 additions & 2 deletions Lib/test/test_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,18 @@ def test_write_quoting(self):
quoting = csv.QUOTE_STRINGS)
self._write_test(['a','',None,1], '"a","",,"1"',
quoting = csv.QUOTE_NOTNULL)
# FULLWIDTH QUOTATION MARK
self._write_test(['a', 1, 'p,q', 'r"s', 'x!y'],
'a,1,"p,q","r""s",x!y',
quotechar='"')

def test_write_delimiter(self):
self._write_test(['a', 1, 'p,q', 'x;y'], 'a,1,"p,q",x;y')
self._write_test(['a', 1, 'p;q', 'x,y'], 'a;1;"p;q";x,y', delimiter=';')
self._write_test(['a', 1, 'p\0q', 'x,y'], 'a\x001\0"p\0q"\0x,y',
delimiter='\0')
self._write_test(['a', 1, 'p🍌q', 'x🍍y'], 'a🍌1🍌"p🍌q"🍌x🍍y',
delimiter='🍌')

def test_write_escape(self):
self._write_test(['a',1,'p,q'], 'a,1,"p,q"',
Expand Down Expand Up @@ -258,19 +270,26 @@ def test_write_escape(self):
escapechar='\\', quoting=csv.QUOTE_MINIMAL)
self._write_test(['C\\', '6', '7', 'X"'], 'C\\\\,6,7,"X"""',
escapechar='\\', quoting=csv.QUOTE_MINIMAL)
# SYMBOL FOR ESCAPE
self._write_test(['a', 1, 'p,q', 'r\u241bs', 'x\u241ay'],
'a,1,p\u241b,q,r\u241b\u241bs,x\u241ay',
escapechar='\u241b', quoting=csv.QUOTE_NONE)

def test_write_lineterminator(self):
for lineterminator in '\r\n', '\n', '\r', '!@#', '\0':
for lineterminator in ('\r\n', '\n', '\r', '!@#', '\0',
'\x85', '\u2028', '\U0001f600'):
with self.subTest(lineterminator=lineterminator):
with StringIO() as sio:
writer = csv.writer(sio, lineterminator=lineterminator)
writer.writerow(['a', 'b'])
writer.writerow([1, 2])
writer.writerow(['\r', '\n'])
writer.writerow([f'a{lineterminator[-1]}b', 'c'])
self.assertEqual(sio.getvalue(),
f'a,b{lineterminator}'
f'1,2{lineterminator}'
f'"\r","\n"{lineterminator}')
f'"\r","\n"{lineterminator}'
f'"a{lineterminator[-1]}b",c{lineterminator}')

def test_write_iterable(self):
self._write_test(iter(['a', 1, 'p,q']), 'a,1,"p,q"')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Speed up :func:`csv.writer` by caching the set of characters that need
quoting or escaping in the dialect. Writing long fields is now up to 5 times
faster.
65 changes: 56 additions & 9 deletions Modules/_csv.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ typedef struct {
Py_UCS4 quotechar; /* quote character */
Py_UCS4 escapechar; /* escape character */
PyObject *lineterminator; /* string to write between records */

/* Cache for the writer: bit c is set if the ASCII character c needs
quoting or escaping (delimiter, quotechar, escapechar, '\r', '\n'
and the characters of lineterminator). */
uint64_t special_chars[2];
/* Whether any of the special characters is non-ASCII. */
bool nonascii_special;
} DialectObj;

typedef struct {
Expand Down Expand Up @@ -332,6 +337,54 @@ _set_str(const char *name, PyObject **target, PyObject *src, const char *dflt)
return 0;
}

static void
dialect_add_special_char(DialectObj *self, Py_UCS4 c)
{
if (c == NOT_SET) {
return;
}
if (c < 128) {
self->special_chars[c / 64] |= (uint64_t)1 << (c % 64);
}
else {
self->nonascii_special = true;
}
}

static void
dialect_init_special_chars_cache(DialectObj *self)
{
self->special_chars[0] = self->special_chars[1] = 0;
self->nonascii_special = false;
dialect_add_special_char(self, self->delimiter);
dialect_add_special_char(self, self->quotechar);
dialect_add_special_char(self, self->escapechar);
dialect_add_special_char(self, '\r');
dialect_add_special_char(self, '\n');
PyObject *lt = self->lineterminator;
for (Py_ssize_t i = 0; i < PyUnicode_GET_LENGTH(lt); i++) {
dialect_add_special_char(self, PyUnicode_READ_CHAR(lt, i));
}
}

/* Whether the character needs quoting or escaping by the writer. */
static inline bool
dialect_is_special_char(DialectObj *self, Py_UCS4 c)
{
if (c < 128) {
return (self->special_chars[c / 64] >> (c % 64)) & 1;
}
if (!self->nonascii_special) {
return false;
}
return (c == self->delimiter ||
c == self->quotechar ||
c == self->escapechar ||
PyUnicode_FindChar(self->lineterminator, c, 0,
PyUnicode_GET_LENGTH(self->lineterminator),
1) >= 0);
}

static int
dialect_check_quoting(int quoting)
{
Expand Down Expand Up @@ -558,6 +611,7 @@ dialect_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
goto err;
}
dialect_init_special_chars_cache(self);

ret = Py_NewRef(self);
err:
Expand Down Expand Up @@ -1208,14 +1262,7 @@ join_append_data(WriterObj *self, int field_kind, const void *field_data,
Py_UCS4 c = PyUnicode_READ(field_kind, field_data, i);
int want_escape = 0;

if (c == dialect->delimiter ||
c == dialect->escapechar ||
c == dialect->quotechar ||
c == '\n' ||
c == '\r' ||
PyUnicode_FindChar(
dialect->lineterminator, c, 0,
PyUnicode_GET_LENGTH(dialect->lineterminator), 1) >= 0) {
if (dialect_is_special_char(dialect, c)) {
if (dialect->quoting == QUOTE_NONE)
want_escape = 1;
else {
Expand Down
Loading