Skip to content

Commit 50a2e22

Browse files
committed
feat(os): custom repr for os.stat_result showing time fields by name
os.stat_result has three unnamed integer time slots (7-9) whose values are mirrored by the named float fields st_atime/st_mtime/st_ctime (statresult_new fills the latter from the former). The generic struct sequence repr shows those unnamed slots as <unnamed@7..9>, which is uninformative. Add statresult_repr showing the named fields (st_mode..st_size and the float st_atime/st_mtime/st_ctime) by name, matching the attributes. Install it through the type dict via PyObject_SetAttrString so repr() and st.__repr__() resolve to the same function; a raw tp_repr assignment would leave the __repr__ wrapper readied by PyStructSequence_NewType pointing at the generic repr. Other struct sequences keep the generic <unnamed@N>.
1 parent 227e089 commit 50a2e22

3 files changed

Lines changed: 144 additions & 32 deletions

File tree

Lib/test/test_structseq.py

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,33 +49,9 @@ def test_repr(self):
4949
self.assertIn("st_dev=", rep)
5050

5151
def test_repr_with_unnamed_fields(self):
52-
# gh-154387: unnamed fields must not borrow the name of a later hidden
53-
# field in repr; they are shown as "<unnamed@N>" instead.
54-
self.assertEqual(os.stat_result.n_sequence_fields, 10)
55-
self.assertEqual(os.stat_result.n_unnamed_fields, 3)
56-
57-
r = os.stat_result(range(os.stat_result.n_sequence_fields))
58-
rep = repr(r)
59-
self.assertEqual(rep,
60-
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
61-
"st_uid=4, st_gid=5, st_size=6, <unnamed@7>=7, <unnamed@8>=8, "
62-
"<unnamed@9>=9)")
63-
64-
# Regression guard: the hidden field names must not leak into the repr.
65-
self.assertNotIn("st_atime=", rep)
66-
self.assertNotIn("st_mtime=", rep)
67-
self.assertNotIn("st_ctime=", rep)
68-
69-
# Supplying the hidden fields too must not change the repr.
70-
r_full = os.stat_result(range(os.stat_result.n_fields))
71-
self.assertEqual(repr(r_full), rep)
72-
self.assertEqual(r_full[7], 7)
73-
self.assertNotEqual(r_full[7], r_full.st_atime)
74-
75-
def test_repr_with_interspersed_unnamed_fields(self):
76-
# gh-154387: unnamed fields need not be contiguous or trailing; each is
77-
# shown at its own index rather than borrowing a neighbour's name. The
78-
# trailing named field is hidden and must not appear in the repr.
52+
# gh-154387: each unnamed field is shown at its own index rather than
53+
# borrowing a neighbour's name. Unnamed fields need not be contiguous or
54+
# trailing.
7955
_testcapi = import_helper.import_module("_testcapi")
8056
cls = _testcapi.structseq_newtype_interspersed_unnamed()
8157
self.assertEqual(cls.n_fields, 5)
@@ -108,6 +84,69 @@ def test_repr_with_interspersed_unnamed_fields(self):
10884
self.assertEqual(t2.third, 2)
10985
self.assertIsNone(t2.fifth)
11086

