diff --git a/Doc/library/os.rst b/Doc/library/os.rst index b06f9bbcd831c2..fb773cdaa3f124 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -1841,6 +1841,16 @@ features: function on your platform using :data:`os.supports_follow_symlinks`. If it's unavailable, using it will raise a :exc:`NotImplementedError`. + On Windows, ``follow_symlinks`` applies to name-surrogate reparse points, + including symlinks and junctions. This type of reparse point is a symbolic + link to another path on the system. All other types of reparse point (e.g. + tiered storage) are always traversed, except that reparse points that are + not supported by the system are operated on directly. + + If a path is either a symlink or a junction that cannot be traversed to the + final path, :func:`os.path.realpath` can be used to resolve as much of the + target path as possible. + .. function:: access(path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True) @@ -2795,17 +2805,6 @@ features: This function can support :ref:`specifying a file descriptor ` and :ref:`not following symlinks `. - On Windows, passing ``follow_symlinks=False`` will disable following all - name-surrogate reparse points, which includes symlinks and directory - junctions. Other types of reparse points that do not resemble links or that - the operating system is unable to follow will be opened directly. When - following a chain of multiple links, this may result in the original link - being returned instead of the non-link that prevented full traversal. To - obtain stat results for the final path in this case, use the - :func:`os.path.realpath` function to resolve the path name as far as - possible and call :func:`lstat` on the result. This does not apply to - dangling symlinks or junction points, which will raise the usual exceptions. - .. index:: module: stat Example:: @@ -2839,11 +2838,71 @@ features: ``follow_symlinks=False`` had been specified instead of raising an error. +.. function:: statx(path, mask, *, dir_fd=None, follow_symlinks=True, flags=0) + + Get selected fields of the status of a file or a file descriptor. Perform the + equivalent of a :c:func:`statx` system call on the given path. *path* may be + specified as either a string or bytes -- directly or indirectly through the + :class:`PathLike` interface -- or as an open file descriptor. Return a + :class:`stat_result` object. + + *mask* is a bitwise combination of the ``STATX_*`` attributes in the + :mod:`stat` module, indicating which fields the caller intends to use. Note + that the set of fields returned may differ from what's requested, if the + operating system or file system does not support the metadata, or if it can + provide additional fields with no extra effort. + + This function normally follows symlinks; to stat a symlink add the argument + ``follow_symlinks=False``. + + This function is present even if the operating system does not support + ``statx``. In this case, the *mask* argument is ignored and a regular + :func:`stat`, :func:`lstat` or :func:`fstat` call will be used. + The :attr:`stat_result.stx_mask` field is always present, and is initialized + with a meaningful value for regular stat calls. + + This function can support :ref:`specifying a file descriptor ` and + :ref:`not following symlinks `. + + .. index:: module: stat + + Example:: + + >>> import os, stat + >>> statinfo = os.statx('somefile.txt', stat.STATX_SIZE) + >>> statinfo + os.stat_result(st_mode=33188, st_ino=0, st_dev=0, + st_nlink=1, st_uid=0, st_gid=0, st_size=264, st_atime=0, + st_mtime=0, st_ctime=0) + >>> statinfo.stx_mask & stat.STATX_SIZE + 512 + >>> statinfo.st_size + 264 + + .. seealso:: + + :func:`stat`, :func:`fstat` and :func:`lstat` functions. + + .. versionadded:: 3.12 + Added ``statx`` function. + + .. class:: stat_result Object whose attributes correspond roughly to the members of the :c:type:`stat` structure. It is used for the result of :func:`os.stat`, - :func:`os.fstat` and :func:`os.lstat`. + :func:`os.fstat`, :func:`os.lstat` and :func:`os.statx`. + + Attributes that are optional under :func:`os.stax` calls are documented + with the mask value that will be present in :attr:`stx_mask` when the + value is present. This can be tested even for regular ``stat`` calls, + as ``stx_mask`` will be initialized with a suitable value even if the + underlying system call uses regular ``stat``. + + Attributes may be missing from this struct if they are not supported by + the underlying operating system. When :func:`os.statx` is supported, all + ``stx_*`` attributes are present, but their ``st_*`` equivalents may not + be. :attr:`stx_mask` is always present, even if ``statx`` is not supported. Attributes: @@ -2851,6 +2910,9 @@ features: File mode: file type and file mode bits (permissions). + This field is valid when ``stx_mask`` contains + :data:`stat.STATX_TYPE` and/or :data:`stat.STATX_MODE`. + .. attribute:: st_ino Platform dependent, but if non-zero, uniquely identifies the @@ -2861,70 +2923,130 @@ features: `_ on Windows + This field is valid when ``stx_mask`` contains :data:`stat.STATX_INO`. + .. attribute:: st_dev Identifier of the device on which this file resides. + On Windows, this field is valid with :data:`stat.STATX_INO`. + Other platforms always set this value. + .. attribute:: st_nlink Number of hard links. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_NLINK`. + .. attribute:: st_uid User identifier of the file owner. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_UID`. + .. attribute:: st_gid Group identifier of the file owner. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_GID`. + .. attribute:: st_size Size of the file in bytes, if it is a regular file or a symbolic link. The size of a symbolic link is the length of the pathname it contains, without a terminating null byte. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_SIZE`. + + .. attribute:: stx_mask + + Flags indicating which values in the result are valid. :func:`os.statx` + allows specifying a mask, though the result may include more or less + than requested. Other ``stat`` functions set a default value representing + the information they return. + Timestamps: .. attribute:: st_atime Time of most recent access expressed in seconds. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_ATIME`. + .. attribute:: st_mtime Time of most recent content modification expressed in seconds. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_MTIME`. + .. attribute:: st_ctime Platform dependent: * the time of most recent metadata change on Unix, - * the time of creation on Windows, expressed in seconds. + * the time of creation on Windows, expressed in seconds, except + when :data:`stat.STATX_CTIME` is in :attr:`stx_mask`, in which + case this is the time of the most recent metadata change. + Regular stat calls will set this field without setting the mask. + + This field is valid when ``stx_mask`` contains :data:`stat.STATX_CTIME`. .. attribute:: st_atime_ns Time of most recent access expressed in nanoseconds as an integer. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_ATIME`. + .. attribute:: st_mtime_ns Time of most recent content modification expressed in nanoseconds as an integer. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_MTIME`. + .. attribute:: st_ctime_ns Platform dependent: * the time of most recent metadata change on Unix, * the time of creation on Windows, expressed in nanoseconds as an - integer. + integer, except when :data:`stat.STATX_CTIME` is in :attr:`stx_mask`, + in which case this is the time of the most recent metadata change. + Regular stat calls will set this field without setting the mask. + + This field is valid when ``stx_mask`` contains :data:`stat.STATX_CTIME`. + + .. attribute:: st_birthtime + + Time of file creation, if available. The attribute may not be present if + your operating system does not support the field. + + This field is valid with :data:`stat.STATX_BTIME`, but is only present when + supported by regular ``stat`` calls. See also: :attr:`stx_btime`. + + .. attribute:: stx_btime + + Alias of :attr:`st_birthtime` that is always present when :func:`statx` + is supported. If ``st_birthtime`` is also present, its value will be + identical. + + This field is valid when ``stx_mask`` contains :data:`stat.STATX_BTIME`. + + .. attribute:: stx_btime_ns + + Time of file creation expressed in nanoseconds as an integer. + + This field is valid when ``stx_mask`` contains :data:`stat.STATX_BTIME`. .. note:: The exact meaning and resolution of the :attr:`st_atime`, - :attr:`st_mtime`, and :attr:`st_ctime` attributes depend on the operating - system and the file system. For example, on Windows systems using the FAT - or FAT32 file systems, :attr:`st_mtime` has 2-second resolution, and - :attr:`st_atime` has only 1-day resolution. See your operating system - documentation for details. + :attr:`st_mtime`, :attr:`st_ctime`, :attr:`st_birthtime` and + :attr:`stx_btime` attributes depend on the operating system and the file + system. For example, on Windows systems using the FAT or FAT32 file + systems, :attr:`st_mtime` has 2-second resolution, and :attr:`st_atime` + has only 1-day resolution. + See your operating system documentation for details. Similarly, although :attr:`st_atime_ns`, :attr:`st_mtime_ns`, and :attr:`st_ctime_ns` are always expressed in nanoseconds, many @@ -2943,19 +3065,64 @@ features: Number of 512-byte blocks allocated for file. This may be smaller than :attr:`st_size`/512 when the file has holes. + This field is valid with :data:`stat.STATX_BLOCKS`, but is only present + when supported by regular ``stat`` calls. See also: :attr:`stx_blocks`. + .. attribute:: st_blksize "Preferred" blocksize for efficient file system I/O. Writing to a file in smaller chunks may cause an inefficient read-modify-rewrite. + This field is valid with :data:`stat.STATX_BLOCKSIZE`, but is only present + when supported by regular ``stat`` calls. See also: :attr:`stx_blksize`. + .. attribute:: st_rdev Type of device if an inode device. + This field is always valid when present, but is only present when + supported by regular ``stat`` calls. See also: :attr:`stx_rdev`. + .. attribute:: st_flags User defined flags for file. + .. attribute:: stx_attributes + + Additional attribute flags (``STATX_ATTR_*`` values). + + This field is only set for calls using :func:`os.statx`. + + .. attribute:: stx_attributes_mask + + Attribute flags (``STATX_ATTR_*``) that were supported on the file system + containing the file. Flags not set in this mask are meaningless in + :attr:`stx_attributes`. + + This field is only set for calls using :func:`os.statx`. + + .. attribute:: stx_blocks + + Alias of :attr:`st_blocks` that is always present when :func:`statx` is + supported. If ``st_blocks`` is also present, its value will be identical. + + This field is set with :data:`stat.STATX_BLOCKS`. + + .. attribute:: stx_blksize + + Alias of :attr:`st_blksize` that is always present when :func:`statx` is + supported. If ``st_blksize`` is also present, its value will be identical. + + This field is set with :data:`stat.STATX_BLKSIZE`. + + .. attribute:: stx_rdev + + Alias of :attr:`st_rdev` that is always present when :func:`statx` is + supported. If ``st_rdev`` is also present, its value will be identical. + + This field is always set, when appropriate, by all ``stat`` calls. + + On other Unix systems (such as FreeBSD), the following attributes may be available (but may be only filled out if root tries to use them): @@ -2963,10 +3130,6 @@ features: File generation number. - .. attribute:: st_birthtime - - Time of file creation. - On Solaris and derivatives, the following attributes may also be available: @@ -2998,12 +3161,16 @@ features: :c:func:`GetFileInformationByHandle`. See the ``FILE_ATTRIBUTE_*`` constants in the :mod:`stat` module. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_TYPE`. + .. attribute:: st_reparse_tag When :attr:`st_file_attributes` has the ``FILE_ATTRIBUTE_REPARSE_POINT`` set, this field contains the tag identifying the type of reparse point. See the ``IO_REPARSE_TAG_*`` constants in the :mod:`stat` module. + This field is valid when ``stx_mask`` contains :data:`stat.STATX_TYPE`. + The standard module :mod:`stat` defines functions and constants that are useful for extracting information from a :c:type:`stat` structure. (On Windows, some items are filled with dummy values.) @@ -3039,6 +3206,13 @@ features: files as :const:`S_IFCHR`, :const:`S_IFIFO` or :const:`S_IFBLK` as appropriate. + .. versionchanged:: 3.12 + Added :attr:`stx_mask` and other ``stx_*`` members along with :func:`statx`. + + .. versionchanged:: 3.12 + Added the :attr:`st_birthtime` member on Windows. + + .. function:: statvfs(path) Perform a :c:func:`statvfs` system call on the given path. The return value is diff --git a/Include/internal/pycore_fileutils.h b/Include/internal/pycore_fileutils.h index ac89c43d569c07..b9276531663a6a 100644 --- a/Include/internal/pycore_fileutils.h +++ b/Include/internal/pycore_fileutils.h @@ -82,6 +82,8 @@ struct _Py_stat_struct { int st_ctime_nsec; unsigned long st_file_attributes; unsigned long st_reparse_tag; + time_t st_btime; + int st_btime_nsec; }; #else # define _Py_stat_struct stat diff --git a/Include/internal/pycore_fileutils_windows.h b/Include/internal/pycore_fileutils_windows.h new file mode 100644 index 00000000000000..82e72b36494c59 --- /dev/null +++ b/Include/internal/pycore_fileutils_windows.h @@ -0,0 +1,77 @@ +#ifndef Py_INTERNAL_FILEUTILS_WINDOWS_H +#define Py_INTERNAL_FILEUTILS_WINDOWS_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "Py_BUILD_CORE must be defined to include this header" +#endif + +#ifdef MS_WINDOWS + +#if !defined(NTDDI_WIN10_NI) || !(NTDDI_VERSION >= NTDDI_WIN10_NI) +typedef struct _FILE_STAT_BASIC_INFORMATION { + LARGE_INTEGER FileId; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG FileAttributes; + ULONG ReparseTag; + ULONG NumberOfLinks; + ULONG DeviceType; + ULONG DeviceCharacteristics; +} FILE_STAT_BASIC_INFORMATION; + +typedef enum _FILE_INFO_BY_NAME_CLASS { + FileStatByNameInfo, + FileStatLxByNameInfo, + FileCaseSensitiveByNameInfo, + FileStatBasicByNameInfo, + MaximumFileInfoByNameClass +} FILE_INFO_BY_NAME_CLASS; +#endif + +typedef BOOL (WINAPI *PGetFileInformationByName)( + PCWSTR FileName, + FILE_INFO_BY_NAME_CLASS FileInformationClass, + PVOID FileInfoBuffer, + ULONG FileInfoBufferSize +); + +static inline BOOL _Py_GetFileInformationByName( + PCWSTR FileName, + FILE_INFO_BY_NAME_CLASS FileInformationClass, + PVOID FileInfoBuffer, + ULONG FileInfoBufferSize +) { + static PGetFileInformationByName GetFileInformationByName = NULL; + static int GetFileInformationByName_init = -1; + + if (GetFileInformationByName_init < 0) { + HMODULE hMod = LoadLibraryW(L"api-ms-win-core-file-l2-1-4"); + GetFileInformationByName_init = 0; + if (hMod) { + GetFileInformationByName = (PGetFileInformationByName)GetProcAddress( + hMod, "GetFileInformationByName"); + if (GetFileInformationByName) { + GetFileInformationByName_init = 1; + } else { + FreeLibrary(hMod); + } + } + } + + if (GetFileInformationByName_init <= 0) { + SetLastError(ERROR_NOT_SUPPORTED); + return FALSE; + } + return GetFileInformationByName(FileName, FileInformationClass, FileInfoBuffer, FileInfoBufferSize); +} + +#endif + +#endif diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 6aba2f19ebde4a..252e3670dba23c 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1012,6 +1012,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(logoption)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(loop)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mapping)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mask)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(match)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(max_length)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxdigits)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index acb9a4fbb92dce..aecc1f01055ee8 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -498,6 +498,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(logoption) STRUCT_FOR_ID(loop) STRUCT_FOR_ID(mapping) + STRUCT_FOR_ID(mask) STRUCT_FOR_ID(match) STRUCT_FOR_ID(max_length) STRUCT_FOR_ID(maxdigits) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 6d1b8702c77698..15d52ad1638221 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1004,6 +1004,7 @@ extern "C" { INIT_ID(logoption), \ INIT_ID(loop), \ INIT_ID(mapping), \ + INIT_ID(mask), \ INIT_ID(match), \ INIT_ID(max_length), \ INIT_ID(maxdigits), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 7f407c0141b8a5..cee023aff67be6 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -902,6 +902,8 @@ _PyUnicode_InitStaticStrings(void) { PyUnicode_InternInPlace(&string); string = &_Py_ID(mapping); PyUnicode_InternInPlace(&string); + string = &_Py_ID(mask); + PyUnicode_InternInPlace(&string); string = &_Py_ID(match); PyUnicode_InternInPlace(&string); string = &_Py_ID(max_length); diff --git a/Lib/filecmp.py b/Lib/filecmp.py index 30bd900fa805aa..a534131c5b4d5b 100644 --- a/Lib/filecmp.py +++ b/Lib/filecmp.py @@ -50,8 +50,8 @@ def cmp(f1, f2, shallow=True): """ - s1 = _sig(os.stat(f1)) - s2 = _sig(os.stat(f2)) + s1 = _sig(os.statx(f1, stat.STATX_TYPE | stat.STATX_SIZE | stat.STATX_MTIME)) + s2 = _sig(os.statx(f2, stat.STATX_TYPE | stat.STATX_SIZE | stat.STATX_MTIME)) if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG: return False if shallow and s1 == s2: @@ -159,12 +159,12 @@ def phase2(self): # Distinguish files, directories, funnies ok = True try: - a_stat = os.stat(a_path) + a_stat = os.statx(a_path, stat.STATX_TYPE) except OSError: # print('Can\'t stat', a_path, ':', why.args[1]) ok = False try: - b_stat = os.stat(b_path) + b_stat = os.statx(b_path, stat.STATX_TYPE) except OSError: # print('Can\'t stat', b_path, ':', why.args[1]) ok = False diff --git a/Lib/genericpath.py b/Lib/genericpath.py index ce36451a3af01c..4fefcc976834d5 100644 --- a/Lib/genericpath.py +++ b/Lib/genericpath.py @@ -16,7 +16,7 @@ def exists(path): """Test whether a path exists. Returns False for broken symbolic links""" try: - os.stat(path) + os.statx(path, stat.STATX_TYPE) except (OSError, ValueError): return False return True @@ -27,7 +27,7 @@ def exists(path): def isfile(path): """Test whether a path is a regular file""" try: - st = os.stat(path) + st = os.statx(path, stat.STATX_TYPE) except (OSError, ValueError): return False return stat.S_ISREG(st.st_mode) @@ -39,7 +39,7 @@ def isfile(path): def isdir(s): """Return true if the pathname refers to an existing directory.""" try: - st = os.stat(s) + st = os.statx(s, stat.STATX_TYPE) except (OSError, ValueError): return False return stat.S_ISDIR(st.st_mode) @@ -47,21 +47,23 @@ def isdir(s): def getsize(filename): """Return the size of a file, reported by os.stat().""" - return os.stat(filename).st_size + return os.statx(filename, stat.STATX_SIZE).st_size def getmtime(filename): """Return the last modification time of a file, reported by os.stat().""" - return os.stat(filename).st_mtime + return os.statx(filename, stat.STATX_MTIME).st_mtime def getatime(filename): """Return the last access time of a file, reported by os.stat().""" - return os.stat(filename).st_atime + return os.statx(filename, stat.STATX_ATIME).st_atime def getctime(filename): """Return the metadata change time of a file, reported by os.stat().""" + # XXX: If we change to statx, st_ctime on Windows will start returning + # change time instead of creation time. return os.stat(filename).st_ctime @@ -86,6 +88,8 @@ def commonprefix(m): # describing the same file? def samestat(s1, s2): """Test whether two stat buffers reference the same file""" + if not s1.stx_mask & s2.stx_mask & stat.STATX_INO: + raise ValueError("stat values must include STATX_INO") return (s1.st_ino == s2.st_ino and s1.st_dev == s2.st_dev) @@ -97,8 +101,8 @@ def samefile(f1, f2): This is determined by the device number and i-node number and raises an exception if an os.stat() call on either pathname fails. """ - s1 = os.stat(f1) - s2 = os.stat(f2) + s1 = os.statx(f1, stat.STATX_INO) + s2 = os.statx(f2, stat.STATX_INO) return samestat(s1, s2) @@ -106,8 +110,8 @@ def samefile(f1, f2): # (Not necessarily the same file descriptor!) def sameopenfile(fp1, fp2): """Test whether two open file objects reference the same file""" - s1 = os.fstat(fp1) - s2 = os.fstat(fp2) + s1 = os.statx(fp1, stat.STATX_INO) + s2 = os.statx(fp2, stat.STATX_INO) return samestat(s1, s2) diff --git a/Lib/glob.py b/Lib/glob.py index a7256422d520fb..30759bfdef4372 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -193,7 +193,7 @@ def _lexists(pathname, dir_fd): if dir_fd is None: return os.path.lexists(pathname) try: - os.lstat(pathname, dir_fd=dir_fd) + os.statx(pathname, stat.STATX_TYPE, dir_fd=dir_fd, follow_symlinks=False) except (OSError, ValueError): return False else: @@ -204,7 +204,7 @@ def _isdir(pathname, dir_fd): if dir_fd is None: return os.path.isdir(pathname) try: - st = os.stat(pathname, dir_fd=dir_fd) + st = os.statx(pathname, stat.STATX_TYPE, dir_fd=dir_fd) except (OSError, ValueError): return False else: diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 71a16064b8ec0a..259586a283e02d 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -137,20 +137,32 @@ def _path_split(path): return path[:i], path[i + 1:] -def _path_stat(path): - """Stat the path. +# Check for _os.statx because we've imported posixmodule directly and our +# fallback method is in os.py and we won't see it. +if hasattr(_os, 'statx'): + def _path_stat(path, mask=0x0241): + """Stat the path. - Made a separate function to make it easier to override in experiments - (e.g. cache stat results). + Made a separate function to make it easier to override in experiments + (e.g. cache stat results). - """ - return _os.stat(path) + Default mask is STATX_SIZE | STATX_MTIME | STATX_TYPE + """ + return _os.statx(path, mask) +else: + def _path_stat(path, mask=None): + """Stat the path. + + Made a separate function to make it easier to override in experiments + (e.g. cache stat results). + """ + return _os.stat(path) def _path_is_mode_type(path, mode): """Test whether the path is the specified mode type.""" try: - stat_info = _path_stat(path) + stat_info = _path_stat(path, mask=0x01) except OSError: return False return (stat_info.st_mode & 0o170000) == mode diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index 40ab1a1aaac328..2e683cbba5ef13 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -13,6 +13,7 @@ import itertools import posixpath import collections +import stat from . import _adapters, _meta from ._collections import FreezableDefaultDict, Pair @@ -628,7 +629,7 @@ def search(self, name): @property def mtime(self): with suppress(OSError): - return os.stat(self.root).st_mtime + return os.statx(self.root, stat.STATX_MTIME).st_mtime self.lookup.cache_clear() @method_cache diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index 9847104446eaf6..82073901f34ae8 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -24,7 +24,7 @@ """ import io, logging, socket, os, pickle, struct, time, re -from stat import ST_DEV, ST_INO, ST_MTIME +from stat import ST_DEV, ST_INO, ST_MTIME, STATX_INO, STATX_MTIME import queue import threading import copy @@ -263,7 +263,7 @@ def __init__(self, filename, when='h', interval=1, backupCount=0, # path object (see Issue #27493), but self.baseFilename will be a string filename = self.baseFilename if os.path.exists(filename): - t = os.stat(filename)[ST_MTIME] + t = os.statx(filename, STATX_MTIME)[ST_MTIME] else: t = int(time.time()) self.rolloverAt = self.computeRollover(t) @@ -484,7 +484,7 @@ def __init__(self, filename, mode='a', encoding=None, delay=False, def _statstream(self): if self.stream: - sres = os.fstat(self.stream.fileno()) + sres = os.statx(self.stream.fileno(), STATX_INO) self.dev, self.ino = sres[ST_DEV], sres[ST_INO] def reopenIfNeeded(self): @@ -501,7 +501,7 @@ def reopenIfNeeded(self): # and patch. try: # stat the file by path, checking for existence - sres = os.stat(self.baseFilename) + sres = os.statx(self.baseFilename, STATX_INO) except FileNotFoundError: sres = None # compare file system stat with that of our stream file handle diff --git a/Lib/mailbox.py b/Lib/mailbox.py index 59834a2b3b5243..ef4a590781908c 100644 --- a/Lib/mailbox.py +++ b/Lib/mailbox.py @@ -18,6 +18,7 @@ import email.generator import io import contextlib +import stat from types import GenericAlias try: import fcntl @@ -499,7 +500,7 @@ def _create_tmp(self): Maildir._count, hostname) path = os.path.join(self._path, 'tmp', uniq) try: - os.stat(path) + os.statx(path, stat.STATX_TYPE) except FileNotFoundError: Maildir._count += 1 try: @@ -699,7 +700,7 @@ def flush(self): # self._file is about to get replaced, so no need to sync. self._file.close() # Make sure the new file's mode is the same as the old file's - mode = os.stat(self._path).st_mode + mode = os.statx(self._path, stat.STATX_MODE).st_mode os.chmod(new_file.name, mode) try: os.rename(new_file.name, self._path) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 265eaa8d4b953f..43dbb862a3a377 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -262,7 +262,7 @@ def islink(path): This will always return false for Windows prior to 6.0. """ try: - st = os.lstat(path) + st = os.statx(path, stat.STATX_TYPE, follow_symlinks=False) except (OSError, ValueError, AttributeError): return False return stat.S_ISLNK(st.st_mode) @@ -274,7 +274,7 @@ def islink(path): def isjunction(path): """Test whether a path is a junction""" try: - st = os.lstat(path) + st = os.statx(path, stat.STATX_TYPE, follow_symlinks=False) except (OSError, ValueError, AttributeError): return False return bool(st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT) @@ -290,7 +290,7 @@ def isjunction(path): def lexists(path): """Test whether a path exists. Returns True for broken symbolic links""" try: - st = os.lstat(path) + st = os.statx(path, stat.STATX_TYPE, follow_symlinks=False) except (OSError, ValueError): return False return True diff --git a/Lib/os.py b/Lib/os.py index fd1e774fdcbcfa..e0ce7d426a38e0 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -118,6 +118,7 @@ def _add(str, fn): _add("HAVE_OPENAT", "open") _add("HAVE_READLINKAT", "readlink") _add("HAVE_RENAMEAT", "rename") + _add("HAVE_STATX", "statx") _add("HAVE_SYMLINKAT", "symlink") _add("HAVE_UNLINKAT", "unlink") _add("HAVE_UNLINKAT", "rmdir") @@ -140,6 +141,7 @@ def _add(str, fn): _add("HAVE_FUTIMENS", "utime") _add("HAVE_FUTIMES", "utime") _add("HAVE_FPATHCONF", "pathconf") + _add("HAVE_STATX", "statx") if _exists("statvfs") and _exists("fstatvfs"): # mac os x10.3 _add("HAVE_FSTATVFS", "statvfs") supports_fd = _set @@ -178,7 +180,9 @@ def _add(str, fn): _add("HAVE_LSTAT", "stat") _add("HAVE_FSTATAT", "stat") _add("HAVE_UTIMENSAT", "utime") + _add("HAVE_STATX", "statx") _add("MS_WINDOWS", "stat") + _add("MS_WINDOWS", "statx") supports_follow_symlinks = _set del _set @@ -1086,6 +1090,23 @@ def __subclasshook__(cls, subclass): __class_getitem__ = classmethod(GenericAlias) +# If there is no C implementation, make a pure Python version +if not _exists('statx'): + def statx(path, mask, *, dir_fd=None, follow_symlinks=True, flags=0): + """Perform a stat system call on the given path, retrieving certain information. + +This is a fallback implementation that calls stat() or lstat() based on the +*follow_symlinks* argument. The *mask* argument is ignored and the stx_mask +result will be set for the information returned by a normal stat() call. +""" + return (stat if follow_symlinks else lstat)(path, dir_fd=dir_fd) + + # Ensure our supports sets include us based on the fallback functions + supports_fd.add(statx) + if stat in supports_follow_symlinks: + supports_follow_symlinks.add(statx) + if stat in supports_dir_fd: + supports_dir_fd.add(statx) if name == 'nt': class _AddedDllDirectory: diff --git a/Lib/pathlib.py b/Lib/pathlib.py index f31eb3010368d5..f120253b6cf878 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -17,7 +17,8 @@ from _collections_abc import Sequence from errno import ENOENT, ENOTDIR, EBADF, ELOOP from operator import attrgetter -from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO +from stat import (S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO, + STATX_TYPE, STATX_GID, STATX_UID) from urllib.parse import quote_from_bytes as urlquote_from_bytes @@ -914,17 +915,20 @@ def check_eloop(e): # Ensure we get an exception by calling stat() if not strict: try: - p.stat() + p.stat(mask=STATX_TYPE) except OSError as e: check_eloop(e) return p - def stat(self, *, follow_symlinks=True): + def stat(self, *, follow_symlinks=True, mask=None): """ Return the result of the stat() system call on this path, like os.stat() does. """ - return os.stat(self, follow_symlinks=follow_symlinks) + if mask is not None: + return os.statx(self, mask, follow_symlinks=follow_symlinks) + else: + return os.stat(self, follow_symlinks=follow_symlinks) def owner(self): """ @@ -932,7 +936,7 @@ def owner(self): """ try: import pwd - return pwd.getpwuid(self.stat().st_uid).pw_name + return pwd.getpwuid(self.stat(mask=STATX_UID).st_uid).pw_name except ImportError: raise NotImplementedError("Path.owner() is unsupported on this system") @@ -943,7 +947,7 @@ def group(self): try: import grp - return grp.getgrgid(self.stat().st_gid).gr_name + return grp.getgrgid(self.stat(mask=STATX_GID).st_gid).gr_name except ImportError: raise NotImplementedError("Path.group() is unsupported on this system") @@ -1069,12 +1073,12 @@ def rmdir(self): """ os.rmdir(self) - def lstat(self): + def lstat(self, *, mask=None): """ Like stat(), except if the path points to a symlink, the symlink's status information is returned, rather than its target's. """ - return self.stat(follow_symlinks=False) + return self.stat(follow_symlinks=False, mask=mask) def rename(self, target): """ @@ -1129,7 +1133,7 @@ def exists(self): Whether this path exists. """ try: - self.stat() + self.stat(mask=STATX_TYPE) except OSError as e: if not _ignore_error(e): raise @@ -1144,7 +1148,7 @@ def is_dir(self): Whether this path is a directory. """ try: - return S_ISDIR(self.stat().st_mode) + return S_ISDIR(self.stat(mask=STATX_TYPE).st_mode) except OSError as e: if not _ignore_error(e): raise @@ -1161,7 +1165,7 @@ def is_file(self): to regular files). """ try: - return S_ISREG(self.stat().st_mode) + return S_ISREG(self.stat(mask=STATX_TYPE).st_mode) except OSError as e: if not _ignore_error(e): raise @@ -1183,7 +1187,7 @@ def is_symlink(self): Whether this path is a symbolic link. """ try: - return S_ISLNK(self.lstat().st_mode) + return S_ISLNK(self.lstat(mask=STATX_TYPE).st_mode) except OSError as e: if not _ignore_error(e): raise @@ -1204,7 +1208,7 @@ def is_block_device(self): Whether this path is a block device. """ try: - return S_ISBLK(self.stat().st_mode) + return S_ISBLK(self.stat(mask=STATX_TYPE).st_mode) except OSError as e: if not _ignore_error(e): raise @@ -1220,7 +1224,7 @@ def is_char_device(self): Whether this path is a character device. """ try: - return S_ISCHR(self.stat().st_mode) + return S_ISCHR(self.stat(mask=STATX_TYPE).st_mode) except OSError as e: if not _ignore_error(e): raise @@ -1236,7 +1240,7 @@ def is_fifo(self): Whether this path is a FIFO. """ try: - return S_ISFIFO(self.stat().st_mode) + return S_ISFIFO(self.stat(mask=STATX_TYPE).st_mode) except OSError as e: if not _ignore_error(e): raise @@ -1252,7 +1256,7 @@ def is_socket(self): Whether this path is a socket. """ try: - return S_ISSOCK(self.stat().st_mode) + return S_ISSOCK(self.stat(mask=STATX_TYPE).st_mode) except OSError as e: if not _ignore_error(e): raise diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 737f8a5c156d81..58e9aa8dfe1e0f 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -164,7 +164,7 @@ def dirname(p): def islink(path): """Test whether a path is a symbolic link""" try: - st = os.lstat(path) + st = os.statx(path, stat.STATX_TYPE, follow_symlinks=False) except (OSError, ValueError, AttributeError): return False return stat.S_ISLNK(st.st_mode) @@ -184,7 +184,7 @@ def isjunction(path): def lexists(path): """Test whether a path exists. Returns True for broken symbolic links""" try: - os.lstat(path) + os.statx(path, stat.STATX_TYPE, follow_symlinks=False) except (OSError, ValueError): return False return True @@ -196,7 +196,7 @@ def lexists(path): def ismount(path): """Test whether a path is a mount point""" try: - s1 = os.lstat(path) + s1 = os.statx(path, stat.STATX_TYPE | stat.STATX_INO, follow_symlinks=False) except (OSError, ValueError): # It doesn't exist -- so not a mount point. :-) return False @@ -212,7 +212,7 @@ def ismount(path): parent = join(path, '..') parent = realpath(parent) try: - s2 = os.lstat(parent) + s2 = os.statx(parent, stat.STATX_TYPE | stat.STATX_INO, follow_symlinks=False) except (OSError, ValueError): return False @@ -458,7 +458,7 @@ def _joinrealpath(path, rest, strict, seen): continue newpath = join(path, name) try: - st = os.lstat(newpath) + st = os.statx(newpath, stat.STATX_TYPE, follow_symlinks=False) except OSError: if strict: raise @@ -478,7 +478,7 @@ def _joinrealpath(path, rest, strict, seen): # The symlink is not resolved, so we must have a symlink loop. if strict: # Raise OSError(errno.ELOOP) - os.stat(newpath) + os.statx(newpath, stat.STATX_TYPE) else: # Return already resolved part + rest of the path unchanged. return join(newpath, rest), False diff --git a/Lib/pstats.py b/Lib/pstats.py index 51bcca84188740..bb941b418d9330 100644 --- a/Lib/pstats.py +++ b/Lib/pstats.py @@ -22,6 +22,7 @@ import sys import os +import stat import time import marshal import re @@ -142,7 +143,7 @@ def load_stats(self, arg): with open(arg, 'rb') as f: self.stats = marshal.load(f) try: - file_stats = os.stat(arg) + file_stats = os.statx(arg, stat.STATX_MTIME) arg = time.ctime(file_stats.st_mtime) + " " + arg except: # in case this is not unix pass diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 0a693f45230c93..eed6acf95e9593 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -66,6 +66,7 @@ class or function within a module or module in a package. If the import pkgutil import platform import re +import stat import sys import sysconfig import time @@ -349,7 +350,7 @@ def source_synopsis(file): def synopsis(filename, cache={}): """Get the one-line summary out of a module file.""" - mtime = os.stat(filename).st_mtime + mtime = os.statx(filename, stat.STATX_MTIME).st_mtime lastupdate, result = cache.get(filename, (None, None)) if lastupdate is None or lastupdate < mtime: # Look for binary suffixes first, falling back to source. diff --git a/Lib/shutil.py b/Lib/shutil.py index 867925aa10cc04..ebcc332fced6d0 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -130,7 +130,7 @@ def _fastcopy_sendfile(fsrc, fdst): # should not make any difference, also in case the file content # changes while being copied. try: - blocksize = max(os.fstat(infd).st_size, 2 ** 23) # min 8MiB + blocksize = max(os.statx(infd, stat.STATX_SIZE).st_size, 2 ** 23) # min 8MiB except OSError: blocksize = 2 ** 27 # 128MiB # On 32-bit architectures truncate to 1GiB to avoid OverflowError, @@ -216,7 +216,9 @@ def _samefile(src, dst): os.path.normcase(os.path.abspath(dst))) def _stat(fn): - return fn.stat() if isinstance(fn, os.DirEntry) else os.stat(fn) + return (fn.stat() + if isinstance(fn, os.DirEntry) + else os.statx(fn, stat.STATX_TYPE | stat.STATX_SIZE)) def _islink(fn): return fn.is_symlink() if isinstance(fn, os.DirEntry) else os.path.islink(fn) @@ -564,7 +566,7 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, if hasattr(os.stat_result, 'st_file_attributes'): def _rmtree_islink(path): try: - st = os.lstat(path) + st = os.statx(path, stat.STATX_TYPE, follow_symlinks=False) return (stat.S_ISLNK(st.st_mode) or (st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT)) diff --git a/Lib/stat.py b/Lib/stat.py index fc024db3f4fbee..de19b943f472b9 100644 --- a/Lib/stat.py +++ b/Lib/stat.py @@ -166,6 +166,24 @@ def filemode(mode): return "".join(perm) +# Mask values for statx() +STATX_TYPE = 0x00000001 # includes st_file_attributes and st_reparse_tag +STATX_MODE = 0x00000002 +STATX_NLINK = 0x00000004 +STATX_UID = 0x00000008 +STATX_GID = 0x00000010 +STATX_ATIME = 0x00000020 +STATX_MTIME = 0x00000040 +STATX_CTIME = 0x00000080 +STATX_INO = 0x00000100 # includes st_dev on Windows +STATX_SIZE = 0x00000200 +STATX_BLOCKS = 0x00000400 +STATX_BASIC_STATS = 0x000007ff +STATX_BTIME = 0x00000800 +STATX_MNT_ID = 0x00001000 +STATX_DIOALIGN = 0x00002000 + + # Windows FILE_ATTRIBUTE constants for interpreting os.stat()'s # "st_file_attributes" member diff --git a/Lib/tempfile.py b/Lib/tempfile.py index bb18d60db0d919..ee3292af172ce0 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -73,15 +73,6 @@ _once_lock = _allocate_lock() -def _exists(fn): - try: - _os.lstat(fn) - except OSError: - return False - else: - return True - - def _infer_return_type(*args): """Look at the type of all args and divine their implied return type.""" return_type = None @@ -406,7 +397,7 @@ def mktemp(suffix="", prefix=template, dir=None): for seq in range(TMP_MAX): name = next(names) file = _os.path.join(dir, prefix + name + suffix) - if not _exists(file): + if not _os.path.exists(file): return file raise FileExistsError(_errno.EEXIST, diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index adc211b3e2169c..480ff0a815556d 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -213,6 +213,8 @@ def format_attr(attr, value): ) copy_attributes(info_add, os, 'os.%s', attributes, formatter=format_attr) + info_add('os.HAVE_STATX', hasattr(sys.modules[os.name], 'statx')) + for func in ( 'cpu_count', 'getcwd', diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 2dda7ccf7bf80c..573047da568b69 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -714,8 +714,10 @@ def check_config(self, configs, expected): if MS_WINDOWS: value = config.get(key := 'program_name') if value and isinstance(value, str): - ext = '_d.exe' if debug_build(sys.executable) else '.exe' - config[key] = value[:len(value.lower().removesuffix(ext))] + value = value[:len(value.lower().removesuffix('.exe'))] + if debug_build(sys.executable): + value = value[:len(value.lower().removesuffix('_d'))] + config[key] = value for key, value in list(expected.items()): if value is self.IGNORE_CONFIG: config.pop(key, None) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index e0577916428a08..b7a5099ac5d2f5 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -526,11 +526,19 @@ class StatAttributeTests(unittest.TestCase): def setUp(self): self.fname = os_helper.TESTFN self.addCleanup(os_helper.unlink, self.fname) - create_file(self.fname, b"ABC") + create_file(self.fname, b"AB") + # short delay to ensure write/change time differ from creation + time.sleep(0.1) + with open(self.fname, "ab") as f: + f.write(b"C") def check_stat_attributes(self, fname): result = os.stat(fname) + # Make sure stx_mask is present and non-zero + # Even for non-statx calls we will set this field + self.assertNotEqual(result.stx_mask, 0) + # Make sure direct access works self.assertEqual(result[stat.ST_SIZE], 3) self.assertEqual(result.st_size, 3) @@ -614,6 +622,35 @@ def test_stat_result_pickle(self): unpickled = pickle.loads(p) self.assertEqual(result, unpickled) + @requires_os_func('statx') + def test_statx(self): + attrs = {a: getattr(stat, a) for a in dir(stat) if a.startswith('STATX_')} + for attr, mask in attrs.items(): + if mask >= stat.STATX_BASIC_STATS: + # Flags above this can't be predicted reliably as they will + # vary too much by operating system + continue + with self.subTest(attr): + r = os.statx(self.fname, mask) + expect = mask + if os.name == 'nt' and attr in {'STATX_GID', 'STATX_UID'}: + # GID and UID will never be set on Windows + expect = 0 + self.assertEqual(expect, r.stx_mask & mask, r.stx_mask) + + @requires_os_func('statx') + @unittest.skipUnless(os.name == 'nt', 'test specific to Windows') + def test_statx_win32(self): + # Note that this test is checking things that are currently true + # about our Win32 implementation of statx to make sure all code + # paths are getting coverage. If these change in the future, just + # update the test, rather than trying to preserve the behaviour. + + result = os.statx(self.fname, stat.STATX_CTIME | stat.STATX_BTIME) + self.assertNotEqual(result.st_ctime, result.st_birthtime) + result = os.stat(self.fname) + self.assertEqual(result.st_ctime, result.st_birthtime) + @unittest.skipUnless(hasattr(os, 'statvfs'), 'test needs os.statvfs()') def test_statvfs_attributes(self): result = os.statvfs(self.fname) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 6c1c0f5577b7ec..780abdb970a15c 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -209,7 +209,8 @@ def test_ismount_different_device(self): # Simulate the path being on a different device from its parent by # mocking out st_dev. save_lstat = os.lstat - def fake_lstat(path): + save_statx = os.statx + def fake_stat(path, *a, **kw): st_ino = 0 st_dev = 0 if path == ABSTFN: @@ -217,17 +218,19 @@ def fake_lstat(path): st_ino = 1 return posix.stat_result((0, st_ino, st_dev, 0, 0, 0, 0, 0, 0, 0)) try: - os.lstat = fake_lstat + os.lstat = os.statx = fake_stat self.assertIs(posixpath.ismount(ABSTFN), True) finally: os.lstat = save_lstat + os.statx = save_statx @unittest.skipIf(posix is None, "Test requires posix module") def test_ismount_directory_not_readable(self): # issue #2466: Simulate ismount run on a directory that is not # readable, which used to return False. save_lstat = os.lstat - def fake_lstat(path): + save_statx = os.statx + def fake_stat(path, *a, **kw): st_ino = 0 st_dev = 0 if path.startswith(ABSTFN) and path != ABSTFN: @@ -239,10 +242,11 @@ def fake_lstat(path): st_ino = 1 return posix.stat_result((0, st_ino, st_dev, 0, 0, 0, 0, 0, 0, 0)) try: - os.lstat = fake_lstat + os.lstat = os.statx = fake_stat self.assertIs(posixpath.ismount(ABSTFN), True) finally: os.lstat = save_lstat + os.statx = save_statx def test_isjunction(self): self.assertFalse(posixpath.isjunction(ABSTFN)) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 8fe62216ecdca0..5e804e44dcbce1 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -371,18 +371,21 @@ def check_args_to_onerror(self, func, arg, exc): def test_rmtree_does_not_choke_on_failing_lstat(self): try: orig_lstat = os.lstat - def raiser(fn, *args, **kwargs): + orig_statx = os.statx + def raiser(fn, *args, _orig, **kwargs): if fn != TESTFN: raise OSError() else: - return orig_lstat(fn) - os.lstat = raiser + return _orig(fn, *args, **kwargs) + os.lstat = functools.partial(raiser, _orig=orig_lstat) + os.statx = functools.partial(raiser, _orig=orig_statx) os.mkdir(TESTFN) write_file((TESTFN, 'foo'), 'foo') shutil.rmtree(TESTFN) finally: os.lstat = orig_lstat + os.statx = orig_statx def test_rmtree_uses_safe_fd_version_if_available(self): _use_fd_functions = ({os.open, os.stat, os.unlink, os.rmdir} <= @@ -2571,10 +2574,11 @@ def test_cant_get_size(self): # Emulate a case where src file size cannot be determined. # Internally bufsize will be set to a small value and # sendfile() will be called repeatedly. - with unittest.mock.patch('os.fstat', side_effect=OSError) as m: + with (unittest.mock.patch('os.fstat', side_effect=OSError) as m1, + unittest.mock.patch('os.statx', side_effect=OSError) as m2): with self.get_files() as (src, dst): shutil._fastcopy_sendfile(src, dst) - assert m.called + assert m1.called or m2.called self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA) def test_small_chunks(self): @@ -2584,10 +2588,11 @@ def test_small_chunks(self): # bigger while it is being copied. mock = unittest.mock.Mock() mock.st_size = 65536 + 1 - with unittest.mock.patch('os.fstat', return_value=mock) as m: + with (unittest.mock.patch('os.fstat', return_value=mock) as m1, + unittest.mock.patch('os.statx', return_value=mock) as m2): with self.get_files() as (src, dst): shutil._fastcopy_sendfile(src, dst) - assert m.called + assert m1.called or m2.called self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA) def test_big_chunk(self): @@ -2597,10 +2602,11 @@ def test_big_chunk(self): # performance. mock = unittest.mock.Mock() mock.st_size = self.FILESIZE + (100 * 1024 * 1024) - with unittest.mock.patch('os.fstat', return_value=mock) as m: + with (unittest.mock.patch('os.fstat', return_value=mock) as m1, + unittest.mock.patch('os.statx', return_value=mock) as m2): with self.get_files() as (src, dst): shutil._fastcopy_sendfile(src, dst) - assert m.called + assert m1.called or m2.called self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA) def test_blocksize_arg(self): diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 151034e6a81bf9..417fd877bf2d79 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -90,6 +90,7 @@ import os import re import socket +import stat import string import sys import time @@ -1502,7 +1503,7 @@ def open_local_file(self, req): filename = req.selector localfile = url2pathname(filename) try: - stats = os.stat(localfile) + stats = os.statx(localfile, stat.STATX_MTIME | stat.STATX_SIZE) size = stats.st_size modified = email.utils.formatdate(stats.st_mtime, usegmt=True) mtype = mimetypes.guess_type(filename)[0] @@ -2016,7 +2017,7 @@ def open_local_file(self, url): host, file = _splithost(url) localname = url2pathname(file) try: - stats = os.stat(localname) + stats = os.statx(localname, stat.STATX_MTIME | stat.STATX_SIZE) except OSError as e: raise URLError(e.strerror, e.filename) size = stats.st_size diff --git a/Lib/uu.py b/Lib/uu.py index 6f8805d8c5d0c6..9f7c771ecacbb9 100755 --- a/Lib/uu.py +++ b/Lib/uu.py @@ -56,7 +56,7 @@ def encode(in_file, out_file, name=None, mode=None, *, backtick=False): name = os.path.basename(in_file) if mode is None: try: - mode = os.stat(in_file).st_mode + mode = os.stat(in_file, stat.STATX_TYPE | stat.STATX_MODE).st_mode except AttributeError: pass in_file = open(in_file, 'rb') diff --git a/Misc/NEWS.d/next/Library/2022-11-24-14-57-48.gh-issue-99726.eayGOV.rst b/Misc/NEWS.d/next/Library/2022-11-24-14-57-48.gh-issue-99726.eayGOV.rst new file mode 100644 index 00000000000000..86c069a6e0cd80 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-11-24-14-57-48.gh-issue-99726.eayGOV.rst @@ -0,0 +1 @@ +Adds :func:`os.statx` function. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 86251008b1bdae..81e0147643018c 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -178,6 +178,137 @@ os_lstat(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kw return return_value; } +#if (defined(HAVE_STATX) || defined(MS_WINDOWS)) + +PyDoc_STRVAR(os_statx__doc__, +"statx($module, /, path, mask, *, dir_fd=None, follow_symlinks=True,\n" +" flags=0)\n" +"--\n" +"\n" +"Perform a stat system call on the given path, retrieving certain information.\n" +"\n" +" path\n" +" Path to be examined; can be string, bytes, a path-like object or\n" +" open-file-descriptor int.\n" +" mask\n" +" A combination of stat.STATX_* flags specifying the fields that the\n" +" caller is interested in. The stx_mask member of the result will\n" +" include all fields that are actually filled in, which may be more\n" +" or fewer than those specified in this argument.\n" +" dir_fd\n" +" If not None, it should be a file descriptor open to a directory,\n" +" and path should be a relative string; path will then be relative to\n" +" that directory.\n" +" follow_symlinks\n" +" If False, and the last element of the path is a symbolic link,\n" +" stat will examine the symbolic link itself instead of the file\n" +" the link points to.\n" +" flags\n" +" A combination of AT_* flags specifying how path should be resolved.\n" +" These are only relevant on Linux.\n" +"\n" +"dir_fd and follow_symlinks may not be implemented\n" +" on your platform. If they are unavailable, using them will raise a\n" +" NotImplementedError.\n" +"\n" +"It\'s an error to use dir_fd or follow_symlinks when specifying path as\n" +" an open file descriptor.\n" +"\n" +"The follow_symlinks parameter adds the AT_SYMLINK_NOFOLLOW flag into flags\n" +" (when passed False) but will not remove it. Using this parameter rather\n" +" than the flag is recommended for maximum portability."); + +#define OS_STATX_METHODDEF \ + {"statx", _PyCFunction_CAST(os_statx), METH_FASTCALL|METH_KEYWORDS, os_statx__doc__}, + +static PyObject * +os_statx_impl(PyObject *module, path_t *path, int mask, int dir_fd, + int follow_symlinks, int flags); + +static PyObject * +os_statx(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 5 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(path), &_Py_ID(mask), &_Py_ID(dir_fd), &_Py_ID(follow_symlinks), &_Py_ID(flags), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"path", "mask", "dir_fd", "follow_symlinks", "flags", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "statx", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[5]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; + path_t path = PATH_T_INITIALIZE("statx", "path", 0, 1); + int mask; + int dir_fd = DEFAULT_DIR_FD; + int follow_symlinks = 1; + int flags = 0; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf); + if (!args) { + goto exit; + } + if (!path_converter(args[0], &path)) { + goto exit; + } + mask = _PyLong_AsInt(args[1]); + if (mask == -1 && PyErr_Occurred()) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + if (args[2]) { + if (!dir_fd_converter(args[2], &dir_fd)) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[3]) { + follow_symlinks = PyObject_IsTrue(args[3]); + if (follow_symlinks < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + flags = _PyLong_AsInt(args[4]); + if (flags == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional_kwonly: + return_value = os_statx_impl(module, &path, mask, dir_fd, follow_symlinks, flags); + +exit: + /* Cleanup for path */ + path_cleanup(&path); + + return return_value; +} + +#endif /* (defined(HAVE_STATX) || defined(MS_WINDOWS)) */ + PyDoc_STRVAR(os_access__doc__, "access($module, /, path, mode, *, dir_fd=None, effective_ids=False,\n" " follow_symlinks=True)\n" @@ -10954,6 +11085,10 @@ os_waitstatus_to_exitcode(PyObject *module, PyObject *const *args, Py_ssize_t na #endif /* (defined(WIFEXITED) || defined(MS_WINDOWS)) */ +#ifndef OS_STATX_METHODDEF + #define OS_STATX_METHODDEF +#endif /* !defined(OS_STATX_METHODDEF) */ + #ifndef OS_TTYNAME_METHODDEF #define OS_TTYNAME_METHODDEF #endif /* !defined(OS_TTYNAME_METHODDEF) */ @@ -11549,4 +11684,4 @@ os_waitstatus_to_exitcode(PyObject *module, PyObject *const *args, Py_ssize_t na #ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF #define OS_WAITSTATUS_TO_EXITCODE_METHODDEF #endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */ -/*[clinic end generated code: output=04fd23c89ab41f75 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=56f0ab245e915a5a input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 4817973262f484..321562b3548bcc 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -39,6 +39,7 @@ # include "posixmodule.h" #else # include "winreparse.h" +# include "pycore_fileutils_windows.h" // _Py_GetFileInformationByName() #endif #if !defined(EX_OK) && defined(EXIT_SUCCESS) @@ -656,8 +657,10 @@ PyOS_AfterFork(void) #ifdef MS_WINDOWS /* defined in fileutils.c */ void _Py_time_t_to_FILE_TIME(time_t, int, FILETIME *); -void _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *, - ULONG, struct _Py_stat_struct *); +void _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *, ULONG, + FILE_BASIC_INFO *, struct _Py_stat_struct *); +void _Py_stat_basic_info_to_stat(FILE_STAT_BASIC_INFORMATION *, + struct _Py_stat_struct *); #endif @@ -1835,6 +1838,7 @@ win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result, { HANDLE hFile; BY_HANDLE_FILE_INFORMATION fileInfo; + FILE_BASIC_INFO basicInfo; FILE_ATTRIBUTE_TAG_INFO tagInfo = { 0 }; DWORD fileType, error; BOOL isUnhandledTag = FALSE; @@ -1969,7 +1973,9 @@ win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result, } } - if (!GetFileInformationByHandle(hFile, &fileInfo)) { + if (!GetFileInformationByHandle(hFile, &fileInfo) || + !GetFileInformationByHandleEx(hFile, FileBasicInfo, + &basicInfo, sizeof(basicInfo))) { switch (GetLastError()) { case ERROR_INVALID_PARAMETER: case ERROR_INVALID_FUNCTION: @@ -1985,7 +1991,7 @@ win32_xstat_impl(const wchar_t *path, struct _Py_stat_struct *result, } } - _Py_attribute_data_to_stat(&fileInfo, tagInfo.ReparseTag, result); + _Py_attribute_data_to_stat(&fileInfo, tagInfo.ReparseTag, &basicInfo, result); if (!(fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { /* Fix the file execute permissions. This hack sets S_IEXEC if @@ -2029,6 +2035,7 @@ win32_xstat(const wchar_t *path, struct _Py_stat_struct *result, BOOL traverse) errno = 0; return code; } + /* About the following functions: win32_lstat_w, win32_stat, win32_stat_w In Posix, stat automatically traverses symlinks and returns the stat @@ -2051,6 +2058,72 @@ win32_stat(const wchar_t* path, struct _Py_stat_struct *result) return win32_xstat(path, result, TRUE); } + +/* The statx values we can get from FileStatBasicByNameInfo. + Notably absent are STATX_INO (because we can't get st_dev efficiently), + and STATX_MODE. + Any flags requested that aren't in this mask will cause the FILE_STATS + below to be used, even if it is missing other flags. */ +#define WIN32_STATX_BASIC_STATS (0x0EE5) +/* The statx values we get from GetFileInformationByHandle(). */ +#define WIN32_STATX_FILE_STATS (0x0FE7) + +static int +win32_statx(const wchar_t *path, struct _Py_stat_struct *result, + int mask, int follow_symlinks, unsigned int *result_mask) +{ + unsigned int rmask = 0; + if (!(mask & ~WIN32_STATX_BASIC_STATS)) { + /* Try and use our fast path to fill in the result */ + FILE_STAT_BASIC_INFORMATION statInfo; + if (_Py_GetFileInformationByName(path, FileStatBasicByNameInfo, + &statInfo, sizeof(statInfo))) { + if (// Cannot use fast path for reparse points ... + !(statInfo.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + // ... unless it's a name surrogate (symlink) and we're not following + || (!follow_symlinks && IsReparseTagNameSurrogate(statInfo.ReparseTag)) + ) { + rmask = WIN32_STATX_BASIC_STATS; + _Py_stat_basic_info_to_stat(&statInfo, result); + } + } else { + /* Some errors aren't worth retrying with the slow path */ + switch(GetLastError()) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_NOT_READY: + case ERROR_BAD_NET_NAME: + return -1; + case ERROR_NOT_SUPPORTED: + /* indicates the API couldn't be loaded */ + break; + } + } + } + + if (!rmask) { + /* No result yet, so either we need to follow a symlink, or the + user requested more information than our fast path offers */ + rmask = WIN32_STATX_FILE_STATS; + int r = win32_xstat(path, result, follow_symlinks); + if (r) { + return r; + } + } + + /* Legacy behaviour: if the caller has not specified STATX_CTIME (0x80), + firstly, they shouldn't be looking at the field, but if they do then we + want them to see st_btime because that's what we've always done. + Provided that the result mask includes STATX_BTIME (0x800), that is. */ + if (!(mask & 0x80) && (rmask & 0x800)) { + result->st_ctime = result->st_btime; + result->st_ctime_nsec = result->st_btime_nsec; + } + + *result_mask = rmask; + return 0; +} + #endif /* MS_WINDOWS */ PyDoc_STRVAR(stat_result__doc__, @@ -2108,6 +2181,26 @@ static PyStructSequence_Field stat_result_fields[] = { #endif #ifdef HAVE_STRUCT_STAT_ST_REPARSE_TAG {"st_reparse_tag", "Windows reparse tag"}, +#endif + /* the stx_mask attribute is always present to allow for fallbacks */ + {"stx_mask", "mask of fields provided by the system"}, +#if defined(HAVE_STATX) || defined(MS_WINDOWS) + /* Many of these are duplicates of the optional fields earlier. + *Presumably* they'll all be present together, but since people + test for the presence of the st_* fields by name at .py compile + time, we can't have them present if they won't be filled in by + regular stat() calls. But we make sure they're filled in by any + statx() call that also fills in these ones */ + {"stx_blksize", "blocksize for filesystem I/O"}, + {"stx_blocks", "number of blocks allocated"}, + {"stx_rdev", "device type (if inode device)"}, + {"stx_btime", "time of creation"}, + {"stx_btime_ns", "time of creation in nanoseconds"}, + {"stx_attributes", "additional file attributes"}, + {"stx_attributes_mask", "mask indicating which attributes are supported"}, +#ifdef STATX_MNT_ID + {"stx_mnt_id", "the mount id"}, +#endif #endif {0} }; @@ -2166,6 +2259,35 @@ static PyStructSequence_Field stat_result_fields[] = { #define ST_REPARSE_TAG_IDX ST_FSTYPE_IDX #endif +#define STX_MASK_IDX (ST_REPARSE_TAG_IDX+1) + +#if defined(HAVE_STATX) || defined(MS_WINDOWS) +#define STX_BLKSIZE_IDX (STX_MASK_IDX+1) +#define STX_BLOCKS_IDX (STX_BLKSIZE_IDX+1) +#define STX_RDEV_IDX (STX_BLOCKS_IDX+1) +#define STX_BTIME_IDX (STX_RDEV_IDX+1) +#define STX_BTIME_NS_IDX (STX_BTIME_IDX+1) +#define STX_ATTRIBUTES_IDX (STX_BTIME_NS_IDX+1) +#define STX_ATTRIBUTES_MASK_IDX (STX_ATTRIBUTES_IDX+1) +#ifdef STATX_MNT_ID +#define STX_MNT_ID_IDX (STX_ATTRIBUTES_MASK_IDX+1) +#else +#define STX_MNT_ID_IDX STX_ATTRIBUTES_MASK_IDX +#endif +#endif + +/* for when regular stat() gets called */ +#ifdef MS_WINDOWS +#define ST_DEFAULT_STX_MASK WIN32_STATX_FILE_STATS +#elif defined(HAVE_STRUCT_STAT_ST_BIRTHTIME) +/* STATX_BASIC_STATS | STATX_BTIME */ +#define ST_DEFAULT_STX_MASK 0x00000fff +#else +/* STATX_BASIC_STATS */ +#define ST_DEFAULT_STX_MASK 0x000007ff +#endif + + static PyStructSequence_Desc stat_result_desc = { "stat_result", /* name */ stat_result__doc__, /* doc */ @@ -2320,7 +2442,7 @@ _posix_free(void *module) } static void -fill_time(PyObject *module, PyObject *v, int index, time_t sec, unsigned long nsec) +fill_time(PyObject *module, PyObject *v, int index, int index_f, int index_ns, time_t sec, unsigned long nsec) { PyObject *s = _PyLong_FromTime_t(sec); PyObject *ns_fractional = PyLong_FromUnsignedLong(nsec); @@ -2331,23 +2453,31 @@ fill_time(PyObject *module, PyObject *v, int index, time_t sec, unsigned long ns if (!(s && ns_fractional)) goto exit; - s_in_ns = PyNumber_Multiply(s, get_posix_state(module)->billion); - if (!s_in_ns) - goto exit; + if (index_ns >= 0) { + s_in_ns = PyNumber_Multiply(s, get_posix_state(module)->billion); + if (!s_in_ns) { + goto exit; + } - ns_total = PyNumber_Add(s_in_ns, ns_fractional); - if (!ns_total) - goto exit; + ns_total = PyNumber_Add(s_in_ns, ns_fractional); + if (!ns_total) { + goto exit; + } + } - float_s = PyFloat_FromDouble(sec + 1e-9*nsec); - if (!float_s) { - goto exit; + if (index_f >= 0) { + float_s = PyFloat_FromDouble(sec + 1e-9*nsec); + if (!float_s) { + goto exit; + } } - PyStructSequence_SET_ITEM(v, index, s); - PyStructSequence_SET_ITEM(v, index+3, float_s); - PyStructSequence_SET_ITEM(v, index+6, ns_total); - s = NULL; + if (index >= 0) + PyStructSequence_SET_ITEM(v, index, Py_NewRef(s)); + if (float_s) + PyStructSequence_SET_ITEM(v, index_f, float_s); + if (ns_total) + PyStructSequence_SET_ITEM(v, index_ns, ns_total); float_s = NULL; ns_total = NULL; exit: @@ -2361,7 +2491,7 @@ fill_time(PyObject *module, PyObject *v, int index, time_t sec, unsigned long ns /* pack a system stat C structure into the Python stat tuple (used by posix_stat() and posix_fstat()) */ static PyObject* -_pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) +_pystat_fromstructstat(PyObject *module, STRUCT_STAT *st, unsigned int stx_mask) { unsigned long ansec, mnsec, cnsec; PyObject *StatResultType = get_posix_state(module)->StatResultType; @@ -2405,9 +2535,9 @@ _pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) #else ansec = mnsec = cnsec = 0; #endif - fill_time(module, v, 7, st->st_atime, ansec); - fill_time(module, v, 8, st->st_mtime, mnsec); - fill_time(module, v, 9, st->st_ctime, cnsec); + fill_time(module, v, 7, 10, 13, st->st_atime, ansec); + fill_time(module, v, 8, 11, 14, st->st_mtime, mnsec); + fill_time(module, v, 9, 12, 15, st->st_ctime, cnsec); #ifdef HAVE_STRUCT_STAT_ST_BLKSIZE PyStructSequence_SET_ITEM(v, ST_BLKSIZE_IDX, @@ -2425,21 +2555,29 @@ _pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) PyStructSequence_SET_ITEM(v, ST_GEN_IDX, PyLong_FromLong((long)st->st_gen)); #endif -#ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME +#if defined(HAVE_STRUCT_STAT_ST_BIRTHTIME) || defined(MS_WINDOWS) { - PyObject *val; - unsigned long bsec,bnsec; - bsec = (long)st->st_birthtime; + time_t bsec; + unsigned long bnsec; +#ifdef MS_WINDOWS + bsec = st->st_btime; + bnsec = st->st_btime_nsec; +#else + bsec = st->st_birthtime; #ifdef HAVE_STAT_TV_NSEC2 bnsec = st->st_birthtimespec.tv_nsec; #else bnsec = 0; +#endif /* HAVE_STAT_TV_NSEC2 */ +#endif /* MS_WINDOWS */ +#ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME + fill_time(module, v, -1, ST_BIRTHTIME_IDX, -1, bsec, bnsec); #endif - val = PyFloat_FromDouble(bsec + 1e-9*bnsec); - PyStructSequence_SET_ITEM(v, ST_BIRTHTIME_IDX, - val); - } +#if defined(HAVE_STATX) || defined(MS_WINDOWS) + fill_time(module, v, -1, STX_BTIME_IDX, STX_BTIME_NS_IDX, bsec, bnsec); #endif + } +#endif /* HAVE_STRUCT_STAT_ST_BIRTHTIME */ #ifdef HAVE_STRUCT_STAT_ST_FLAGS PyStructSequence_SET_ITEM(v, ST_FLAGS_IDX, PyLong_FromLong((long)st->st_flags)); @@ -2457,6 +2595,152 @@ _pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) PyLong_FromUnsignedLong(st->st_reparse_tag)); #endif + if (!stx_mask) { + stx_mask = ST_DEFAULT_STX_MASK; + } + PyStructSequence_SET_ITEM(v, STX_MASK_IDX, + PyLong_FromUnsignedLong(stx_mask)); + +#if defined(HAVE_STATX) || defined(MS_WINDOWS) + /* ensure unused fields that are present for statx are initialized */ + PyObject *zero = PyLong_FromLong(0); + if (!zero) { + Py_DECREF(v); + return NULL; + } +#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE + PyStructSequence_SET_ITEM(v, STX_BLKSIZE_IDX, + Py_NewRef(PyStructSequence_GET_ITEM(v, ST_BLKSIZE_IDX))); +#else + PyStructSequence_SET_ITEM(v, STX_BLKSIZE_IDX, Py_NewRef(zero)); +#endif +#ifdef HAVE_STRUCT_STAT_ST_BLOCKS + PyStructSequence_SET_ITEM(v, STX_BLOCKS_IDX, + Py_NewRef(PyStructSequence_GET_ITEM(v, ST_BLOCKS_IDX))); +#else + PyStructSequence_SET_ITEM(v, STX_BLOCKS_IDX, Py_NewRef(zero)); +#endif +#ifdef HAVE_STRUCT_STAT_ST_RDEV + PyStructSequence_SET_ITEM(v, STX_RDEV_IDX, + Py_NewRef(PyStructSequence_GET_ITEM(v, ST_RDEV_IDX))); +#else + PyStructSequence_SET_ITEM(v, STX_RDEV_IDX, Py_NewRef(zero)); +#endif +#ifndef HAVE_STRUCT_STAT_ST_BIRTHTIME + /* would've been filled in by now if we had st_birthtime */ + PyStructSequence_SET_ITEM(v, STX_BTIME_IDX, Py_NewRef(zero)); + PyStructSequence_SET_ITEM(v, STX_BTIME_NS_IDX, Py_NewRef(zero)); +#endif + PyStructSequence_SET_ITEM(v, STX_ATTRIBUTES_IDX, Py_NewRef(zero)); + PyStructSequence_SET_ITEM(v, STX_ATTRIBUTES_MASK_IDX, Py_NewRef(zero)); +#ifdef STATX_MNT_ID + PyStructSequence_SET_ITEM(v, STX_MNT_ID_IDX, Py_NewRef(zero)); +#endif + Py_DECREF(zero); +#endif + + if (PyErr_Occurred()) { + Py_DECREF(v); + return NULL; + } + + return v; +} + +#ifdef HAVE_STATX + +/* pack a system statx C structure into the Python stat tuple + (used by posix_statx()) */ +static PyObject* +_pystat_fromstructstatx(PyObject *module, struct statx *st) +{ + unsigned long long dev; + PyObject *StatResultType = get_posix_state(module)->StatResultType; + PyObject *v = PyStructSequence_New((PyTypeObject *)StatResultType); + if (v == NULL) + return NULL; + + PyStructSequence_SET_ITEM(v, 0, PyLong_FromLong((long)st->stx_mode)); + static_assert(sizeof(unsigned long long) >= sizeof(st->stx_ino), + "statx.stx_ino is larger than unsigned long long"); + PyStructSequence_SET_ITEM(v, 1, PyLong_FromUnsignedLongLong(st->stx_ino)); + dev = (unsigned long long)st->stx_dev_major << 32 | st->stx_dev_minor; + PyStructSequence_SET_ITEM(v, 2, PyLong_FromUnsignedLongLong(dev)); + PyStructSequence_SET_ITEM(v, 3, PyLong_FromUnsignedLong(st->stx_nlink)); + PyStructSequence_SET_ITEM(v, 4, _PyLong_FromUid(st->stx_uid)); + PyStructSequence_SET_ITEM(v, 5, _PyLong_FromGid(st->stx_gid)); + static_assert(sizeof(unsigned long long) >= sizeof(st->stx_size), + "statx.stx_size is larger than unsigned long long"); + PyStructSequence_SET_ITEM(v, 6, PyLong_FromUnsignedLongLong(st->stx_size)); + + fill_time(module, v, 7, 10, 13, st->stx_atime.tv_sec, st->stx_atime.tv_nsec); + fill_time(module, v, 8, 11, 14, st->stx_mtime.tv_sec, st->stx_mtime.tv_nsec); + fill_time(module, v, 9, 12, 15, st->stx_ctime.tv_sec, st->stx_ctime.tv_nsec); + fill_time(module, v, -1, STX_BTIME_IDX, STX_BTIME_NS_IDX, + st->stx_btime.tv_sec, st->stx_btime.tv_nsec); + + PyStructSequence_SET_ITEM(v, STX_BLKSIZE_IDX, + PyLong_FromUnsignedLong(st->stx_blksize)); + PyStructSequence_SET_ITEM(v, STX_BLOCKS_IDX, + PyLong_FromUnsignedLongLong(st->stx_blocks)); + dev = (unsigned long long)st->stx_rdev_major << 32 | st->stx_rdev_minor; + PyStructSequence_SET_ITEM(v, STX_RDEV_IDX, + PyLong_FromUnsignedLongLong(dev)); + + PyStructSequence_SET_ITEM(v, STX_MASK_IDX, + PyLong_FromUnsignedLong(st->stx_mask)); + PyStructSequence_SET_ITEM(v, STX_ATTRIBUTES_IDX, + PyLong_FromUnsignedLongLong(st->stx_attributes)); + PyStructSequence_SET_ITEM(v, STX_ATTRIBUTES_MASK_IDX, + PyLong_FromUnsignedLongLong(st->stx_attributes_mask)); +#ifdef STATX_MNT_ID + PyStructSequence_SET_ITEM(v, STX_MNT_ID_IDX, + PyLong_FromUnsignedLongLong(st->stx_mnt_id)); +#endif + + /* copy stx fields into optional st fields that are present */ +#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE + PyStructSequence_SET_ITEM(v, ST_BLKSIZE_IDX, + Py_NewRef(PyStructSequence_GET_ITEM(v, STX_BLKSIZE_IDX))); +#endif +#ifdef HAVE_STRUCT_STAT_ST_BLOCKS + PyStructSequence_SET_ITEM(v, ST_BLOCKS_IDX, + Py_NewRef(PyStructSequence_GET_ITEM(v, STX_BLOCKS_IDX))); +#endif +#ifdef HAVE_STRUCT_STAT_ST_RDEV + PyStructSequence_SET_ITEM(v, ST_RDEV_IDX, + Py_NewRef(PyStructSequence_GET_ITEM(v, STX_RDEV_IDX))); +#endif +#ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME + PyStructSequence_SET_ITEM(v, ST_BIRTHTIME_IDX, + Py_NewRef(PyStructSequence_GET_ITEM(v, STX_BTIME_IDX))); +#endif + + /* ensure unused fields that are present for regular stat are initialized */ + PyObject *zero = PyLong_FromLong(0); + if (!zero) { + Py_DECREF(v); + return NULL; + } + +#ifdef HAVE_STRUCT_STAT_ST_GEN + PyStructSequence_SET_ITEM(v, ST_GEN_IDX, Py_NewRef(zero)); +#endif +#ifdef HAVE_STRUCT_STAT_ST_FLAGS + PyStructSequence_SET_ITEM(v, ST_FLAGS_IDX, Py_NewRef(zero)); +#endif +#ifdef HAVE_STRUCT_STAT_ST_FILE_ATTRIBUTES + PyStructSequence_SET_ITEM(v, ST_FILE_ATTRIBUTES_IDX, Py_NewRef(zero)); +#endif +#ifdef HAVE_STRUCT_STAT_ST_FSTYPE + PyStructSequence_SET_ITEM(v, ST_FSTYPE_IDX, Py_NewRef(Py_None)); +#endif +#ifdef HAVE_STRUCT_STAT_ST_REPARSE_TAG + PyStructSequence_SET_ITEM(v, ST_REPARSE_TAG_IDX, Py_NewRef(zero)); +#endif + + Py_DECREF(zero); + if (PyErr_Occurred()) { Py_DECREF(v); return NULL; @@ -2465,6 +2749,9 @@ _pystat_fromstructstat(PyObject *module, STRUCT_STAT *st) return v; } +#endif + + /* POSIX methods */ @@ -2530,9 +2817,54 @@ posix_do_stat(PyObject *module, const char *function_name, path_t *path, return path_error(path); } - return _pystat_fromstructstat(module, &st); +#ifdef MS_WINDOWS + /* Legacy behaviour is to return creation time as st_ctime (change time). + We now calculate the change time, so st_ctime is "correct", but we keep + replacing it here. Callers who want it should use statx() */ + st.st_ctime = st.st_btime; + st.st_ctime_nsec = st.st_btime_nsec; +#endif + + return _pystat_fromstructstat(module, &st, 0); +} + + +#ifdef HAVE_STATX + +static PyObject * +posix_do_statx(PyObject *module, path_t *path, unsigned int mask, + int dir_fd, int flags) +{ + struct statx st; + int result; + + if (path_and_dir_fd_invalid("statx", path, dir_fd) || + dir_fd_and_fd_invalid("statx", dir_fd, path->fd) || + fd_and_follow_symlinks_invalid("statx", path->fd, !(flags & AT_SYMLINK_NOFOLLOW))) + return NULL; + + if (dir_fd == DEFAULT_DIR_FD) { + dir_fd = AT_FDCWD; + } + + Py_BEGIN_ALLOW_THREADS + if (path->fd != -1) { + result = statx(path->fd, "", flags | AT_EMPTY_PATH, mask, &st); + } else { + result = statx(dir_fd, path->narrow, flags, mask, &st); + } + Py_END_ALLOW_THREADS + + if (result != 0) { + return path_error(path); + } + + return _pystat_fromstructstatx(module, &st); } +#endif + + /*[python input] for s in """ @@ -2887,6 +3219,92 @@ os_lstat_impl(PyObject *module, path_t *path, int dir_fd) } +#if defined(HAVE_STATX) || defined(MS_WINDOWS) + +/*[clinic input] + +os.statx + + path : path_t(allow_fd=True) + Path to be examined; can be string, bytes, a path-like object or + open-file-descriptor int. + + mask : int + A combination of stat.STATX_* flags specifying the fields that the + caller is interested in. The stx_mask member of the result will + include all fields that are actually filled in, which may be more + or fewer than those specified in this argument. + + * + + dir_fd : dir_fd = None + If not None, it should be a file descriptor open to a directory, + and path should be a relative string; path will then be relative to + that directory. + + follow_symlinks: bool = True + If False, and the last element of the path is a symbolic link, + stat will examine the symbolic link itself instead of the file + the link points to. + + flags : int = 0 + A combination of AT_* flags specifying how path should be resolved. + These are only relevant on Linux. + + +Perform a stat system call on the given path, retrieving certain information. + +dir_fd and follow_symlinks may not be implemented + on your platform. If they are unavailable, using them will raise a + NotImplementedError. + +It's an error to use dir_fd or follow_symlinks when specifying path as + an open file descriptor. + +The follow_symlinks parameter adds the AT_SYMLINK_NOFOLLOW flag into flags + (when passed False) but will not remove it. Using this parameter rather + than the flag is recommended for maximum portability. + +[clinic start generated code]*/ + +static PyObject * +os_statx_impl(PyObject *module, path_t *path, int mask, int dir_fd, + int follow_symlinks, int flags) +/*[clinic end generated code: output=e38f7e693d96b2c6 input=5e832dfc79a58fb7]*/ +{ +#ifdef MS_WINDOWS + struct _Py_stat_struct st; + unsigned int result_mask; + int result; + + Py_BEGIN_ALLOW_THREADS + if (path->fd != -1) { + _Py_BEGIN_SUPPRESS_IPH + result = FSTAT(path->fd, &st); + result_mask = ST_DEFAULT_STX_MASK; + _Py_END_SUPPRESS_IPH + } else { + result = win32_statx(path->wide, &st, mask, follow_symlinks, &result_mask); + } + Py_END_ALLOW_THREADS + + if (result != 0) { + return path_error(path); + } + + return _pystat_fromstructstat(module, &st, result_mask); + +#else /* MS_WINDOWS */ + + if (!follow_symlinks) { + flags |= AT_SYMLINK_NOFOLLOW; + } + return posix_do_statx(module, path, mask, dir_fd, flags); +#endif +} + +#endif + /*[clinic input] os.access -> bool @@ -10194,7 +10612,7 @@ os_fstat_impl(PyObject *module, int fd) #endif } - return _pystat_fromstructstat(module, &st); + return _pystat_fromstructstat(module, &st, 0); } @@ -13705,7 +14123,7 @@ DirEntry_fetch_stat(PyObject *module, DirEntry *self, int follow_symlinks) if (result != 0) return path_object_error(self->path); - return _pystat_fromstructstat(module, &st); + return _pystat_fromstructstat(module, &st, 0); } static PyObject * @@ -13714,7 +14132,7 @@ DirEntry_get_lstat(PyTypeObject *defining_class, DirEntry *self) if (!self->lstat) { PyObject *module = PyType_GetModule(defining_class); #ifdef MS_WINDOWS - self->lstat = _pystat_fromstructstat(module, &self->win32_lstat); + self->lstat = _pystat_fromstructstat(module, &self->win32_lstat, 0); #else /* POSIX */ self->lstat = DirEntry_fetch_stat(module, self, 0); #endif @@ -14049,7 +14467,11 @@ DirEntry_from_find_data(PyObject *module, path_t *path, WIN32_FIND_DATAW *dataW) } find_data_to_file_info(dataW, &file_info, &reparse_tag); - _Py_attribute_data_to_stat(&file_info, reparse_tag, &entry->win32_lstat); + _Py_attribute_data_to_stat(&file_info, reparse_tag, NULL, &entry->win32_lstat); + /* Without passing FILE_BASIC_INFO, st_ctime will always be 0. + For legacy reasons, we copy st_btime into that field */ + entry->win32_lstat.st_ctime = entry->win32_lstat.st_btime; + entry->win32_lstat.st_ctime_nsec = entry->win32_lstat.st_btime_nsec; return (PyObject *)entry; @@ -14855,6 +15277,7 @@ os_waitstatus_to_exitcode_impl(PyObject *module, PyObject *status_obj) static PyMethodDef posix_methods[] = { OS_STAT_METHODDEF + OS_STATX_METHODDEF OS_ACCESS_METHODDEF OS_TTYNAME_METHODDEF OS_CHDIR_METHODDEF @@ -15847,6 +16270,10 @@ static const struct have_function { { "HAVE_RENAMEAT", probe_renameat }, #endif +#if defined(HAVE_STATX) || defined(MS_WINDOWS) + { "HAVE_STATX", NULL }, +#endif + #ifdef HAVE_SYMLINKAT { "HAVE_SYMLINKAT", probe_symlinkat }, #endif diff --git a/PC/pyconfig.h b/PC/pyconfig.h index 1d8408b363a66a..8f33d720cbe22b 100644 --- a/PC/pyconfig.h +++ b/PC/pyconfig.h @@ -714,4 +714,7 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Define if libssl has X509_VERIFY_PARAM_set1_host and related function */ #define HAVE_X509_VERIFY_PARAM_SET1_HOST 1 +/* We add st_birthtime on Windows for ftCreationTime */ +#define HAVE_STRUCT_STAT_ST_BIRTHTIME 1 + #endif /* !Py_CONFIG_H */ diff --git a/Python/fileutils.c b/Python/fileutils.c index 244bd899b3bd24..dcb9bb95b0c4b9 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -9,6 +9,8 @@ # include # include # include // PathCchCombineEx +# include +# include "pycore_fileutils_windows.h" // FILE_STAT_BASIC_INFORMATION extern int winerror_to_errno(int); #endif @@ -1048,6 +1050,13 @@ FILE_TIME_to_time_t_nsec(FILETIME *in_ptr, time_t *time_out, int* nsec_out) *time_out = Py_SAFE_DOWNCAST((in / 10000000) - secs_between_epochs, __int64, time_t); } +static void +LARGE_INTEGER_to_time_t_nsec(LARGE_INTEGER *in_ptr, time_t *time_out, int* nsec_out) +{ + *nsec_out = (int)(in_ptr->QuadPart % 10000000) * 100; /* FILETIME is in units of 100 nsec. */ + *time_out = Py_SAFE_DOWNCAST((in_ptr->QuadPart / 10000000) - secs_between_epochs, __int64, time_t); +} + void _Py_time_t_to_FILE_TIME(time_t time_in, int nsec_in, FILETIME *out_ptr) { @@ -1079,16 +1088,25 @@ attributes_to_mode(DWORD attr) void _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *info, ULONG reparse_tag, - struct _Py_stat_struct *result) + FILE_BASIC_INFO *basic_info, struct _Py_stat_struct *result) { memset(result, 0, sizeof(*result)); result->st_mode = attributes_to_mode(info->dwFileAttributes); result->st_size = (((__int64)info->nFileSizeHigh)<<32) + info->nFileSizeLow; result->st_dev = info->dwVolumeSerialNumber; result->st_rdev = result->st_dev; - FILE_TIME_to_time_t_nsec(&info->ftCreationTime, &result->st_ctime, &result->st_ctime_nsec); - FILE_TIME_to_time_t_nsec(&info->ftLastWriteTime, &result->st_mtime, &result->st_mtime_nsec); - FILE_TIME_to_time_t_nsec(&info->ftLastAccessTime, &result->st_atime, &result->st_atime_nsec); + if (basic_info) { + LARGE_INTEGER_to_time_t_nsec(&basic_info->CreationTime, &result->st_btime, &result->st_btime_nsec); + LARGE_INTEGER_to_time_t_nsec(&basic_info->ChangeTime, &result->st_ctime, &result->st_ctime_nsec); + LARGE_INTEGER_to_time_t_nsec(&basic_info->LastWriteTime, &result->st_mtime, &result->st_mtime_nsec); + LARGE_INTEGER_to_time_t_nsec(&basic_info->LastAccessTime, &result->st_atime, &result->st_atime_nsec); + } else { + FILE_TIME_to_time_t_nsec(&info->ftCreationTime, &result->st_btime, &result->st_btime_nsec); + /* We leave ctime as zero because we do not have it without FILE_BASIC_INFO. + Our callers will replace it with btime if they want legacy behaviour */ + FILE_TIME_to_time_t_nsec(&info->ftLastWriteTime, &result->st_mtime, &result->st_mtime_nsec); + FILE_TIME_to_time_t_nsec(&info->ftLastAccessTime, &result->st_atime, &result->st_atime_nsec); + } result->st_nlink = info->nNumberOfLinks; result->st_ino = (((uint64_t)info->nFileIndexHigh) << 32) + info->nFileIndexLow; /* bpo-37834: Only actual symlinks set the S_IFLNK flag. But lstat() will @@ -1097,13 +1115,75 @@ _Py_attribute_data_to_stat(BY_HANDLE_FILE_INFORMATION *info, ULONG reparse_tag, result->st_reparse_tag = reparse_tag; if (info->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && reparse_tag == IO_REPARSE_TAG_SYMLINK) { - /* first clear the S_IFMT bits */ - result->st_mode ^= (result->st_mode & S_IFMT); - /* now set the bits that make this a symlink */ - result->st_mode |= S_IFLNK; + /* set the bits that make this a symlink */ + result->st_mode = (result->st_mode & ~S_IFMT) | S_IFLNK; } result->st_file_attributes = info->dwFileAttributes; } + +void +_Py_stat_basic_info_to_stat(FILE_STAT_BASIC_INFORMATION *info, + struct _Py_stat_struct *result) +{ + /* set values here to match WIN32_STATX_BASIC_STATS defined in posixmodule.c */ + memset(result, 0, sizeof(*result)); + result->st_mode = attributes_to_mode(info->FileAttributes); + result->st_size = info->EndOfFile.QuadPart; + LARGE_INTEGER_to_time_t_nsec(&info->CreationTime, &result->st_btime, &result->st_btime_nsec); + LARGE_INTEGER_to_time_t_nsec(&info->ChangeTime, &result->st_ctime, &result->st_ctime_nsec); + LARGE_INTEGER_to_time_t_nsec(&info->LastWriteTime, &result->st_mtime, &result->st_mtime_nsec); + LARGE_INTEGER_to_time_t_nsec(&info->LastAccessTime, &result->st_atime, &result->st_atime_nsec); + result->st_nlink = info->NumberOfLinks; + result->st_ino = info->FileId.QuadPart; + /* bpo-37834: Only actual symlinks set the S_IFLNK flag. But lstat() will + open other name surrogate reparse points without traversing them. To + detect/handle these, check st_file_attributes and st_reparse_tag. */ + result->st_reparse_tag = info->ReparseTag; + if (info->FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && + info->ReparseTag == IO_REPARSE_TAG_SYMLINK) { + /* set the bits that make this a symlink */ + result->st_mode = (result->st_mode & ~S_IFMT) | S_IFLNK; + } + result->st_file_attributes = info->FileAttributes; + switch (info->DeviceType) { + case FILE_DEVICE_DISK: + case FILE_DEVICE_VIRTUAL_DISK: + case FILE_DEVICE_DFS: + case FILE_DEVICE_CD_ROM: + case FILE_DEVICE_CONTROLLER: + case FILE_DEVICE_DATALINK: + break; + case FILE_DEVICE_DISK_FILE_SYSTEM: + case FILE_DEVICE_CD_ROM_FILE_SYSTEM: + case FILE_DEVICE_NETWORK_FILE_SYSTEM: + result->st_mode = (result->st_mode & ~S_IFMT) | 0x6000; /* _S_IFBLK */ + break; + case FILE_DEVICE_CONSOLE: + case FILE_DEVICE_NULL: + case FILE_DEVICE_KEYBOARD: + case FILE_DEVICE_MODEM: + case FILE_DEVICE_MOUSE: + case FILE_DEVICE_PARALLEL_PORT: + case FILE_DEVICE_PRINTER: + case FILE_DEVICE_SCREEN: + case FILE_DEVICE_SERIAL_PORT: + case FILE_DEVICE_SOUND: + /* \\.\nul */ + result->st_mode = (result->st_mode & ~S_IFMT) | _S_IFCHR; + break; + case FILE_DEVICE_NAMED_PIPE: + /* \\.\pipe\spam */ + result->st_mode = (result->st_mode & ~S_IFMT) | _S_IFIFO; + break; + default: + if (info->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + /* \\.\pipe\ or \\.\mailslot\ */ + result->st_mode = (result->st_mode & ~S_IFMT) | _S_IFDIR; + } + break; + } +} + #endif /* Return information about a file. @@ -1123,6 +1203,7 @@ _Py_fstat_noraise(int fd, struct _Py_stat_struct *status) { #ifdef MS_WINDOWS BY_HANDLE_FILE_INFORMATION info; + FILE_BASIC_INFO basicInfo; HANDLE h; int type; @@ -1154,14 +1235,15 @@ _Py_fstat_noraise(int fd, struct _Py_stat_struct *status) return 0; } - if (!GetFileInformationByHandle(h, &info)) { + if (!GetFileInformationByHandle(h, &info) || + !GetFileInformationByHandleEx(h, FileBasicInfo, &basicInfo, sizeof(basicInfo))) { /* The Win32 error is already set, but we also set errno for callers who expect it */ errno = winerror_to_errno(GetLastError()); return -1; } - _Py_attribute_data_to_stat(&info, 0, status); + _Py_attribute_data_to_stat(&info, 0, &basicInfo, status); /* specific to fstat() */ status->st_ino = (((uint64_t)info.nFileIndexHigh) << 32) + info.nFileIndexLow; return 0; diff --git a/configure b/configure index 3f8daf9dad5fd8..cdde760e2a8dea 100755 --- a/configure +++ b/configure @@ -16075,7 +16075,7 @@ for ac_func in \ setitimer setlocale setpgid setpgrp setpriority setregid setresgid \ setresuid setreuid setsid setuid setvbuf shutdown sigaction sigaltstack \ sigfillset siginterrupt sigpending sigrelse sigtimedwait sigwait \ - sigwaitinfo snprintf splice strftime strlcpy strsignal symlinkat sync \ + sigwaitinfo snprintf splice statx strftime strlcpy strsignal symlinkat sync \ sysconf system tcgetpgrp tcsetpgrp tempnam timegm times tmpfile \ tmpnam tmpnam_r truncate ttyname umask uname unlinkat utimensat utimes vfork \ wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \ diff --git a/configure.ac b/configure.ac index 734a4db8389915..a4eed415bffa5e 100644 --- a/configure.ac +++ b/configure.ac @@ -4790,7 +4790,7 @@ AC_CHECK_FUNCS([ \ setitimer setlocale setpgid setpgrp setpriority setregid setresgid \ setresuid setreuid setsid setuid setvbuf shutdown sigaction sigaltstack \ sigfillset siginterrupt sigpending sigrelse sigtimedwait sigwait \ - sigwaitinfo snprintf splice strftime strlcpy strsignal symlinkat sync \ + sigwaitinfo snprintf splice statx strftime strlcpy strsignal symlinkat sync \ sysconf system tcgetpgrp tcsetpgrp tempnam timegm times tmpfile \ tmpnam tmpnam_r truncate ttyname umask uname unlinkat utimensat utimes vfork \ wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \ diff --git a/pyconfig.h.in b/pyconfig.h.in index 236cee6588d49b..03537cad055908 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1181,6 +1181,9 @@ /* Define to 1 if you have the `statvfs' function. */ #undef HAVE_STATVFS +/* Define to 1 if you have the `statx' function. */ +#undef HAVE_STATX + /* Define if you have struct stat.st_mtim.tv_nsec */ #undef HAVE_STAT_TV_NSEC