https://github.com/python/cpython/commit/5d2edf72d25c2616f0e13d10646460a8e69344fa
commit: 5d2edf72d25c2616f0e13d10646460a8e69344fa
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2025-10-23T22:35:17+02:00
summary:

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.

files:
M Doc/library/os.rst
M Lib/test/test_os/test_os.py
M Modules/posixmodule.c

diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index 8f7b9ac15a0d22..d31d0ce9c85e9a 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -3413,11 +3413,6 @@ features:
 
    :class:`!statx_result` has the following attributes:
 
-   .. attribute:: stx_mask
-
-      Bitmask of :const:`STATX_* <STATX_TYPE>` constants specifying the
-      information retrieved, which may differ from what was requested.
-
    .. attribute:: stx_atime
 
       Time of most recent access expressed in seconds.
@@ -3442,9 +3437,9 @@ features:
       .. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
          userspace API headers >= 6.11.
 
-   .. attribute:: stx_atomic_write_unit_min
+   .. attribute:: stx_atomic_write_unit_max
 
-      Minimum size for direct I/O with torn-write protection.
+      Maximum size for direct I/O with torn-write protection.
 
       Equal to ``None`` if :data:`STATX_WRITE_ATOMIC` is missing from
       :attr:`~statx_result.stx_mask`.
@@ -3452,25 +3447,25 @@ features:
       .. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
          userspace API headers >= 6.11.
 
-   .. attribute:: stx_atomic_write_unit_max
+   .. attribute:: stx_atomic_write_unit_max_opt
 
-      Maximum size for direct I/O with torn-write protection.
+      Maximum optimized size for direct I/O with torn-write protection.
 
       Equal to ``None`` if :data:`STATX_WRITE_ATOMIC` is missing from
       :attr:`~statx_result.stx_mask`.
 
       .. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
-         userspace API headers >= 6.11.
+         userspace API headers >= 6.16.
 
-   .. attribute:: stx_atomic_write_unit_max_opt
+   .. attribute:: stx_atomic_write_unit_min
 
-      Maximum optimized size for direct I/O with torn-write protection.
+      Minimum size for direct I/O with torn-write protection.
 
       Equal to ``None`` if :data:`STATX_WRITE_ATOMIC` is missing from
       :attr:`~statx_result.stx_mask`.
 
       .. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
-         userspace API headers >= 6.16.
+         userspace API headers >= 6.11.
 
    .. attribute:: stx_attributes
 
@@ -3536,9 +3531,9 @@ features:
 
       Minor number of the device on which this file resides.
 
-   .. attribute:: stx_dio_offset_align
+   .. attribute:: stx_dio_mem_align
 
-      Direct I/O file offset alignment requirement.
+      Direct I/O memory buffer alignment requirement.
 
       Equal to ``None`` if :data:`STATX_DIOALIGN` is missing from
       :attr:`~statx_result.stx_mask`.
@@ -3546,9 +3541,9 @@ features:
       .. availability:: Linux >= 4.11 with glibc >= 2.28 and build-time kernel
          userspace API headers >= 6.1.
 
-   .. attribute:: stx_dio_mem_align
+   .. attribute:: stx_dio_offset_align
 
-      Direct I/O memory buffer alignment requirement.
+      Direct I/O file offset alignment requirement.
 
       Equal to ``None`` if :data:`STATX_DIOALIGN` is missing from
       :attr:`~statx_result.stx_mask`.
@@ -3580,6 +3575,11 @@ features:
       Equal to ``None`` if :data:`STATX_INO` is missing from
       :attr:`~statx_result.stx_mask`.
 
+   .. attribute:: stx_mask
+
+      Bitmask of :const:`STATX_* <STATX_TYPE>` constants specifying the
+      information retrieved, which may differ from what was requested.
+
    .. attribute:: stx_mnt_id
 
       Mount identifier.
@@ -3594,6 +3594,9 @@ features:
 
       File mode: file type and file mode bits (permissions).
 
+      Equal to ``None`` if :data:`STATX_TYPE | STATX_MODE <STATX_TYPE>`
+      is missing from :attr:`~statx_result.stx_mask`.
+
    .. attribute:: stx_mtime
 
       Time of most recent content modification expressed in seconds.
diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py
index 9a40c5c2a1f1f3..ddb8a63095bce5 100644
--- a/Lib/test/test_os/test_os.py
+++ b/Lib/test/test_os/test_os.py
@@ -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_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_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_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_dev', 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)
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 465af26b1c5a8c..a30712f75d5d06 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -3314,7 +3314,6 @@ os_lstat_impl(PyObject *module, path_t *path, int dir_fd)
 #ifdef HAVE_STATX
 typedef struct {
     PyObject_HEAD
-    double atime_sec, btime_sec, ctime_sec, mtime_sec;
     dev_t rdev, dev;
     struct statx stx;
 } Py_statx_result;
