Skip to content

Commit 452f2b0

Browse files
authored
csv: apply dialect quoting and lineterminator in writer (#8254)
* csv: map QUOTE_MINIMAL to Necessary quote style QuoteStyle::Minimal was mapped to csv_core's Always, which quoted every field even under QUOTE_MINIMAL. Map it to Necessary so only fields that require quoting are quoted. Unmarks Test_Csv.test_write_quoting. Assisted-by: Claude Code:claude-opus-4-8 * csv: apply dialect quoting and lineterminator in writer csv.writer ignored the quoting and lineterminator from a dialect (whether passed by name or object), always using the defaults. Resolve both from the dialect via get_quoting()/get_lineterminator(), mirroring get_delimiter(), so an explicit keyword argument still overrides the dialect. Unmarks TestDialectUnix.test_simple_writer. Assisted-by: Claude Code:claude-opus-4-8
1 parent 9d9fc27 commit 452f2b0

2 files changed

Lines changed: 45 additions & 7 deletions

File tree

Lib/test/test_csv.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ def test_write_bigfield(self):
213213
self._write_test([bigstring,bigstring], '%s,%s' % \
214214
(bigstring, bigstring))
215215

216-
@unittest.expectedFailure # TODO: RUSTPYTHON
217216
def test_write_quoting(self):
218217
self._write_test(['a',1,'p,q'], 'a,1,"p,q"')
219218
self._write_error_test(csv.Error, ['a',1,'p,q'],
@@ -863,7 +862,6 @@ def test_read_escape_fieldsep(self):
863862
class TestDialectUnix(TestCsvBase):
864863
dialect = 'unix'
865864

866-
@unittest.expectedFailure # TODO: RUSTPYTHON
867865
def test_simple_writer(self):
868866
self.writerAssertEqual([[1, 'abc def', 'abc']], '"1","abc def","abc"\n')
869867

crates/stdlib/src/csv.rs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ mod _csv {
462462
impl From<QuoteStyle> for csv_core::QuoteStyle {
463463
fn from(val: QuoteStyle) -> Self {
464464
match val {
465-
QuoteStyle::Minimal => Self::Always,
465+
QuoteStyle::Minimal => Self::Necessary,
466466
QuoteStyle::All => Self::Always,
467467
QuoteStyle::Nonnumeric => Self::NonNumeric,
468468
QuoteStyle::None => Self::Never,
@@ -796,6 +796,48 @@ mod _csv {
796796
delimiter
797797
}
798798

799+
fn get_lineterminator(&self) -> csv_core::Terminator {
800+
let mut lineterminator = match &self.dialect {
801+
DialectItem::Str(name) => {
802+
let g = GLOBAL_HASHMAP.lock();
803+
if let Some(dialect) = g.get(name) {
804+
dialect.lineterminator
805+
} else {
806+
Terminator::CRLF
807+
}
808+
}
809+
DialectItem::Obj(obj) => obj.lineterminator,
810+
_ => Terminator::CRLF,
811+
};
812+
813+
if let Some(attr) = self.lineterminator {
814+
lineterminator = attr
815+
}
816+
817+
lineterminator
818+
}
819+
820+
fn get_quoting(&self) -> QuoteStyle {
821+
let mut quoting = match &self.dialect {
822+
DialectItem::Str(name) => {
823+
let g = GLOBAL_HASHMAP.lock();
824+
if let Some(dialect) = g.get(name) {
825+
dialect.quoting
826+
} else {
827+
QuoteStyle::Minimal
828+
}
829+
}
830+
DialectItem::Obj(obj) => obj.quoting,
831+
_ => QuoteStyle::Minimal,
832+
};
833+
834+
if let Some(attr) = self.quoting {
835+
quoting = attr
836+
}
837+
838+
quoting
839+
}
840+
799841
fn to_reader(&self) -> csv_core::Reader {
800842
let dialect = match &self.dialect {
801843
DialectItem::Str(name) => GLOBAL_HASHMAP.lock().get(name).copied(),
@@ -900,15 +942,13 @@ mod _csv {
900942
writer = writer.double_quote(t);
901943
}
902944

903-
writer = writer.terminator(self.lineterminator.unwrap_or(Terminator::CRLF));
945+
writer = writer.terminator(self.get_lineterminator());
904946

905947
if let Some(e) = self.escapechar {
906948
writer = writer.escape(e);
907949
}
908950

909-
if let Some(e) = self.quoting {
910-
writer = writer.quote_style(e.into());
911-
}
951+
writer = writer.quote_style(self.get_quoting().into());
912952

913953
writer.build()
914954
}

0 commit comments

Comments
 (0)