Skip to content
Open
4 changes: 3 additions & 1 deletion Lib/test/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,14 +1019,16 @@ def test_detach_materialized_dict_no_memory(self):
import test.support
import _testcapi

set_nomemory = _testcapi.set_nomemory # eagerly load the function

class A:
def __init__(self):
self.a = 1
self.b = 2
a = A()
d = a.__dict__
with test.support.catch_unraisable_exception() as ex:
_testcapi.set_nomemory(0, 1)
set_nomemory(0, 1)
del a
assert ex.unraisable.exc_type is MemoryError
try:
Expand Down
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 @@ -18860,6 +18919,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