87+
def test_os_stat_result_has_custom_repr(self):
88+
# gh-154387: os.stat_result has a custom repr (statresult_repr) showing
89+
# the float times st_atime/st_mtime/st_ctime by name, not the unnamed
90+
# integer slots with generic "<unnamed@N>".
91+
self.assertEqual(os.stat_result.n_sequence_fields, 10)
92+
self.assertEqual(os.stat_result.n_unnamed_fields, 3)
93+
94+
# All fields supplied: st_atime/st_mtime/st_ctime are the float slots.
95+
r1 = os.stat_result(range(os.stat_result.n_fields))
96+
rep = repr(r1)
97+
self.assertEqual(rep,
98+
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
99+
"st_uid=4, st_gid=5, st_size=6, st_atime=10, st_mtime=11, "
100+
"st_ctime=12)")
101+
self.assertNotIn("<unnamed@", rep)
102+
self.assertEqual(r1.st_atime, 10)
103+
self.assertEqual(r1.st_mtime, 11)
104+
self.assertEqual(r1.st_ctime, 12)
105+
106+
# Only the visible slots supplied: statresult_new fills the float times
107+
# from the integer slots, so the repr shows those filled values.
108+
r2 = os.stat_result(range(10))
109+
self.assertEqual(repr(r2),
110+
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
111+
"st_uid=4, st_gid=5, st_size=6, st_atime=7, st_mtime=8, "
112+
"st_ctime=9)")
113+
self.assertEqual(r2.st_atime, 7)
114+
self.assertEqual(r2.st_mtime, 8)
115+
self.assertEqual(r2.st_ctime, 9)
116+
117+
# The st_atime/st_mtime slots are supplied, but st_ctime is not; the
118+
# st_ctime float slot is filled from the st_ctime integer slot.
119+
r3 = os.stat_result(range(12))
120+
self.assertEqual(repr(r3),
121+
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
122+
"st_uid=4, st_gid=5, st_size=6, st_atime=10, st_mtime=11, "
123+
"st_ctime=9)")
124+
self.assertEqual(r3.st_atime, 10)
125+
self.assertEqual(r3.st_mtime, 11)
126+
self.assertEqual(r3.st_ctime, 9)
127+
128+
# The st_mtime slot is supplied, but st_atime/st_ctime are not; the
129+
# st_atime/st_ctime float slots are filled from the st_atime/st_ctime
130+
# integer slots.
131+
r4 = os.stat_result(range(10), {'st_mtime': -1.0})
132+
self.assertEqual(repr(r4),
133+
"os.stat_result(st_mode=0, st_ino=1, st_dev=2, st_nlink=3, "
134+
"st_uid=4, st_gid=5, st_size=6, st_atime=7, st_mtime=-1.0, "
135+
"st_ctime=9)")
136+
self.assertEqual(r4.st_atime, 7)
137+
self.assertEqual(r4.st_mtime, -1.0)
138+
self.assertEqual(r4.st_ctime, 9)
139+
140+
# repr(), __repr__(), and str() must all resolve to the custom repr
141+
# (a raw tp_repr override left __repr__() diverging).
142+
for r in (r1, r2, r3, r4):
143+
rep = repr(r)
144+
self.assertEqual(r.__repr__(), rep)
145+
self.assertEqual(type(r).__repr__(r), rep)
146+
self.assertEqual(str(r), rep)
147+
self.assertEqual(f"{r!r}", rep)
148+
self.assertEqual(f"{r!s}", rep)
149+
111150
def test_concat(self):
112151
t1 = time.gmtime()
113152
t2 = t1 + tuple(t1)
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Fix the :func:`repr` of struct sequence objects that contain unnamed fields,
2-
such as :class:`os.stat_result`. Previously each unnamed field borrowed the
3-
name of a later, hidden field (for example the integer time slots of
4-
``os.stat_result`` were shown as ``st_atime``, ``st_mtime`` and ``st_ctime``,
5-
which are in fact separate hidden float fields); unnamed fields are now shown
6-
as ``<unnamed@N>``, where ``N`` is the sequence index. Patch by Xuehai Pan.
2+
such as :class:`os.stat_result`, whose integer time slots previously borrowed
3+
the ``st_atime``/``st_mtime``/``st_ctime`` names of separate hidden float
4+
fields. Unnamed fields are now shown as ``<unnamed@N>`` (``N`` is the sequence
5+
index); :class:`os.stat_result` uses a dedicated repr that shows the float
6+
``st_atime``, ``st_mtime`` and ``st_ctime`` fields by name. Patch by Xuehai Pan.

