Skip to content

GH-46145: make PyStructSequence compatible with namedtuple #108648

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

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions Doc/c-api/tuple.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_structseq.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import textwrap
import time
import unittest
import _testcapi
from test.support import script_helper


Expand Down Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line.

assert os.times_result.n_unnamed_fields == 0
assert os.times_result.n_sequence_fields == os.times_result.n_fields
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
22 changes: 22 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3975,13 +3975,28 @@ 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
*/

PyMODINIT_FUNC
PyInit__testcapi(void)
{
PyObject *m;
PyTypeObject *type;

m = PyModule_Create(&_testcapimodule);
if (m == NULL)
Expand Down Expand Up @@ -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;
Expand Down
112 changes: 111 additions & 1 deletion Objects/structseq.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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
};

Expand All @@ -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 { \
Expand Down Expand Up @@ -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;
}
Expand Down
Loading