gh-110222: Add support of PyStructSequence in copy.replace() (GH-110223)
diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py
index 2ef1316..6aec63e 100644
--- a/Lib/test/test_structseq.py
+++ b/Lib/test/test_structseq.py
@@ -264,6 +264,84 @@ def test_match_args_with_unnamed_fields(self):
self.assertEqual(os.stat_result.n_unnamed_fields, 3)
self.assertEqual(os.stat_result.__match_args__, expected_args)
+ def test_copy_replace_all_fields_visible(self):
+ assert os.times_result.n_unnamed_fields == 0
+ assert os.times_result.n_sequence_fields == os.times_result.n_fields
+
+ t = os.times()
+
+ # visible fields
+ self.assertEqual(copy.replace(t), t)
+ self.assertIsInstance(copy.replace(t), os.times_result)
+ self.assertEqual(copy.replace(t, user=1.5), (1.5, *t[1:]))
+ self.assertEqual(copy.replace(t, system=2.5), (t[0], 2.5, *t[2:]))
+ self.assertEqual(copy.replace(t, user=1.5, system=2.5), (1.5, 2.5, *t[2:]))
+
+ # unknown fields
+ with self.assertRaisesRegex(TypeError, 'unexpected field name'):
+ copy.replace(t, error=-1)
+ with self.assertRaisesRegex(TypeError, 'unexpected field name'):
+ copy.replace(t, user=1, error=-1)
+
+ def test_copy_replace_with_invisible_fields(self):
+ assert time.struct_time.n_unnamed_fields == 0
+ assert time.struct_time.n_sequence_fields < time.struct_time.n_fields
+
+ t = time.gmtime(0)
+
+ # visible fields
+ t2 = copy.replace(t)
+ self.assertEqual(t2, (1970, 1, 1, 0, 0, 0, 3, 1, 0))
+ self.assertIsInstance(t2, time.struct_time)
+ t3 = copy.replace(t, tm_year=2000)
+ self.assertEqual(t3, (2000, 1, 1, 0, 0, 0, 3, 1, 0))
+ self.assertEqual(t3.tm_year, 2000)
+ t4 = copy.replace(t, tm_mon=2)
+ self.assertEqual(t4, (1970, 2, 1, 0, 0, 0, 3, 1, 0))
+ self.assertEqual(t4.tm_mon, 2)
+ t5 = copy.replace(t, tm_year=2000, tm_mon=2)
+ self.assertEqual(t5, (2000, 2, 1, 0, 0, 0, 3, 1, 0))
+ self.assertEqual(t5.tm_year, 2000)
+ self.assertEqual(t5.tm_mon, 2)
+
+ # named invisible fields
+ self.assertTrue(hasattr(t, 'tm_zone'), f"{t} has no attribute 'tm_zone'")
+ with self.assertRaisesRegex(AttributeError, 'readonly attribute'):
+ t.tm_zone = 'some other zone'
+ self.assertEqual(t2.tm_zone, t.tm_zone)
+ self.assertEqual(t3.tm_zone, t.tm_zone)
+ self.assertEqual(t4.tm_zone, t.tm_zone)
+ t6 = copy.replace(t, tm_zone='some other zone')
+ self.assertEqual(t, t6)
+ self.assertEqual(t6.tm_zone, 'some other zone')
+ t7 = copy.replace(t, tm_year=2000, tm_zone='some other zone')
+ self.assertEqual(t7, (2000, 1, 1, 0, 0, 0, 3, 1, 0))
+ self.assertEqual(t7.tm_year, 2000)
+ self.assertEqual(t7.tm_zone, 'some other zone')
+
+ # unknown fields
+ with self.assertRaisesRegex(TypeError, 'unexpected field name'):
+ copy.replace(t, error=2)
+ with self.assertRaisesRegex(TypeError, 'unexpected field name'):
+ copy.replace(t, tm_year=2000, error=2)
+ with self.assertRaisesRegex(TypeError, 'unexpected field name'):
+ copy.replace(t, tm_zone='some other zone', error=2)
+
+ def test_copy_replace_with_unnamed_fields(self):
+ assert os.stat_result.n_unnamed_fields > 0
+
+ r = os.stat_result(range(os.stat_result.n_sequence_fields))
+
+ error_message = re.escape('__replace__() is not supported')
+ with self.assertRaisesRegex(TypeError, error_message):
+ copy.replace(r)
+ with self.assertRaisesRegex(TypeError, error_message):
+ copy.replace(r, st_mode=1)
+ with self.assertRaisesRegex(TypeError, error_message):
+ copy.replace(r, error=2)
+ with self.assertRaisesRegex(TypeError, error_message):
+ copy.replace(r, st_mode=1, error=2)
+
if __name__ == "__main__":
unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2023-10-02-15-07-28.gh-issue-110222.zl_oHh.rst b/Misc/NEWS.d/next/Library/2023-10-02-15-07-28.gh-issue-110222.zl_oHh.rst
new file mode 100644
index 0000000..fd2ecdf
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-10-02-15-07-28.gh-issue-110222.zl_oHh.rst
@@ -0,0 +1,2 @@
+Add support of struct sequence objects in :func:`copy.replace`.
+Patched by Xuehai Pan.
diff --git a/Objects/structseq.c b/Objects/structseq.c
index 2c98288..e4a4b45 100644
--- a/Objects/structseq.c
+++ b/Objects/structseq.c
@@ -8,6 +8,7 @@
*/
#include "Python.h"
+#include "pycore_dict.h" // _PyDict_Pop()
#include "pycore_tuple.h" // _PyTuple_FromArray()
#include "pycore_object.h" // _PyObject_GC_TRACK()
@@ -380,9 +381,82 @@ structseq_reduce(PyStructSequence* self, PyObject *Py_UNUSED(ignored))
return NULL;
}
+
+static PyObject *
+structseq_replace(PyStructSequence *self, PyObject *args, PyObject *kwargs)
+{
+ PyStructSequence *result = NULL;
+ Py_ssize_t n_fields, n_unnamed_fields, i;
+
+ if (!_PyArg_NoPositional("__replace__", args)) {
+ return NULL;
+ }
+
+ n_fields = REAL_SIZE(self);
+ if (n_fields < 0) {
+ return NULL;
+ }
+ n_unnamed_fields = UNNAMED_FIELDS(self);
+ if (n_unnamed_fields < 0) {
+ return NULL;
+ }
+ if (n_unnamed_fields > 0) {
+ PyErr_Format(PyExc_TypeError,
+ "__replace__() is not supported for %.500s "
+ "because it has unnamed field(s)",
+ Py_TYPE(self)->tp_name);
+ return NULL;
+ }
+
+ result = (PyStructSequence *) PyStructSequence_New(Py_TYPE(self));
+ if (!result) {
+ return NULL;
+ }
+
+ if (kwargs != NULL) {
+ // We do not support types with unnamed fields, so we can iterate over
+ // i >= n_visible_fields case without slicing with (i - n_unnamed_fields).
+ for (i = 0; i < n_fields; ++i) {
+ PyObject *key = PyUnicode_FromString(Py_TYPE(self)->tp_members[i].name);
+ if (!key) {
+ goto error;
+ }
+ PyObject *ob = _PyDict_Pop(kwargs, key, self->ob_item[i]);
+ Py_DECREF(key);
+ if (!ob) {
+ goto error;
+ }
+ result->ob_item[i] = ob;
+ }
+ // Check if there are any unexpected fields.
+ if (PyDict_GET_SIZE(kwargs) > 0) {
+ PyObject *names = PyDict_Keys(kwargs);
+ if (names) {
+ PyErr_Format(PyExc_TypeError, "Got unexpected field name(s): %R", names);
+ Py_DECREF(names);
+ }
+ goto error;
+ }
+ }
+ else
+ {
+ // Just create a copy of the original.
+ for (i = 0; i < n_fields; ++i) {
+ result->ob_item[i] = Py_NewRef(self->ob_item[i]);
+ }
+ }
+
+ return (PyObject *)result;
+
+error:
+ Py_DECREF(result);
+ return NULL;
+}
+
static PyMethodDef structseq_methods[] = {
{"__reduce__", (PyCFunction)structseq_reduce, METH_NOARGS, NULL},
- {NULL, NULL}
+ {"__replace__", _PyCFunction_CAST(structseq_replace), METH_VARARGS | METH_KEYWORDS, NULL},
+ {NULL, NULL} // sentinel
};
static Py_ssize_t