Skip to content

Commit 5d2edf7

Browse files
authored
pythongh-83714: Set os.statx().stx_mode to None if missing from stx_mask (python#140484)
* Set stx_mode to None if STATX_TYPE|STATX_MODE is missing from stx_mask. * Enhance os.statx() tests. * statx_result structure: remove atime_sec, btime_sec, ctime_sec and mtime_sec members. Compute them on demand when stx_atime, stx_btime, stx_ctime and stx_mtime are read. * Doc: fix statx members sorting.
1 parent f0291c3 commit 5d2edf7

File tree

3 files changed

+145
-72
lines changed

3 files changed

+145
-72
lines changed

Doc/library/os.rst

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3413,11 +3413,6 @@ features:
34133413

34143414
:class:`!statx_result` has the following attributes:
34153415

3416-
.. attribute:: stx_mask
3417-
3418-
Bitmask of :const:`STATX_* <STATX_TYPE>` constants specifying the
3419-
information retrieved, which may differ from what was requested.
3420-
34213416
.. attribute:: stx_atime
34223417

34233418
Time of most recent access expressed in seconds.
@@ -3442,35 +3437,35 @@ features:
34423437
.. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
34433438
userspace API headers >= 6.11.
34443439

3445-
.. attribute:: stx_atomic_write_unit_min
3440+
.. attribute:: stx_atomic_write_unit_max
34463441

3447-
Minimum size for direct I/O with torn-write protection.
3442+
Maximum size for direct I/O with torn-write protection.
34483443

34493444
Equal to ``None`` if :data:`STATX_WRITE_ATOMIC` is missing from
34503445
:attr:`~statx_result.stx_mask`.
34513446

34523447
.. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
34533448
userspace API headers >= 6.11.
34543449

3455-
.. attribute:: stx_atomic_write_unit_max
3450+
.. attribute:: stx_atomic_write_unit_max_opt
34563451

3457-
Maximum size for direct I/O with torn-write protection.
3452+
Maximum optimized size for direct I/O with torn-write protection.
34583453

34593454
Equal to ``None`` if :data:`STATX_WRITE_ATOMIC` is missing from
34603455
:attr:`~statx_result.stx_mask`.
34613456

34623457
.. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
3463-
userspace API headers >= 6.11.
3458+
userspace API headers >= 6.16.
34643459

3465-
.. attribute:: stx_atomic_write_unit_max_opt
3460+
.. attribute:: stx_atomic_write_unit_min
34663461

3467-
Maximum optimized size for direct I/O with torn-write protection.
3462+
Minimum size for direct I/O with torn-write protection.
34683463

34693464
Equal to ``None`` if :data:`STATX_WRITE_ATOMIC` is missing from
34703465
:attr:`~statx_result.stx_mask`.
34713466

34723467
.. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
3473-
userspace API headers >= 6.16.
3468+
userspace API headers >= 6.11.
34743469

34753470
.. attribute:: stx_attributes
34763471

@@ -3536,19 +3531,19 @@ features:
35363531

35373532
Minor number of the device on which this file resides.
35383533

3539-
.. attribute:: stx_dio_offset_align
3534+
.. attribute:: stx_dio_mem_align
35403535

3541-
Direct I/O file offset alignment requirement.
3536+
Direct I/O memory buffer alignment requirement.
35423537

35433538
Equal to ``None`` if :data:`STATX_DIOALIGN` is missing from
35443539
:attr:`~statx_result.stx_mask`.
35453540

35463541
.. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
35473542
userspace API headers >= 6.1.
35483543

3549-
.. attribute:: stx_dio_mem_align
3544+
.. attribute:: stx_dio_offset_align
35503545

3551-
Direct I/O memory buffer alignment requirement.
3546+
Direct I/O file offset alignment requirement.
35523547

35533548
Equal to ``None`` if :data:`STATX_DIOALIGN` is missing from
35543549
:attr:`~statx_result.stx_mask`.
@@ -3580,6 +3575,11 @@ features:
35803575
Equal to ``None`` if :data:`STATX_INO` is missing from
35813576
:attr:`~statx_result.stx_mask`.
35823577

3578+
.. attribute:: stx_mask
3579+
3580+
Bitmask of :const:`STATX_* <STATX_TYPE>` constants specifying the
3581+
information retrieved, which may differ from what was requested.
3582+
35833583
.. attribute:: stx_mnt_id
35843584

35853585
Mount identifier.
@@ -3594,6 +3594,9 @@ features:
35943594

35953595
File mode: file type and file mode bits (permissions).
35963596

3597+
Equal to ``None`` if :data:`STATX_TYPE | STATX_MODE <STATX_TYPE>`
3598+
is missing from :attr:`~statx_result.stx_mask`.
3599+
35973600
.. attribute:: stx_mtime
35983601

35993602
Time of most recent content modification expressed in seconds.

Lib/test/test_os/test_os.py

Lines changed: 106 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ def check_statx_attributes(self, filename):
748748
if name.startswith('STATX_'):
749749
maximal_mask |= getattr(os, name)
750750
result = os.statx(filename, maximal_mask)
751-
basic_result = os.stat(filename)
751+
stat_result = os.stat(filename)
752752

753753
time_attributes = ('stx_atime', 'stx_btime', 'stx_ctime', 'stx_mtime')
754754
# gh-83714: stx_btime can be None on tmpfs even if STATX_BTIME mask
@@ -757,62 +757,108 @@ def check_statx_attributes(self, filename):
757757
if getattr(result, name) is not None]
758758
self.check_timestamp_agreement(result, time_attributes)
759759

