Skip to content

gh-110222: Add support of PyStructSequence in copy.replace() #110223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6af4757
Add support of PyStructSequence in copy.replace()
XuehaiPan Oct 2, 2023
1960d8d
Correct error message for `namedtuple._replace`
XuehaiPan Oct 2, 2023
5ee97a8
📜🤖 Added by blurb_it.
blurb-it[bot] Oct 2, 2023
e8e6be4
Add test for `copy.replace()` for struct sequence objects
XuehaiPan Oct 2, 2023
43324a4
Move test for `copy.replace(structseq)` to test_structseq.py
XuehaiPan Oct 2, 2023
9e6cd98
Fix refcnt for temp variables
XuehaiPan Oct 2, 2023
350a3d0
Change copy.replace() to raise TypeError for unkown fields for named …
XuehaiPan Oct 2, 2023
c4655ca
Make copy.replace() to raise TypeError for PyStructSequence with unna…
XuehaiPan Oct 2, 2023
a9dba93
Revert copy.replace() to raise ValueError for unkown fields for named…
XuehaiPan Oct 2, 2023
83fd01b
Change comment in tests
XuehaiPan Oct 2, 2023
73e5e65
Add more tests for PyStructSequence with invisible fields
XuehaiPan Oct 2, 2023
7abaf64
Change test function names to match code style
XuehaiPan Oct 2, 2023
26eedbc
Refactor implementation with copy and replacement
XuehaiPan Oct 2, 2023
d74b449
Fix missing assertHasAttr
XuehaiPan Oct 2, 2023
5186c35
Fix test cases
XuehaiPan Oct 2, 2023
0f74efa
Fix error handling
XuehaiPan Oct 2, 2023
43a81b0
Remove unused variable
XuehaiPan Oct 2, 2023
d90e5ae
Cast return type to `PyObject*`
XuehaiPan Oct 2, 2023
287f8b0
Merge branch 'main' into copy-replace-structseq
XuehaiPan Oct 2, 2023
5722773
Update news entry
XuehaiPan Oct 2, 2023
c23165b
Prefer ++i over i++
XuehaiPan Oct 2, 2023
38f331b
Limit the length of typename to 500 in error message
XuehaiPan Oct 3, 2023
30758ba
Merge branch 'main' into copy-replace-structseq
XuehaiPan Oct 3, 2023
2a6259c
Prefer PyDict_GET_SIZE over PyDict_Size
XuehaiPan Oct 3, 2023
a6b0292
Apply suggestions from code review
XuehaiPan Oct 3, 2023
d76bb4e
Apply suggestions from code review
XuehaiPan Oct 3, 2023
3ae391f
Update test code style
XuehaiPan Oct 3, 2023
3664109
Merge branch 'main' into copy-replace-structseq
XuehaiPan Oct 3, 2023
4a606ad
Apply suggestions from code review
serhiy-storchaka Oct 3, 2023
8504345
Merge branch 'main' into copy-replace-structseq
XuehaiPan Oct 3, 2023
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
79 changes: 79 additions & 0 deletions Lib/test/test_structseq.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import copy
import os
import pickle
import re
import time
import unittest

Expand Down Expand Up @@ -204,6 +205,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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add support of struct sequence objects in :func:`copy.replace`.
Patched by Xuehai Pan.
76 changes: 75 additions & 1 deletion Objects/structseq.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -365,9 +366,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
Expand Down