Skip to content
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
101 changes: 100 additions & 1 deletion Lib/test/test_structseq.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import textwrap
import time
import unittest
from test.support import script_helper
from test.support import import_helper, script_helper


class StructSeqTest(unittest.TestCase):
Expand Down Expand Up @@ -48,6 +48,105 @@ def test_repr(self):
self.assertIn("st_ino=", rep)
self.assertIn("st_dev=", rep)

def test_repr_with_unnamed_fields(self):
# gh-154387: each unnamed field is shown at its own index rather than
# borrowing a neighbour's name. Unnamed fields need not be contiguous or
# trailing.
_testcapi = import_helper.import_module("_testcapi")
cls = _testcapi.structseq_newtype_interspersed_unnamed()
self.assertEqual(cls.n_fields, 5)
self.assertEqual(cls.n_sequence_fields, 4)
self.assertEqual(cls.n_unnamed_fields, 2)
self.assertEqual(cls.__match_args__, ("first", "third"))

t1 = cls(range(5))
self.assertEqual(repr(t1),
"_testcapi.Interspersed(first=0, <unnamed@1>=1, third=2, "
"<unnamed@3>=3)")
# Only the visible fields form the tuple; the hidden "fifth" is excluded.
self.assertEqual(tuple(t1), (0, 1, 2, 3))
self.assertEqual(len(t1), 4)
# Named fields are accessible by attribute, including the hidden one.
self.assertEqual(t1.first, 0)
self.assertEqual(t1.third, 2)
self.assertEqual(t1.fifth, 4)

t2 = cls(range(4))
self.assertEqual(repr(t2),
"_testcapi.Interspersed(first=0, <unnamed@1>=1, third=2, "
"<unnamed@3>=3)")
# Only the visible fields form the tuple; the hidden "fifth" is excluded.
self.assertEqual(tuple(t2), (0, 1, 2, 3))
self.assertEqual(len(t2), 4)
# Named fields are accessible by attribute, including the hidden one.
# The hidden "fifth" is not set, so it returns None.
self.assertEqual(t2.first, 0)
self.assertEqual(t2.third, 2)
self.assertIsNone(t2.fifth)

def test_os_stat_result_has_custom_repr(self):
# gh-154387: os.stat_result has a custom repr (statresult_repr) showing
# the float times st_atime/st_mtime/st_ctime by name, not the unnamed
# integer slots with generic "<unnamed@N>".
self.assertEqual(os.stat_result.n_sequence_fields, 10)
self.assertEqual(os.stat_result.n_unnamed_fields, 3)

# All fields supplied: st_atime/st_mtime/st_ctime are the float slots.
r1 = os.stat_result(range(os.stat_result.n_fields))
rep = repr(r1)
self.assertEqual(rep,
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
"st_uid=4, st_gid=5, st_size=6, st_atime=10, st_mtime=11, "
"st_ctime=12)")
self.assertNotIn("<unnamed@", rep)
self.assertEqual(r1.st_atime, 10)
self.assertEqual(r1.st_mtime, 11)
self.assertEqual(r1.st_ctime, 12)

# Only the visible slots supplied: statresult_new fills the float times
# from the integer slots, so the repr shows those filled values.
r2 = os.stat_result(range(10))
self.assertEqual(repr(r2),
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
"st_uid=4, st_gid=5, st_size=6, st_atime=7, st_mtime=8, "
"st_ctime=9)")
self.assertEqual(r2.st_atime, 7)
self.assertEqual(r2.st_mtime, 8)
self.assertEqual(r2.st_ctime, 9)

# The st_atime/st_mtime slots are supplied, but st_ctime is not; the
# st_ctime float slot is filled from the st_ctime integer slot.
r3 = os.stat_result(range(12))
self.assertEqual(repr(r3),
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
"st_uid=4, st_gid=5, st_size=6, st_atime=10, st_mtime=11, "
"st_ctime=9)")
self.assertEqual(r3.st_atime, 10)
self.assertEqual(r3.st_mtime, 11)
self.assertEqual(r3.st_ctime, 9)

# The st_mtime slot is supplied, but st_atime/st_ctime are not; the
# st_atime/st_ctime float slots are filled from the st_atime/st_ctime
# integer slots.
r4 = os.stat_result(range(10), {'st_mtime': -1.0})
self.assertEqual(repr(r4),
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
"st_uid=4, st_gid=5, st_size=6, st_atime=7, st_mtime=-1.0, "
"st_ctime=9)")
self.assertEqual(r4.st_atime, 7)
self.assertEqual(r4.st_mtime, -1.0)
self.assertEqual(r4.st_ctime, 9)