760-
# Check that valid attributes match os.stat.
760+
def getmask(name):
761+
return getattr(os, name, 0)
762+
761763
requirements = (
762-
('stx_mode', os.STATX_TYPE | os.STATX_MODE),
763-
('stx_nlink', os.STATX_NLINK),
764-
('stx_uid', os.STATX_UID),
765-
('stx_gid', os.STATX_GID),
766764
('stx_atime', os.STATX_ATIME),
767765
('stx_atime_ns', os.STATX_ATIME),
768-
('stx_mtime', os.STATX_MTIME),
769-
('stx_mtime_ns', os.STATX_MTIME),
766+
('stx_atomic_write_segments_max', getmask('STATX_WRITE_ATOMIC')),
767+
('stx_atomic_write_unit_max', getmask('STATX_WRITE_ATOMIC')),
768+
('stx_atomic_write_unit_max_opt', getmask('STATX_WRITE_ATOMIC')),
769+
('stx_atomic_write_unit_min', getmask('STATX_WRITE_ATOMIC')),
770+
('stx_attributes', 0),
771+
('stx_attributes_mask', 0),
772+
('stx_blksize', 0),
773+
('stx_blocks', os.STATX_BLOCKS),
774+
('stx_btime', os.STATX_BTIME),
775+
('stx_btime_ns', os.STATX_BTIME),
770776
('stx_ctime', os.STATX_CTIME),
771777
('stx_ctime_ns', os.STATX_CTIME),
778+
('stx_dev', 0),
779+
('stx_dev_major', 0),
780+
('stx_dev_minor', 0),
781+
('stx_dio_mem_align', getmask('STATX_DIOALIGN')),
782+
('stx_dio_offset_align', getmask('STATX_DIOALIGN')),
783+
('stx_dio_read_offset_align', getmask('STATX_DIO_READ_ALIGN')),
784+
('stx_gid', os.STATX_GID),
772785
('stx_ino', os.STATX_INO),
773-
('stx_size', os.STATX_SIZE),
774-
('stx_blocks', os.STATX_BLOCKS),
775-
('stx_birthtime', os.STATX_BTIME),
776-
('stx_birthtime_ns', os.STATX_BTIME),
777-
# unconditionally valid members
778-
('stx_blksize', 0),
786+
('stx_mask', 0),
787+
('stx_mnt_id', getmask('STATX_MNT_ID')),
788+
('stx_mode', os.STATX_TYPE | os.STATX_MODE),
789+
('stx_mtime', os.STATX_MTIME),
790+
('stx_mtime_ns', os.STATX_MTIME),
791+
('stx_nlink', os.STATX_NLINK),
779792
('stx_rdev', 0),
780-
('stx_dev', 0),
793+
('stx_rdev_major', 0),
794+
('stx_rdev_minor', 0),
795+
('stx_size', os.STATX_SIZE),
796+
('stx_subvol', getmask('STATX_SUBVOL')),
797+
('stx_uid', os.STATX_UID),
781798
)
782-
for name, bits in requirements:
783-
st_name = "st_" + name[4:]
784-
if result.stx_mask & bits == bits and hasattr(basic_result, st_name):
785-
x = getattr(result, name)
786-
b = getattr(basic_result, st_name)
787-
self.assertEqual(type(x), type(b))
788-
if isinstance(x, float):
789-
self.assertAlmostEqual(x, b, msg=name)
799+
optional_members = {
800+
'stx_atomic_write_segments_max',
801+
'stx_atomic_write_unit_max',
802+
'stx_atomic_write_unit_max_opt',
803+
'stx_atomic_write_unit_min',
804+
'stx_dio_mem_align',
805+
'stx_dio_offset_align',
806+
'stx_dio_read_offset_align',
807+
'stx_mnt_id',
808+
'stx_subvol',
809+
}
810+
float_type = {
811+
'stx_atime',
812+
'stx_btime',
813+
'stx_ctime',
814+
'stx_mtime',
815+
}
816+
817+
members = set(name for name in dir(result)
818+
if name.startswith('stx_'))
819+
tested = set(name for name, mask in requirements)
820+
if members - tested:
821+
raise ValueError(f"statx members not tested: {members - tested}")
822+
823+
for name, mask in requirements:
824+
with self.subTest(name=name):
825+
try:
826+
x = getattr(result, name)
827+
except AttributeError:
828+
if name in optional_members:
829+
continue
830+
else:
831+
raise
832+
833+
if not(result.stx_mask & mask == mask):
834+
self.assertIsNone(x)
835+
continue
836+
837+
if name in float_type:
838+
self.assertIsInstance(x, float)
790839
else:
791-
self.assertEqual(x, b, msg=name)
840+
self.assertIsInstance(x, int)
841+
842+
# Compare with stat_result
843+
try:
844+
b = getattr(stat_result, "st_" + name[4:])
845+
except AttributeError:
846+
pass
847+
else:
848+
self.assertEqual(type(x), type(b))
849+
if isinstance(x, float):
850+
self.assertAlmostEqual(x, b)
851+
else:
852+
self.assertEqual(x, b)
792853

