diff --git a/Doc/c-api/tuple.rst b/Doc/c-api/tuple.rst index 815afddad19df1..09208cf38f3ccc 100644 --- a/Doc/c-api/tuple.rst +++ b/Doc/c-api/tuple.rst @@ -140,6 +140,10 @@ objects, i.e. a sequence whose items can also be accessed through attributes. To create a struct sequence, you first have to create a specific struct sequence type. +.. c:var:: PyTypeObject PyStructSequence + + This instance of :c:type:`PyTypeObject` represents the struct sequence type. + .. c:function:: PyTypeObject* PyStructSequence_NewType(PyStructSequence_Desc *desc) Create a new struct sequence type from the data in *desc*, described below. Instances diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index d0bc0bd7b61520..3c60aad2ddc09b 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -5,6 +5,7 @@ import textwrap import time import unittest +import _testcapi from test.support import script_helper @@ -266,6 +267,35 @@ 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_tuple_field_keys(self): + expected_keys = ('tm_year', 'tm_mon', 'tm_mday', 'tm_hour', 'tm_min', + 'tm_sec', 'tm_wday', 'tm_yday', 'tm_isdst') + self.assertEqual(time.gmtime()._fields, time.struct_time._fields, expected_keys) + + def test_tuple_field_defaults(self): + self.assertEqual(time.gmtime()._field_defaults, time.struct_time._field_defaults, {}) + + def test_tuple_asdict(self): + t = time.gmtime(0) + self.assertEqual(t._asdict(), { + 'tm_year': 1970, + 'tm_mon': 1, + 'tm_mday': 1, + 'tm_hour': 0, + 'tm_min': 0, + 'tm_sec': 0, + 'tm_wday': 3, + 'tm_yday': 1, + 'tm_isdst': 0, + }) + + def test_tuple_attributes_unnamed(self): + for key in ['_fields', '_field_defaults']: + self.assertNotIn(key, dir(_testcapi.MixedNamedTuple)) + + def test_asdict_unnamed(self): + t = _testcapi.MixedNamedTuple((0, 1)) + self.assertRaises(ValueError, t._asdict) 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 diff --git a/Misc/NEWS.d/next/C_API/2023-08-29-17-27-01.gh-issue-108647.gyXLmO.rst b/Misc/NEWS.d/next/C_API/2023-08-29-17-27-01.gh-issue-108647.gyXLmO.rst new file mode 100644 index 00000000000000..792f7bc74f70c8 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2023-08-29-17-27-01.gh-issue-108647.gyXLmO.rst @@ -0,0 +1,3 @@ +Add :attr:`collections.somenamedtuple._fields` and +:attr:`collections.somenamedtuple._asdict` helpers to :c:var:`PyStructSequence`, +improving its compatibility with :class:`collections.namedtuple`. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 26f68691e44f83..f80fd725e5fee6 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3975,6 +3975,20 @@ static struct PyModuleDef _testcapimodule = { .m_methods = TestMethods, }; +static PyStructSequence_Field MixedNamedTuple_fields[] = { + {"something", "some named entry"}, + {NULL, "some unnamed entry"}, + {0} +}; + +static PyStructSequence_Desc MixedNamedTuple_desc = { + .name = "_testcpi.MixedNamedTuple", + .doc = PyDoc_STR("Example of a named tuple with both named and unnamed fields."), + .fields = MixedNamedTuple_fields, + .n_in_sequence = 2 +}; + + /* Per PEP 489, this module will not be converted to multi-phase initialization */ @@ -3982,6 +3996,7 @@ PyMODINIT_FUNC PyInit__testcapi(void) { PyObject *m; + PyTypeObject *type; m = PyModule_Create(&_testcapimodule); if (m == NULL) @@ -4101,6 +4116,13 @@ PyInit__testcapi(void) (PyObject *) &ContainerNoGC_type) < 0) return NULL; + MixedNamedTuple_desc.fields[1].name = PyStructSequence_UnnamedField; + type = PyStructSequence_NewType(&MixedNamedTuple_desc); + if (type == NULL) + return NULL; + if (PyModule_AddType(m, type) < 0) + return NULL; + /* Include tests from the _testcapi/ directory */ if (_PyTestCapi_Init_Vectorcall(m) < 0) { return NULL; diff --git a/Objects/structseq.c b/Objects/structseq.c index 56a7851b98788d..e816c8480dd454 100644 --- a/Objects/structseq.c +++ b/Objects/structseq.c @@ -18,6 +18,8 @@ static const char visible_length_key[] = "n_sequence_fields"; static const char real_length_key[] = "n_fields"; static const char unnamed_fields_key[] = "n_unnamed_fields"; static const char match_args_key[] = "__match_args__"; +static const char named_fields_list_key[] = "_fields"; +static const char named_fields_defaults_key[] = "_field_defaults"; /* Fields with this name have only a field index, not a field name. They are only allowed for indices < n_visible_fields. */ @@ -445,11 +447,101 @@ structseq_replace(PyStructSequence *self, PyObject *args, PyObject *kwargs) return NULL; } +static PyObject * +structseq_asdict(PyStructSequence* self, PyObject *Py_UNUSED(ignored)) +{ + PyObject* dict = NULL; + Py_ssize_t n_visible_fields, n_unnamed_fields, i; + + n_visible_fields = VISIBLE_SIZE(self); + if (n_visible_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, + "_asdict() is not supported for %.500s " + "because it has unnamed field(s)", + Py_TYPE(self)->tp_name); + return NULL; + } + + dict = PyDict_New(); + if (!dict) + return NULL; + + for (i = 0; i < n_visible_fields; i++) { + const char *n = Py_TYPE(self)->tp_members[i-n_unnamed_fields].name; + if (PyDict_SetItemString(dict, n, self->ob_item[i]) < 0) + goto error; + } + + return dict; + +error: + Py_DECREF(dict); + return NULL; +} + +static PyObject * +structseq_make(PyStructSequence *self, PyObject *iterable) +{ + Py_ssize_t field_index = 0; + PyStructSequence *result = NULL; + + Py_ssize_t n_fields = REAL_SIZE(self); + if (n_fields < 0) { + return NULL; + } + + PyObject *values = PySequence_List(iterable); + if (values == NULL) { + return NULL; + } + + Py_ssize_t values_len = PyList_Size(values); + if (values_len != n_fields) { + PyErr_Format(PyExc_TypeError, "Expected %d arguments, got %d", + n_fields, values_len); + goto error; + } + + result = (PyStructSequence *) PyStructSequence_New(Py_TYPE(self)); + if (!result) { + goto error; + } + + for (field_index = 0; field_index < n_fields; ++field_index) { + PyObject *item = PyList_GetItemRef(values, field_index); + if (item == NULL) { + goto error; + } + result->ob_item[field_index] = item; + } + + return (PyObject *)result; + +error: + for (Py_ssize_t i = 0; i < field_index; ++i) { + Py_DECREF(result->ob_item[i]); + } + Py_DECREF(values); + Py_XDECREF(result); + return NULL; +} + static PyMethodDef structseq_methods[] = { {"__reduce__", (PyCFunction)structseq_reduce, METH_NOARGS, NULL}, {"__replace__", _PyCFunction_CAST(structseq_replace), METH_VARARGS | METH_KEYWORDS, PyDoc_STR("__replace__($self, /, **changes)\n--\n\n" "Return a copy of the structure with new values for the specified fields.")}, + {"_replace", _PyCFunction_CAST(structseq_replace), METH_VARARGS | METH_KEYWORDS}, + {"_asdict", (PyCFunction)structseq_asdict, METH_NOARGS, NULL}, + {"_make", (PyCFunction)structseq_make, METH_O, NULL}, {NULL, NULL} // sentinel }; @@ -469,7 +561,7 @@ count_members(PyStructSequence_Desc *desc, Py_ssize_t *n_unnamed_members) { static int initialize_structseq_dict(PyStructSequence_Desc *desc, PyObject* dict, Py_ssize_t n_members, Py_ssize_t n_unnamed_members) { - PyObject *v; + PyObject *v, *defaults = NULL; #define SET_DICT_FROM_SIZE(key, value) \ do { \ @@ -515,10 +607,28 @@ initialize_structseq_dict(PyStructSequence_Desc *desc, PyObject* dict, goto error; } + // Set _field and _field_defaults when we have no unnammed members + if (n_unnamed_members == 0) { + if (PyDict_SetItemString(dict, named_fields_list_key, keys) < 0) { + goto error; + } + + // Set _field_defaults to an empty dict, as we don't support defaults + defaults = PyDict_New(); + if (!defaults) { + goto error; + } + if (PyDict_SetItemString(dict, named_fields_defaults_key, defaults) < 0) { + goto error; + } + Py_DECREF(defaults); + } + Py_DECREF(keys); return 0; error: + Py_XDECREF(defaults); Py_DECREF(keys); return -1; }