@@ -3332,7 +3331,6 @@ static PyMemberDef pystatx_result_members[] = {
     MM(stx_mask, Py_T_UINT, mask, "member validity mask"),
     MM(stx_blksize, Py_T_UINT, blksize, "blocksize for filesystem I/O"),
     MM(stx_attributes, Py_T_ULONGLONG, attributes, "Linux inode attribute 
bits"),
-    MM(stx_mode, Py_T_USHORT, mode, "protection bits"),
     MM(stx_attributes_mask, Py_T_ULONGLONG, attributes_mask,
         "Mask of supported bits in stx_attributes"),
     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)
 #endif
 
 
+static PyObject*
+pystatx_result_get_stx_mode(PyObject *op, void *Py_UNUSED(context))
+{
+    Py_statx_result *self = Py_statx_result_CAST(op);
+    if (!(self->stx.stx_mask & (STATX_TYPE | STATX_MODE))) {
+        Py_RETURN_NONE;
+    }
+    return PyLong_FromUnsignedLong(self->stx.stx_mode);
+}
+
+
 #define STATX_GET_ULONGLONG(ATTR, MASK) \
     static PyObject* \
     pystatx_result_get_##ATTR(PyObject *op, void *Py_UNUSED(context)) \
@@ -3404,7 +3413,7 @@ STATX_GET_ULONGLONG(stx_subvol, STATX_SUBVOL)
 #endif
 
 
-#define STATX_GET_DOUBLE(ATTR, MEMBER, MASK) \
+#define STATX_GET_DOUBLE(ATTR, MASK) \
     static PyObject* \
     pystatx_result_get_##ATTR(PyObject *op, void *Py_UNUSED(context)) \
     { \
@@ -3412,14 +3421,15 @@ STATX_GET_ULONGLONG(stx_subvol, STATX_SUBVOL)
         if (!(self->stx.stx_mask & MASK)) { \
             Py_RETURN_NONE; \
         } \
-        double sec = self->MEMBER; \
+        struct statx_timestamp *ts = &self->stx.ATTR; \
+        double sec = ((double)ts->tv_sec + ts->tv_nsec * 1e-9); \
         return PyFloat_FromDouble(sec); \
     }
 
-STATX_GET_DOUBLE(stx_atime, atime_sec, STATX_ATIME)
-STATX_GET_DOUBLE(stx_btime, btime_sec, STATX_BTIME)
-STATX_GET_DOUBLE(stx_ctime, ctime_sec, STATX_CTIME)
-STATX_GET_DOUBLE(stx_mtime, mtime_sec, STATX_MTIME)
+STATX_GET_DOUBLE(stx_atime, STATX_ATIME)
+STATX_GET_DOUBLE(stx_btime, STATX_BTIME)
+STATX_GET_DOUBLE(stx_ctime, STATX_CTIME)
+STATX_GET_DOUBLE(stx_mtime, STATX_MTIME)
 
 #define STATX_GET_NSEC(ATTR, MEMBER, MASK) \
     static PyObject* \
@@ -3444,6 +3454,7 @@ STATX_GET_NSEC(stx_mtime_ns, stx_mtime, STATX_MTIME)
     {#attr, pystatx_result_get_##attr, NULL, PyDoc_STR(doc), NULL}
 
 static PyGetSetDef pystatx_result_getset[] = {
+    G(stx_mode, "protection bits"),
     G(stx_nlink, "number of hard links"),
     G(stx_uid, "user ID of owner"),
     G(stx_gid, "group ID of owner"),
@@ -3670,14 +3681,6 @@ os_statx_impl(PyObject *module, path_t *path, unsigned 
int mask, int flags,
         return path_error(path);
     }
 
-    v->atime_sec = ((double)v->stx.stx_atime.tv_sec
-                    + 1e-9 * v->stx.stx_atime.tv_nsec);
-    v->btime_sec = ((double)v->stx.stx_btime.tv_sec
-                    + 1e-9 * v->stx.stx_btime.tv_nsec);
-    v->ctime_sec = ((double)v->stx.stx_ctime.tv_sec
-                    + 1e-9 * v->stx.stx_ctime.tv_nsec);
-    v->mtime_sec = ((double)v->stx.stx_mtime.tv_sec
-                    + 1e-9 * v->stx.stx_mtime.tv_nsec);
     v->rdev = makedev(v->stx.stx_rdev_major, v->stx.stx_rdev_minor);
     v->dev = makedev(v->stx.stx_dev_major, v->stx.stx_dev_minor);
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to