793854
self.assertEqual(result.stx_rdev_major, os.major(result.stx_rdev))
794855
self.assertEqual(result.stx_rdev_minor, os.minor(result.stx_rdev))
795856
self.assertEqual(result.stx_dev_major, os.major(result.stx_dev))
796857
self.assertEqual(result.stx_dev_minor, os.minor(result.stx_dev))
797858

798-
members = [name for name in dir(result)
799-
if name.startswith('stx_')]
800-
for name in members:
801-
try:
802-
setattr(result, name, 1)
803-
self.fail("No exception raised")
804-
except AttributeError:
805-
pass
806-
807859
self.assertEqual(result.stx_attributes & result.stx_attributes_mask,
808860
result.stx_attributes)
809861

810-
# statx_result is not a tuple or tuple-like object.
811-
with self.assertRaisesRegex(TypeError, 'not subscriptable'):
812-
result[0]
813-
with self.assertRaisesRegex(TypeError, 'cannot unpack'):
814-
_, _ = result
815-
816862
@unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()')
817863
def test_statx_attributes(self):
818864
self.check_statx_attributes(self.fname)
@@ -829,6 +875,27 @@ def test_statx_attributes_bytes(self):
829875
def test_statx_attributes_pathlike(self):
830876
self.check_statx_attributes(FakePath(self.fname))
831877

878+
@unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()')
879+
def test_statx_result(self):
880+
result = os.statx(self.fname, os.STATX_BASIC_STATS)
881+
882+
# Check that attributes are read-only
883+
members = [name for name in dir(result)
884+
if name.startswith('stx_')]
885+
for name in members:
886+
try:
887+
setattr(result, name, 1)
888+
except AttributeError:
889+
pass
890+
else:
891+
self.fail("No exception raised")
892+
893+
# statx_result is not a tuple or tuple-like object.
894+
with self.assertRaisesRegex(TypeError, 'not subscriptable'):
895+
result[0]
896+
with self.assertRaisesRegex(TypeError, 'cannot unpack'):
897+
_, _ = result
898+
832899
@unittest.skipUnless(hasattr(os, 'statvfs'), 'test needs os.statvfs()')
833900
def test_statvfs_attributes(self):
834901
result = os.statvfs(self.fname)

