From 4b1d95eff57e95dcacc4aa0eff1c5f15aceda1b7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 1 Aug 2026 17:33:33 +0300 Subject: [PATCH 1/3] gh-155033: Support copy.replace() for csv dialects Add __replace__() to the _csv.Dialect type and to the csv.Dialect base class. Formatting parameters that are not replaced are inherited from the original dialect, and the result is validated like a newly created dialect. Co-Authored-By: Claude Opus 5 (1M context) --- Doc/library/csv.rst | 7 +++ Doc/whatsnew/3.16.rst | 5 ++ Lib/csv.py | 16 ++++++ Lib/test/test_csv.py | 55 +++++++++++++++++++ ...-08-01-12-00-00.gh-issue-155033.dR3pLc.rst | 2 + Modules/_csv.c | 25 +++++++++ 6 files changed, 110 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-01-12-00-00.gh-issue-155033.dR3pLc.rst diff --git a/Doc/library/csv.rst b/Doc/library/csv.rst index 81949261c563d0..33d22ffef1bbd7 100644 --- a/Doc/library/csv.rst +++ b/Doc/library/csv.rst @@ -528,6 +528,13 @@ Dialects support the following attributes: When ``True``, raise exception :exc:`Error` on bad CSV input. The default is ``False``. +Dialects support :func:`copy.replace`, +which returns a copy of the dialect +with the specified formatting parameters replaced. + +.. versionchanged:: next + Added support of :func:`copy.replace`. + .. _reader-objects: Reader Objects diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 6e69737768d5e1..beafeadabacf65 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -120,6 +120,11 @@ csv The results may differ from those of earlier Python versions. (Contributed by Serhiy Storchaka in :gh:`83273`.) +* CSV dialects now support :func:`copy.replace`, + which returns a copy of the dialect + with the specified formatting parameters replaced. + (Contributed by Serhiy Storchaka in :gh:`155033`.) + curses ------ diff --git a/Lib/csv.py b/Lib/csv.py index c66717dc1ee59e..0c5c40c759240c 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -82,6 +82,11 @@ class excel: "unix_dialect"] +_dialect_attributes = frozenset({ + 'delimiter', 'quotechar', 'escapechar', 'doublequote', + 'skipinitialspace', 'lineterminator', 'quoting', 'strict', +}) + class Dialect: """Describe a CSV dialect. @@ -113,6 +118,17 @@ def _validate(self): # Re-raise to get a traceback showing more user code. raise Error(str(e)) from None + def __replace__(self, /, **changes): + unexpected = changes.keys() - _dialect_attributes + if unexpected: + raise TypeError(f'__replace__() got an unexpected keyword ' + f'argument {min(unexpected)!r}') + new = object.__new__(self.__class__) + new.__dict__.update(self.__dict__) + new.__dict__.update(changes) + new._validate() + return new + class excel(Dialect): """Describe the usual properties of Excel-generated CSV files.""" delimiter = ',' diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index 91170cc16b3ac9..7a679d627ff8c0 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -707,6 +707,61 @@ def test_copy(self): dialect = csv.get_dialect(name) self.assertRaises(TypeError, copy.copy, dialect) + def test_replace(self): + dialect = csv.get_dialect('excel') + new = copy.replace(dialect, delimiter=';', strict=True) + self.assertIsInstance(new, type(dialect)) + self.assertEqual(new.delimiter, ';') + self.assertTrue(new.strict) + # Not replaced parameters are inherited from the original dialect. + self.assertEqual(new.quotechar, dialect.quotechar) + self.assertEqual(new.escapechar, dialect.escapechar) + self.assertEqual(new.lineterminator, dialect.lineterminator) + self.assertEqual(new.quoting, dialect.quoting) + self.assertEqual(new.doublequote, dialect.doublequote) + self.assertEqual(new.skipinitialspace, dialect.skipinitialspace) + # The original dialect is left unchanged. + self.assertEqual(dialect.delimiter, ',') + self.assertFalse(dialect.strict) + self.assertEqual(list(csv.reader(['a;b'], new)), [['a', 'b']]) + + self.assertIs(copy.replace(dialect), dialect) + self.assertRaises(TypeError, copy.replace, dialect, delimeter=';') + self.assertRaises(TypeError, copy.replace, dialect, delimiter=';;') + self.assertRaises(TypeError, dialect.__replace__, dialect) + + def test_replace_dialect_subclass(self): + class mydialect(csv.Dialect): + delimiter = ";" + quotechar = '"' + doublequote = False + skipinitialspace = True + lineterminator = '\r\n' + quoting = csv.QUOTE_ALL + + dialect = mydialect() + new = copy.replace(dialect, delimiter=':', quoting=csv.QUOTE_MINIMAL) + self.assertIsInstance(new, mydialect) + self.assertEqual(new.delimiter, ':') + self.assertEqual(new.quoting, csv.QUOTE_MINIMAL) + # Not replaced parameters are inherited from the original dialect. + self.assertEqual(new.quotechar, '"') + self.assertEqual(new.escapechar, None) + self.assertEqual(new.lineterminator, '\r\n') + self.assertFalse(new.doublequote) + self.assertTrue(new.skipinitialspace) + # The original dialect is left unchanged. + self.assertEqual(dialect.delimiter, ';') + self.assertEqual(dialect.quoting, csv.QUOTE_ALL) + self.assertEqual(list(csv.reader(['a:b'], new)), [['a', 'b']]) + # "strict" is supported even if it is not set on the class. + self.assertTrue(copy.replace(dialect, strict=True).strict) + + with self.assertRaises(csv.Error): + copy.replace(dialect, delimiter='::') + with self.assertRaisesRegex(TypeError, "'delimeter'"): + copy.replace(dialect, delimeter=':') + def test_pickle(self): for name in csv.list_dialects(): dialect = csv.get_dialect(name) diff --git a/Misc/NEWS.d/next/Library/2026-08-01-12-00-00.gh-issue-155033.dR3pLc.rst b/Misc/NEWS.d/next/Library/2026-08-01-12-00-00.gh-issue-155033.dR3pLc.rst new file mode 100644 index 00000000000000..9100d7be447e41 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-01-12-00-00.gh-issue-155033.dR3pLc.rst @@ -0,0 +1,2 @@ +CSV dialects (instances of :class:`csv.Dialect` subclasses and dialect +objects returned by :func:`csv.get_dialect`) now support :func:`copy.replace`. diff --git a/Modules/_csv.c b/Modules/_csv.c index a7fcc78e058f05..c640f2d36a8464 100644 --- a/Modules/_csv.c +++ b/Modules/_csv.c @@ -586,9 +586,34 @@ Dialect_reduce(PyObject *self, PyObject *args) { return NULL; } +PyDoc_STRVAR(dialect_replace_doc, +"__replace__($self, /, **changes)\n" +"--\n" +"\n" +"Return a copy of the dialect with the specified options replaced."); + +static PyObject * +Dialect_replace(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (PyTuple_GET_SIZE(args) != 0) { + PyErr_SetString(PyExc_TypeError, + "__replace__() takes no positional arguments"); + return NULL; + } + PyObject *newargs = PyTuple_Pack(1, self); + if (newargs == NULL) { + return NULL; + } + PyObject *result = dialect_new(Py_TYPE(self), newargs, kwargs); + Py_DECREF(newargs); + return result; +} + static struct PyMethodDef dialect_methods[] = { {"__reduce__", Dialect_reduce, METH_VARARGS, dialect_reduce_doc}, {"__reduce_ex__", Dialect_reduce, METH_VARARGS, dialect_reduce_doc}, + {"__replace__", _PyCFunction_CAST(Dialect_replace), + METH_VARARGS | METH_KEYWORDS, dialect_replace_doc}, {NULL, NULL} }; From 0a3eeb5fa4c9f84cad8de93274c76a6e4fa3e56d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 1 Aug 2026 18:51:44 +0300 Subject: [PATCH 2/3] Remove the What's New entry The change is too minor for What's New. Co-Authored-By: Claude Opus 5 (1M context) --- Doc/whatsnew/3.16.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index beafeadabacf65..6e69737768d5e1 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -120,11 +120,6 @@ csv The results may differ from those of earlier Python versions. (Contributed by Serhiy Storchaka in :gh:`83273`.) -* CSV dialects now support :func:`copy.replace`, - which returns a copy of the dialect - with the specified formatting parameters replaced. - (Contributed by Serhiy Storchaka in :gh:`155033`.) - curses ------ From 4e1a526156dbc3422b570315b8c8516b3170089f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 1 Aug 2026 21:48:09 +0300 Subject: [PATCH 3/3] Say "support for" instead of "support of" Co-Authored-By: Claude Opus 5 (1M context) --- Doc/library/csv.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/csv.rst b/Doc/library/csv.rst index 33d22ffef1bbd7..c3e02f21d31891 100644 --- a/Doc/library/csv.rst +++ b/Doc/library/csv.rst @@ -533,7 +533,7 @@ which returns a copy of the dialect with the specified formatting parameters replaced. .. versionchanged:: next - Added support of :func:`copy.replace`. + Added support for :func:`copy.replace`. .. _reader-objects: