gh-83714: Set os.statx().stx_mode to None if missing from stx_mask (#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.
This commit is contained in:
Victor Stinner 2025-10-23 22:35:17 +02:00 committed by GitHub
parent f0291c3f2d
commit 5d2edf72d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 148 additions and 75 deletions

View file

@ -748,7 +748,7 @@ def check_statx_attributes(self, filename):
if name.startswith('STATX_'):
maximal_mask |= getattr(os, name)
result = os.statx(filename, maximal_mask)
basic_result = os.stat(filename)
stat_result = os.stat(filename)
time_attributes = ('stx_atime', 'stx_btime', 'stx_ctime', 'stx_mtime')
# gh-83714: stx_btime can be None on tmpfs even if STATX_BTIME mask
@ -757,62 +757,108 @@ def check_statx_attributes(self, filename):
if getattr(result, name) is not None]
self.check_timestamp_agreement(result, time_attributes)
# Check that valid attributes match os.stat.
def getmask(name):
return getattr(os, name, 0)
requirements = (
('stx_mode', os.STATX_TYPE | os.STATX_MODE),
('stx_nlink', os.STATX_NLINK),
('stx_uid', os.STATX_UID),
('stx_gid', os.STATX_GID),
('stx_atime', os.STATX_ATIME),
('stx_atime_ns', os.STATX_ATIME),
('stx_mtime', os.STATX_MTIME),
('stx_mtime_ns', os.STATX_MTIME),
('stx_atomic_write_segments_max', getmask('STATX_WRITE_ATOMIC')),
('stx_atomic_write_unit_max', getmask('STATX_WRITE_ATOMIC')),
('stx_atomic_write_unit_max_opt', getmask('STATX_WRITE_ATOMIC')),
('stx_atomic_write_unit_min', getmask('STATX_WRITE_ATOMIC')),
('stx_attributes', 0),
('stx_attributes_mask', 0),
('stx_blksize', 0),
('stx_blocks', os.STATX_BLOCKS),
('stx_btime', os.STATX_BTIME),
('stx_btime_ns', os.STATX_BTIME),
('stx_ctime', os.STATX_CTIME),
('stx_ctime_ns', os.STATX_CTIME),
('stx_ino', os.STATX_INO),
('stx_size', os.STATX_SIZE),
('stx_blocks', os.STATX_BLOCKS),
('stx_birthtime', os.STATX_BTIME),
('stx_birthtime_ns', os.STATX_BTIME),
# unconditionally valid members
('stx_blksize', 0),
('stx_rdev', 0),
('stx_dev', 0),
('stx_dev_major', 0),
('stx_dev_minor', 0),
('stx_dio_mem_align', getmask('STATX_DIOALIGN')),
('stx_dio_offset_align', getmask('STATX_DIOALIGN')),
('stx_dio_read_offset_align', getmask('STATX_DIO_READ_ALIGN')),
('stx_gid', os.STATX_GID),
('stx_ino', os.STATX_INO),
('stx_mask', 0),
('stx_mnt_id', getmask('STATX_MNT_ID')),
('stx_mode', os.STATX_TYPE | os.STATX_MODE),
('stx_mtime', os.STATX_MTIME),
('stx_mtime_ns', os.STATX_MTIME),
('stx_nlink', os.STATX_NLINK),
('stx_rdev', 0),
('stx_rdev_major', 0),
('stx_rdev_minor', 0),
('stx_size', os.STATX_SIZE),
('stx_subvol', getmask('STATX_SUBVOL')),
('stx_uid', os.STATX_UID),
)
for name, bits in requirements:
st_name = "st_" + name[4:]
if result.stx_mask & bits == bits and hasattr(basic_result, st_name):
x = getattr(result, name)
b = getattr(basic_result, st_name)
self.assertEqual(type(x), type(b))
if isinstance(x, float):
self.assertAlmostEqual(x, b, msg=name)
optional_members = {
'stx_atomic_write_segments_max',
'stx_atomic_write_unit_max',
'stx_atomic_write_unit_max_opt',
'stx_atomic_write_unit_min',
'stx_dio_mem_align',
'stx_dio_offset_align',
'stx_dio_read_offset_align',
'stx_mnt_id',
'stx_subvol',
}
float_type = {
'stx_atime',
'stx_btime',
'stx_ctime',
'stx_mtime',
}
members = set(name for name in dir(result)
if name.startswith('stx_'))
tested = set(name for name, mask in requirements)
if members - tested:
raise ValueError(f"statx members not tested: {members - tested}")
for name, mask in requirements:
with self.subTest(name=name):
try:
x = getattr(result, name)
except AttributeError:
if name in optional_members:
continue
else:
raise
if not(result.stx_mask & mask == mask):
self.assertIsNone(x)
continue
if name in float_type:
self.assertIsInstance(x, float)
else:
self.assertEqual(x, b, msg=name)
self.assertIsInstance(x, int)
# Compare with stat_result
try:
b = getattr(stat_result, "st_" + name[4:])
except AttributeError:
pass
else:
self.assertEqual(type(x), type(b))
if isinstance(x, float):
self.assertAlmostEqual(x, b)
else:
self.assertEqual(x, b)
self.assertEqual(result.stx_rdev_major, os.major(result.stx_rdev))
self.assertEqual(result.stx_rdev_minor, os.minor(result.stx_rdev))
self.assertEqual(result.stx_dev_major, os.major(result.stx_dev))
self.assertEqual(result.stx_dev_minor, os.minor(result.stx_dev))
members = [name for name in dir(result)
if name.startswith('stx_')]
for name in members:
try:
setattr(result, name, 1)
self.fail("No exception raised")
except AttributeError:
pass
self.assertEqual(result.stx_attributes & result.stx_attributes_mask,
result.stx_attributes)
# statx_result is not a tuple or tuple-like object.
with self.assertRaisesRegex(TypeError, 'not subscriptable'):
result[0]
with self.assertRaisesRegex(TypeError, 'cannot unpack'):
_, _ = result
@unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()')
def test_statx_attributes(self):
self.check_statx_attributes(self.fname)
@ -829,6 +875,27 @@ def test_statx_attributes_bytes(self):
def test_statx_attributes_pathlike(self):
self.check_statx_attributes(FakePath(self.fname))
@unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()')
def test_statx_result(self):
result = os.statx(self.fname, os.STATX_BASIC_STATS)
# Check that attributes are read-only
members = [name for name in dir(result)
if name.startswith('stx_')]
for name in members:
try:
setattr(result, name, 1)
except AttributeError:
pass
else:
self.fail("No exception raised")
# statx_result is not a tuple or tuple-like object.
with self.assertRaisesRegex(TypeError, 'not subscriptable'):
result[0]
with self.assertRaisesRegex(TypeError, 'cannot unpack'):
_, _ = result
@unittest.skipUnless(hasattr(os, 'statvfs'), 'test needs os.statvfs()')
def test_statvfs_attributes(self):
result = os.statvfs(self.fname)