Modules/posixmodule.c

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3314,7 +3314,6 @@ os_lstat_impl(PyObject *module, path_t *path, int dir_fd)
33143314
#ifdef HAVE_STATX
33153315
typedef struct {
33163316
PyObject_HEAD
3317-
double atime_sec, btime_sec, ctime_sec, mtime_sec;
33183317
dev_t rdev, dev;
33193318
struct statx stx;
33203319
} Py_statx_result;
@@ -3332,7 +3331,6 @@ static PyMemberDef pystatx_result_members[] = {
33323331
MM(stx_mask, Py_T_UINT, mask, "member validity mask"),
33333332
MM(stx_blksize, Py_T_UINT, blksize, "blocksize for filesystem I/O"),
33343333
MM(stx_attributes, Py_T_ULONGLONG, attributes, "Linux inode attribute bits"),
3335-
MM(stx_mode, Py_T_USHORT, mode, "protection bits"),
33363334
MM(stx_attributes_mask, Py_T_ULONGLONG, attributes_mask,
33373335
"Mask of supported bits in stx_attributes"),
33383336
MM(stx_rdev_major, Py_T_UINT, rdev_major, "represented device major number"),
@@ -3381,6 +3379,17 @@ STATX_GET_UINT(stx_atomic_write_unit_max_opt, STATX_WRITE_ATOMIC)
33813379
#endif
33823380

33833381

3382+
static PyObject*
3383+
pystatx_result_get_stx_mode(PyObject *op, void *Py_UNUSED(context))
3384+
{
3385+
Py_statx_result *self = Py_statx_result_CAST(op);
3386+
if (!(self->stx.stx_mask & (STATX_TYPE | STATX_MODE))) {
3387+
Py_RETURN_NONE;
3388+
}
3389+
return PyLong_FromUnsignedLong(self->stx.stx_mode);
3390+
}
3391+
3392+
33843393
#define STATX_GET_ULONGLONG(ATTR, MASK) \
33853394
static PyObject* \
33863395
pystatx_result_get_##ATTR(PyObject *op, void *Py_UNUSED(context)) \
@@ -3404,22 +3413,23 @@ STATX_GET_ULONGLONG(stx_subvol, STATX_SUBVOL)
34043413
#endif
34053414

34063415

3407-
#define STATX_GET_DOUBLE(ATTR, MEMBER, MASK) \
3416+
#define STATX_GET_DOUBLE(ATTR, MASK) \
34083417
static PyObject* \
34093418
pystatx_result_get_##ATTR(PyObject *op, void *Py_UNUSED(context)) \
34103419
{ \
34113420
Py_statx_result *self = Py_statx_result_CAST(op); \
34123421
if (!(self->stx.stx_mask & MASK)) { \
34133422
Py_RETURN_NONE; \
34143423
} \
3415-
double sec = self->MEMBER; \
3424+
struct statx_timestamp *ts = &self->stx.ATTR; \
3425+
double sec = ((double)ts->tv_sec + ts->tv_nsec * 1e-9); \
34163426
return PyFloat_FromDouble(sec); \
34173427
}
34183428

3419-
STATX_GET_DOUBLE(stx_atime, atime_sec, STATX_ATIME)
3420-
STATX_GET_DOUBLE(stx_btime, btime_sec, STATX_BTIME)
3421-
STATX_GET_DOUBLE(stx_ctime, ctime_sec, STATX_CTIME)
3422-
STATX_GET_DOUBLE(stx_mtime, mtime_sec, STATX_MTIME)
3429+
STATX_GET_DOUBLE(stx_atime, STATX_ATIME)
3430+
STATX_GET_DOUBLE(stx_btime, STATX_BTIME)
3431+
STATX_GET_DOUBLE(stx_ctime, STATX_CTIME)
3432+
STATX_GET_DOUBLE(stx_mtime, STATX_MTIME)
34233433

34243434
#define STATX_GET_NSEC(ATTR, MEMBER, MASK) \
34253435
static PyObject* \
@@ -3444,6 +3454,7 @@ STATX_GET_NSEC(stx_mtime_ns, stx_mtime, STATX_MTIME)
34443454
{#attr, pystatx_result_get_##attr, NULL, PyDoc_STR(doc), NULL}
34453455

34463456
static PyGetSetDef pystatx_result_getset[] = {
3457+
G(stx_mode, "protection bits"),
34473458
G(stx_nlink, "number of hard links"),
34483459
G(stx_uid, "user ID of owner"),
34493460
G(stx_gid, "group ID of owner"),
@@ -3670,14 +3681,6 @@ os_statx_impl(PyObject *module, path_t *path, unsigned int mask, int flags,
36703681
return path_error(path);
36713682
}
36723683

3673-
v->atime_sec = ((double)v->stx.stx_atime.tv_sec
3674-
+ 1e-9 * v->stx.stx_atime.tv_nsec);
3675-
v->btime_sec = ((double)v->stx.stx_btime.tv_sec
3676-
+ 1e-9 * v->stx.stx_btime.tv_nsec);
3677-
v->ctime_sec = ((double)v->stx.stx_ctime.tv_sec
3678-
+ 1e-9 * v->stx.stx_ctime.tv_nsec);
3679-
v->mtime_sec = ((double)v->stx.stx_mtime.tv_sec
3680-
+ 1e-9 * v->stx.stx_mtime.tv_nsec);
36813684
v->rdev = makedev(v->stx.stx_rdev_major, v->stx.stx_rdev_minor);
36823685
v->dev = makedev(v->stx.stx_dev_major, v->stx.stx_dev_minor);
36833686

0 commit comments

Comments
 (0)