Skip to content

Commit c80787c

Browse files
committed
Implement os.statx
1 parent ac5c5d4 commit c80787c

File tree

16 files changed

+665
-64
lines changed

16 files changed

+665
-64
lines changed

Doc/library/os.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3383,6 +3383,63 @@ features:
33833383
Added the :attr:`st_birthtime` member on Windows.
33843384

33853385

3386+
.. function:: statx(path, mask, *, dir_fd=None, follow_symlinks=True, sync=None)
3387+
3388+
Get the status of a file or file descriptor by performing a :c:func:`!statx`
3389+
system call on the given path. *path* may be specified as either a string or
3390+
bytes -- directly or indirectly through the :class:`PathLike` interface --
3391+
or as an open file descriptor. *mask* is a combination of the module-level
3392+
:const:`STATX_* <STATX_TYPE>` constants specifying the information to
3393+
retrieve. Returns a :class:`types.SimpleNamespace` object with attributes
3394+
from :c:struct:`!statx` corresponding to the set bits in *mask* and the
3395+
unconditionally valid members. The :attr:`!stx_mask` attribute, which may
3396+
differ from *mask*, specifies the validity of the information retrieved.
3397+
3398+
The optional parameter *sync* controls the freshness of the returned
3399+
information. ``sync=True`` requests that the kernel return up-to-date
3400+
information, even when doing so is expensive (for example, requiring a
3401+
round trip to the server for a file on a network filesystem).
3402+
``sync=False`` requests that the kernel return cached information if
3403+
available. ``sync=None`` expresses no preference, in which case the kernel
3404+
will return information as fresh as :func:`~os.stat` does.
3405+
3406+
This function supports :ref:`specifying a file descriptor <path_fd>`,
3407+
:ref:`paths relative to directory descriptors <dir_fd>`, and
3408+
:ref:`not following symlinks <follow_symlinks>`.
3409+
3410+
.. seealso:: The :manpage:`statx(2)` man page.
3411+
3412+
.. availability:: Linux >= 4.11 with glibc >= 2.28.
3413+
3414+
.. versionadded:: next
3415+
3416+
.. data:: STATX_TYPE
3417+
STATX_MODE
3418+
STATX_NLINK
3419+
STATX_UID
3420+
STATX_GID
3421+
STATX_ATIME
3422+
STATX_MTIME
3423+
STATX_CTIME
3424+
STATX_INO
3425+
STATX_SIZE
3426+
STATX_BLOCKS
3427+
STATX_BASIC_STATS
3428+
STATX_BTIME
3429+
STATX_MNT_ID
3430+
STATX_DIOALIGN
3431+
STATX_MNT_ID_UNIQUE
3432+
STATX_SUBVOL
3433+
STATX_WRITE_ATOMIC
3434+
STATX_DIO_READ_ALIGN
3435+
3436+
Bitflags for use as the *mask* parameter to :func:`os.statx`.
3437+
3438+
.. availability:: Linux >= 4.11 with glibc >= 2.28.
3439+
3440+
.. versionadded:: next
3441+
3442+
33863443
.. function:: statvfs(path)
33873444

33883445
Perform a :c:func:`!statvfs` system call on the given path. The return value is

Doc/library/stat.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,3 +493,22 @@ constants, but are not an exhaustive list.
493493
IO_REPARSE_TAG_APPEXECLINK
494494