# repr(), __repr__(), and str() must all resolve to the custom repr
# (a raw tp_repr override left __repr__() diverging).
for r in (r1, r2, r3, r4):
rep = repr(r)
self.assertEqual(r.__repr__(), rep)
self.assertEqual(type(r).__repr__(r), rep)
self.assertEqual(str(r), rep)
self.assertEqual(f"{r!r}", rep)
self.assertEqual(f"{r!s}", rep)

def test_concat(self):
t1 = time.gmtime()
t2 = t1 + tuple(t1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix the :func:`repr` of struct sequence objects that contain unnamed fields,
such as :class:`os.stat_result`, whose integer time slots previously borrowed
the ``st_atime``/``st_mtime``/``st_ctime`` names of separate hidden float
fields. Unnamed fields are now shown as ``<unnamed@N>`` (``N`` is the sequence
index); :class:`os.stat_result` uses a dedicated repr that shows the float
``st_atime``, ``st_mtime`` and ``st_ctime`` fields by name. Patch by Xuehai Pan.
24 changes: 24 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,28 @@ test_structseq_newtype_null_descr_doc(PyObject *Py_UNUSED(self),
Py_RETURN_NONE;
}

static PyObject *
structseq_newtype_interspersed_unnamed(PyObject *Py_UNUSED(self),
PyObject *Py_UNUSED(args))
{
// gh-154387: unnamed fields may appear anywhere among the visible fields
// (here at indices 1 and 3), and a named field may be hidden (here "fifth"
// at index 4, since n_in_sequence is 4).
PyStructSequence_Field descr_fields[] = {
{"first", "field 0"},
{PyStructSequence_UnnamedField, "field 1"},
{"third", "field 2"},
{PyStructSequence_UnnamedField, "field 3"},
{"fifth", "field 4"},
{NULL, NULL},
};
PyStructSequence_Desc descr = {
"_testcapi.Interspersed", NULL, descr_fields, 4,
};

return (PyObject *)PyStructSequence_NewType(&descr);
}

typedef struct {
PyThread_type_lock start_event;
PyThread_type_lock exit_event;
Expand Down Expand Up @@ -3007,6 +3029,8 @@ static PyMethodDef TestMethods[] = {
test_structseq_newtype_doesnt_leak, METH_NOARGS},
{"test_structseq_newtype_null_descr_doc",
test_structseq_newtype_null_descr_doc, METH_NOARGS},
{"structseq_newtype_interspersed_unnamed",
structseq_newtype_interspersed_unnamed, METH_NOARGS},
{"pyobject_repr_from_null", pyobject_repr_from_null, METH_NOARGS},
{"pyobject_str_from_null", pyobject_str_from_null, METH_NOARGS},
{"pyobject_bytes_from_null", pyobject_bytes_from_null, METH_NOARGS},
Expand Down
73 changes: 73 additions & 0 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2572,6 +2572,8 @@ statresult_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
/* If we have been initialized from a tuple,
st_?time might be set to None. Initialize it
from the int slots. */
/* See also statresult_repr() below, which assumes that the float slots are
always initialized. */
for (i = 7; i <= 9; i++) {
if (result->ob_item[i+3] == Py_None) {
Py_DECREF(Py_None);
Expand All @@ -2581,6 +2583,63 @@ statresult_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
return (PyObject*)result;
}

static PyObject *
statresult_repr(PyObject *op, PyObject *Py_UNUSED(ignored))
{
// gh-154387: Show the unnamed integer slots 7-9 with following named float
// slots st_atime/st_mtime/st_ctime instead of the generic "<unnamed@N>".
// The custom statresult_new() above always initializes the float slots.
// So it is safe to use the float slots for the repr().
PyTypeObject *type = Py_TYPE(op);
PyUnicodeWriter *writer = PyUnicodeWriter_Create(128);
if (writer == NULL) {
return NULL;
}
if (PyUnicodeWriter_WriteUTF8(writer, type->tp_name, -1) < 0) {
goto error;
}
if (PyUnicodeWriter_WriteChar(writer, '(') < 0) {
goto error;
}
// The first 10 packed members: st_mode..st_size and the 3 float times
// st_atime/st_mtime/st_ctime (tp_members omits the unnamed slots 7-9).
// NB: This approach assumes the 3 unnamed slots are at the end of the
// visible members, which is true for the current stat_result_desc.
for (Py_ssize_t i = 0; i < 10; i++) {
if (i > 0 && PyUnicodeWriter_WriteUTF8(writer, ", ", 2) < 0) {
goto error;
}
PyMemberDef *member = &type->tp_members[i];
if (PyUnicodeWriter_WriteUTF8(writer, member->name, -1) < 0) {
goto error;
}
if (PyUnicodeWriter_WriteChar(writer, '=') < 0) {
goto error;
}
PyObject *value = PyMember_GetOne((const char *)op, member);
if (value == NULL) {
goto error;
}
if (PyUnicodeWriter_WriteRepr(writer, value) < 0) {
Py_DECREF(value);
goto error;
}
Py_DECREF(value);
}
if (PyUnicodeWriter_WriteChar(writer, ')') < 0) {
goto error;
}
return PyUnicodeWriter_Finish(writer);

error:
PyUnicodeWriter_Discard(writer);
return NULL;
}

static PyMethodDef statresult_repr_methoddef = {
"__repr__", statresult_repr, METH_NOARGS, NULL,
};

static int
_posix_clear(PyObject *module)
{
Expand Down Expand Up @@ -18875,6 +18934,20 @@ posixmodule_exec(PyObject *m)
}
state->statresult_new_orig = ((PyTypeObject *)state->StatResultType)->tp_new;
((PyTypeObject *)state->StatResultType)->tp_new = statresult_new;
// Install __repr__ through the type dict so repr() and st.__repr__()
// resolve to the same function; a raw type->tp_repr assignment leaves the
// stale __repr__ wrapper which is readied by PyStructSequence_NewType.
PyObject *statresult_repr_descr = PyDescr_NewMethod(
(PyTypeObject *)state->StatResultType, &statresult_repr_methoddef);
if (statresult_repr_descr == NULL) {
return -1;
}
if (PyObject_SetAttrString(state->StatResultType, "__repr__",
statresult_repr_descr) < 0) {
Py_DECREF(statresult_repr_descr);
return -1;
}
Py_DECREF(statresult_repr_descr);

state->StatVFSResultType = (PyObject *)PyStructSequence_NewType(&statvfs_result_desc);
if (PyModule_AddObjectRef(m, "statvfs_result", state->StatVFSResultType) < 0) {
Expand Down
37 changes: 25 additions & 12 deletions Objects/structseq.c
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,11 @@ structseq_repr(PyObject *op)
{
PyStructSequence *obj = (PyStructSequence *)op;
PyTypeObject *typ = Py_TYPE(obj);
Py_ssize_t n_visible_fields = VISIBLE_SIZE(obj);

// count 5 characters per item: "x=1, "
Py_ssize_t type_name_len = strlen(typ->tp_name);
Py_ssize_t prealloc = (type_name_len + 1
+ VISIBLE_SIZE(obj) * 5 + 1);
Py_ssize_t prealloc = type_name_len + 1 + n_visible_fields * 5 + 1;
PyUnicodeWriter *writer = PyUnicodeWriter_Create(prealloc);
if (writer == NULL) {
return NULL;
Expand All @@ -293,7 +293,10 @@ structseq_repr(PyObject *op)
goto error;
}

for (Py_ssize_t i=0; i < VISIBLE_SIZE(obj); i++) {
// Unnamed slots have no tp_members entry; match each member to its slot by
// offset so unnamed slots print "<unnamed@i>", not a later name (gh-154387).
Py_ssize_t member_index = 0;
for (Py_ssize_t i = 0; i < n_visible_fields; i++) {
if (i > 0) {
// Write ", "
if (PyUnicodeWriter_WriteChar(writer, ',') < 0) {
Expand All @@ -304,16 +307,26 @@ structseq_repr(PyObject *op)
}
}

// Write name
const char *name_utf8 = typ->tp_members[i].name;
if (name_utf8 == NULL) {
PyErr_Format(PyExc_SystemError,
"In structseq_repr(), member %zd name is NULL"
" for type %.500s", i, typ->tp_name);
goto error;
// The named member's slot is recovered from its offset.
const PyMemberDef *member = &typ->tp_members[member_index];
Py_ssize_t named_slot = -1;
if (member->name != NULL) {
// Recover the true slot index. See also function `initialize_members` below.
named_slot = (member->offset - (Py_ssize_t)offsetof(PyStructSequence, ob_item))
/ sizeof(PyObject *);
}
if (PyUnicodeWriter_WriteUTF8(writer, name_utf8, -1) < 0) {
goto error;

// Write the field name, or "<unnamed@i>" for an unnamed field.
if (named_slot == i) {
if (PyUnicodeWriter_WriteUTF8(writer, member->name, -1) < 0) {
goto error;
}
member_index++;
}
else {
if (PyUnicodeWriter_Format(writer, "<unnamed@%zd>", i) < 0) {
goto error;
}
}

// Write "=" + repr(value)
Expand Down
Loading