Modules/posixmodule.c

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2572,6 +2572,8 @@ statresult_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
25722572
/* If we have been initialized from a tuple,
25732573
st_?time might be set to None. Initialize it
25742574
from the int slots. */
2575+
/* See also statresult_repr() below, which assumes that the float slots are
2576+
always initialized. */
25752577
for (i = 7; i <= 9; i++) {
25762578
if (result->ob_item[i+3] == Py_None) {
25772579
Py_DECREF(Py_None);
@@ -2581,6 +2583,63 @@ statresult_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
25812583
return (PyObject*)result;
25822584
}
25832585

2586+
static PyObject *
2587+
statresult_repr(PyObject *op, PyObject *Py_UNUSED(ignored))
2588+
{
2589+
// gh-154387: Show the unnamed integer slots 7-9 with following named float
2590+
// slots st_atime/st_mtime/st_ctime instead of the generic "<unnamed@N>".
2591+
// The custom statresult_new() above always initializes the float slots.
2592+
// So it is safe to use the float slots for the repr().
2593+
PyTypeObject *type = Py_TYPE(op);
2594+
PyUnicodeWriter *writer = PyUnicodeWriter_Create(128);
2595+
if (writer == NULL) {
2596+
return NULL;
2597+
}
2598+
if (PyUnicodeWriter_WriteUTF8(writer, type->tp_name, -1) < 0) {
2599+
goto error;
2600+
}
2601+
if (PyUnicodeWriter_WriteChar(writer, '(') < 0) {
2602+
goto error;
2603+
}
2604+
// The first 10 packed members: st_mode..st_size and the 3 float times
2605+
// st_atime/st_mtime/st_ctime (tp_members omits the unnamed slots 7-9).
2606+
// NB: This approach assumes the 3 unnamed slots are at the end of the
2607+
// visible members, which is true for the current stat_result_desc.
2608+
for (Py_ssize_t i = 0; i < 10; i++) {
2609+
if (i > 0 && PyUnicodeWriter_WriteUTF8(writer, ", ", 2) < 0) {
2610+
goto error;
2611+
}
2612+
PyMemberDef *member = &type->tp_members[i];
2613+
if (PyUnicodeWriter_WriteUTF8(writer, member->name, -1) < 0) {
2614+
goto error;
2615+
}
2616+
if (PyUnicodeWriter_WriteChar(writer, '=') < 0) {
2617+
goto error;
2618+
}
2619+
PyObject *value = PyMember_GetOne((const char *)op, member);
2620+
if (value == NULL) {
2621+
goto error;
2622+
}
2623+
if (PyUnicodeWriter_WriteRepr(writer, value) < 0) {
2624+
Py_DECREF(value);
2625+
goto error;
2626+
}
2627+
Py_DECREF(value);
2628+
}
2629+
if (PyUnicodeWriter_WriteChar(writer, ')') < 0) {
2630+
goto error;
2631+
}
2632+
return PyUnicodeWriter_Finish(writer);
2633+
2634+
error:
2635+
PyUnicodeWriter_Discard(writer);
2636+
return NULL;
2637+
}
2638+
2639+
static PyMethodDef statresult_repr_methoddef = {
2640+
"__repr__", statresult_repr, METH_NOARGS, NULL,
2641+
};
2642+
25842643
static int
25852644
_posix_clear(PyObject *module)
25862645
{
@@ -18860,6 +18919,20 @@ posixmodule_exec(PyObject *m)
1886018919
}
1886118920
state->statresult_new_orig = ((PyTypeObject *)state->StatResultType)->tp_new;
1886218921
((PyTypeObject *)state->StatResultType)->tp_new = statresult_new;
18922+
// Install __repr__ through the type dict so repr() and st.__repr__()
18923+
// resolve to the same function; a raw type->tp_repr assignment leaves the
18924+
// stale __repr__ wrapper which is readied by PyStructSequence_NewType.
18925+
PyObject *statresult_repr_descr = PyDescr_NewMethod(
18926+
(PyTypeObject *)state->StatResultType, &statresult_repr_methoddef);
18927+
if (statresult_repr_descr == NULL) {
18928+
return -1;
18929+
}
18930+
if (PyObject_SetAttrString(state->StatResultType, "__repr__",
18931+
statresult_repr_descr) < 0) {
18932+
Py_DECREF(statresult_repr_descr);
18933+
return -1;
18934+
}
18935+
Py_DECREF(statresult_repr_descr);
1886318936

1886418937
state->StatVFSResultType = (PyObject *)PyStructSequence_NewType(&statvfs_result_desc);
1886518938
if (PyModule_AddObjectRef(m, "statvfs_result", state->StatVFSResultType) < 0) {

0 commit comments

Comments
 (0)