495495
.. versionadded:: 3.8
496+
497+
On Linux, the following file attribute constants are available for use when
498+
testing bits in the :attr:`~os.statx_result.stx_attributes` and
499+
:attr:`~os.statx_result.stx_attributes_mask` members returned by
500+
:func:`os.statx`. See the :manpage:`statx(2)` man page for more detail on the
501+
meaning of these constants.
502+
503+
.. data:: STATX_ATTR_COMPRESSED
504+
STATX_ATTR_IMMUTABLE
505+
STATX_ATTR_APPEND
506+
STATX_ATTR_NODUMP
507+
STATX_ATTR_ENCRYPTED
508+
STATX_ATTR_AUTOMOUNT
509+
STATX_ATTR_MOUNT_ROOT
510+
STATX_ATTR_VERITY
511+
STATX_ATTR_DAX
512+
STATX_ATTR_WRITE_ATOMIC
513+
514+
.. versionadded:: next

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ struct _Py_global_strings {
590590
STRUCT_FOR_ID(loop)
591591
STRUCT_FOR_ID(manual_reset)
592592
STRUCT_FOR_ID(mapping)
593+
STRUCT_FOR_ID(mask)
593594
STRUCT_FOR_ID(match)
594595
STRUCT_FOR_ID(max_length)
595596
STRUCT_FOR_ID(maxdigits)
@@ -784,6 +785,7 @@ struct _Py_global_strings {
784785
STRUCT_FOR_ID(sub_key)
785786
STRUCT_FOR_ID(subcalls)
786787
STRUCT_FOR_ID(symmetric_difference_update)
788+
STRUCT_FOR_ID(sync)
787789
STRUCT_FOR_ID(tabsize)
788790
STRUCT_FOR_ID(tag)
789791
STRUCT_FOR_ID(target)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/os.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ def _add(str, fn):
131131
_add("HAVE_UNLINKAT", "unlink")
132132
_add("HAVE_UNLINKAT", "rmdir")
133133
_add("HAVE_UTIMENSAT", "utime")
134+
if _exists("statx"):
135+
_set.add(statx)
134136
supports_dir_fd = _set
135137

136138
_set = set()
@@ -152,6 +154,8 @@ def _add(str, fn):
152154
_add("HAVE_FPATHCONF", "pathconf")
153155
if _exists("statvfs") and _exists("fstatvfs"): # mac os x10.3
154156
_add("HAVE_FSTATVFS", "statvfs")
157+
if _exists("statx"):
158+
_set.add(statx)
155159
supports_fd = _set
156160

157161
_set = set()
@@ -190,6 +194,8 @@ def _add(str, fn):
190194
_add("HAVE_FSTATAT", "stat")
191195
_add("HAVE_UTIMENSAT", "utime")
192196
_add("MS_WINDOWS", "stat")
197+
if _exists("statx"):
198+
_set.add(statx)
193199
supports_follow_symlinks = _set
194200

195201
del _set

Lib/stat.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,21 @@ def filemode(mode):
200200
FILE_ATTRIBUTE_VIRTUAL = 65536
201201

202202

203+
# Linux STATX_ATTR constants for interpreting os.statx()'s
204+
# "stx_attributes" and "stx_attributes_mask" members
205+
206+
STATX_ATTR_COMPRESSED = 0x00000004
207+
STATX_ATTR_IMMUTABLE = 0x00000010
208+
STATX_ATTR_APPEND = 0x00000020
209+
STATX_ATTR_NODUMP = 0x00000040
210+
STATX_ATTR_ENCRYPTED = 0x00000800
211+
STATX_ATTR_AUTOMOUNT = 0x00001000
212+
STATX_ATTR_MOUNT_ROOT = 0x00002000
213+
STATX_ATTR_VERITY = 0x00100000
214+
STATX_ATTR_DAX = 0x00200000
215+
STATX_ATTR_WRITE_ATOMIC = 0x00400000
216+
217+
203218
# If available, use C implementation
204219
try:
205220
from _stat import *

Lib/test/test_os.py

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,14 @@ def setUp(self):
640640
self.addCleanup(os_helper.unlink, self.fname)
641641
create_file(self.fname, b"ABC")
642642

643+
def check_timestamp_agreement(self, result, names):
644+
# Make sure that the st_?time and st_?time_ns fields roughly agree
645+
# (they should always agree up to around tens-of-microseconds)
646+
for name in names:
647+
floaty = int(getattr(result, name) * 100000)
648+
nanosecondy = getattr(result, name + "_ns") // 10000
649+
self.assertAlmostEqual(floaty, nanosecondy, delta=2, msg=name)
650+
643651
def check_stat_attributes(self, fname):
644652
result = os.stat(fname)
645653

@@ -660,21 +668,15 @@ def trunc(x): return x
660668
result[getattr(stat, name)])
661669
self.assertIn(attr, members)
662670

663-
# Make sure that the st_?time and st_?time_ns fields roughly agree
664-
# (they should always agree up to around tens-of-microseconds)
665-
for name in 'st_atime st_mtime st_ctime'.split():
666-
floaty = int(getattr(result, name) * 100000)
667-
nanosecondy = getattr(result, name + "_ns") // 10000
668-
self.assertAlmostEqual(floaty, nanosecondy, delta=2)
669-
670-
# Ensure both birthtime and birthtime_ns roughly agree, if present
671+
time_attributes = ['st_atime', 'st_mtime', 'st_ctime']
671672
try:
672-
floaty = int(result.st_birthtime * 100000)
673-
nanosecondy = result.st_birthtime_ns // 10000
673+
result.st_birthtime
674+
result.st_birthtime_ns
674675
except AttributeError:
675676
pass
676677
else:
677-
self.assertAlmostEqual(floaty, nanosecondy, delta=2)
678+
time_attributes.append('st_birthtime')
679+
self.check_timestamp_agreement(result, time_attributes)
678680

679681
try:
680682
result[200]
@@ -735,6 +737,82 @@ def test_stat_result_pickle(self):
735737
unpickled = pickle.loads(p)
736738
self.assertEqual(result, unpickled)
737739

740+
def check_statx_attributes(self, fname):
741+
maximal_mask = 0
742+
for name in dir(os):
743+
if name.startswith('STATX_'):
744+
maximal_mask |= getattr(os, name)
745+
result = os.statx(self.fname, maximal_mask)
746+
747+
time_attributes = ('st_atime', 'st_mtime', 'st_ctime', 'st_birthtime')
748+
self.check_timestamp_agreement(result, time_attributes)
749+
750+
# Check that valid attributes match os.stat.
751+
requirements = (
752+
('st_mode', os.STATX_TYPE | os.STATX_MODE),
753+
('st_nlink', os.STATX_NLINK),
754+
('st_uid', os.STATX_UID),
755+
('st_gid', os.STATX_GID),
756+
('st_atime', os.STATX_ATIME),
757+
('st_atime_ns', os.STATX_ATIME),
758+
('st_mtime', os.STATX_MTIME),
759+
('st_mtime_ns', os.STATX_MTIME),
760+
('st_ctime', os.STATX_CTIME),
761+
('st_ctime_ns', os.STATX_CTIME),
762+
('st_ino', os.STATX_INO),
763+
('st_size', os.STATX_SIZE),
764+
('st_blocks', os.STATX_BLOCKS),
765+
('st_birthtime', os.STATX_BTIME),
766+
('st_birthtime_ns', os.STATX_BTIME),
767+
# unconditionally valid members
768+
('st_blksize', 0),
769+
('st_dev', 0),
770+
('st_rdev', 0),
771+
)
772+
basic_result = os.stat(self.fname)
773+
for name, bits in requirements:
774+
if result.stx_mask & bits == bits and hasattr(basic_result, name):
775+
x = getattr(result, name)
776+
b = getattr(basic_result, name)
777+
if isinstance(x, float):
778+
self.assertAlmostEqual(x, b, msg=name)
779+
else:
780+
self.assertEqual(x, b, msg=name)
781+
782+
# Access all the attributes multiple times to test cache refcounting.
783+
members = [name for name in dir(result)
784+
if name.startswith('st_') or name.startswith('stx_')]
785+
for _ in range(10):
786+
for name in members:
787+
getattr(result, name)
788+
789+
self.assertEqual(result.stx_attributes & result.stx_attributes_mask,
790+
result.stx_attributes)
791+
792+
@unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()')
793+
def test_statx_attributes(self):
794+
self.check_statx_attributes(self.fname)
795+
796+
@unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()')
797+
def test_statx_attributes_bytes(self):
798+
try:
799+
fname = self.fname.encode(sys.getfilesystemencoding())
800+
except UnicodeEncodeError:
801+
self.skipTest("cannot encode %a for the filesystem" % self.fname)
802+
self.check_statx_attributes(fname)
803+
804+
@unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()')
805+
def test_statx_attributes_pathlike(self):
806+
self.check_statx_attributes(FakePath(self.fname))
807+
808+
@unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()')
809+
def test_statx_sync(self):
810+
# Test sync= kwarg parsing. (We can't predict if or how the result
811+
# will change.)
812+
for sync in (False, True):
813+
with self.subTest(sync=sync):
814+
os.statx(self.fname, os.STATX_BASIC_STATS, sync=sync)
815+
738816
@unittest.skipUnless(hasattr(os, 'statvfs'), 'test needs os.statvfs()')
739817
def test_statvfs_attributes(self):
740818
result = os.statvfs(self.fname)

Lib/test/test_posix.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,33 +1629,41 @@ def test_chown_dir_fd(self):
16291629
with self.prepare_file() as (dir_fd, name, fullname):
16301630
posix.chown(name, os.getuid(), os.getgid(), dir_fd=dir_fd)
16311631

1632-
@unittest.skipUnless(os.stat in os.supports_dir_fd, "test needs dir_fd support in os.stat()")
1633-
def test_stat_dir_fd(self):
1632+
def check_statlike_dir_fd(self, func):
16341633
with self.prepare() as (dir_fd, name, fullname):
16351634
with open(fullname, 'w') as outfile:
16361635
outfile.write("testline\n")
16371636
self.addCleanup(posix.unlink, fullname)
16381637

1639-
s1 = posix.stat(fullname)
1640-
s2 = posix.stat(name, dir_fd=dir_fd)
1641-
self.assertEqual(s1, s2)
1642-
s2 = posix.stat(fullname, dir_fd=None)
1643-
self.assertEqual(s1, s2)
1638+
s1 = func(fullname)
1639+
s2 = func(name, dir_fd=dir_fd)
1640+
self.assertEqual((s1.st_dev, s1.st_ino), (s2.st_dev, s2.st_ino))
1641+
s2 = func(fullname, dir_fd=None)
1642+
self.assertEqual((s1.st_dev, s1.st_ino), (s2.st_dev, s2.st_ino))
16441643

16451644
self.assertRaisesRegex(TypeError, 'should be integer or None, not',
1646-
posix.stat, name, dir_fd=posix.getcwd())
1645+
func, name, dir_fd=posix.getcwd())
16471646
self.assertRaisesRegex(TypeError, 'should be integer or None, not',
1648-
posix.stat, name, dir_fd=float(dir_fd))
1647+
func, name, dir_fd=float(dir_fd))
16491648
self.assertRaises(OverflowError,
1650-
posix.stat, name, dir_fd=10**20)
1649+
func, name, dir_fd=10**20)
16511650

16521651
for fd in False, True:
16531652
with self.assertWarnsRegex(RuntimeWarning,
16541653
'bool is used as a file descriptor') as cm:
16551654
with self.assertRaises(OSError):
1656-
posix.stat('nonexisting', dir_fd=fd)
1655+
func('nonexisting', dir_fd=fd)
16571656
self.assertEqual(cm.filename, __file__)
16581657

1658+
@unittest.skipUnless(os.stat in os.supports_dir_fd, "test needs dir_fd support in os.stat()")
1659+
def test_stat_dir_fd(self):
1660+
self.check_statlike_dir_fd(posix.stat)
1661+
1662+
@unittest.skipUnless(hasattr(posix, 'statx'), "test needs os.statx()")
1663+
def test_statx_dir_fd(self):
1664+
func = lambda path, **kwargs: posix.statx(path, os.STATX_INO, **kwargs)
1665+
self.check_statlike_dir_fd(func)
1666+
16591667
@unittest.skipUnless(os.utime in os.supports_dir_fd, "test needs dir_fd support in os.utime()")
16601668
def test_utime_dir_fd(self):
16611669
with self.prepare_file() as (dir_fd, name, fullname):

0 commit comments

Comments
 (0)