Skip to content

Commit

Permalink
pythongh-99726: Improves correctness of stat results for Windows, and…
Browse files Browse the repository at this point in the history
… uses faster API when available (pythonGH-102149)

This deprecates `st_ctime` fields on Windows, with the intent to change them to contain the correct value in 3.14. For now, they should keep returning the creation time as they always have.
  • Loading branch information
zooba authored and warsaw committed Apr 11, 2023
1 parent 4eba33f commit ea7d6fe
Show file tree
Hide file tree
Showing 10 changed files with 446 additions and 82 deletions.
88 changes: 65 additions & 23 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2858,6 +2858,12 @@ features:
Added support for the :class:`~os.PathLike` interface. Added support
for :class:`bytes` paths on Windows.

.. versionchanged:: 3.12
The ``st_ctime`` attribute of a stat result is deprecated on Windows.
The file creation time is properly available as ``st_birthtime``, and
in the future ``st_ctime`` may be changed to return zero or the
metadata change time, if available.


.. function:: stat(path, *, dir_fd=None, follow_symlinks=True)

Expand Down Expand Up @@ -2973,10 +2979,12 @@ features:

.. attribute:: st_ctime

Platform dependent:
Time of most recent metadata change expressed in seconds.

* the time of most recent metadata change on Unix,
* the time of creation on Windows, expressed in seconds.
.. versionchanged:: 3.12
``st_ctime`` is deprecated on Windows. Use ``st_birthtime`` for
the file creation time. In the future, ``st_ctime`` will contain
the time of the most recent metadata change, as for other platforms.

.. attribute:: st_atime_ns

Expand All @@ -2989,29 +2997,48 @@ features:

.. attribute:: st_ctime_ns

Platform dependent:
Time of most recent metadata change expressed in nanoseconds as an
integer.

.. versionchanged:: 3.12
``st_ctime_ns`` is deprecated on Windows. Use ``st_birthtime_ns``
for the file creation time. In the future, ``st_ctime`` will contain
the time of the most recent metadata change, as for other platforms.

.. attribute:: st_birthtime

Time of file creation expressed in seconds. This attribute is not
always available, and may raise :exc:`AttributeError`.

.. versionchanged:: 3.12
``st_birthtime`` is now available on Windows.

.. attribute:: st_birthtime_ns

* the time of most recent metadata change on Unix,
* the time of creation on Windows, expressed in nanoseconds as an
integer.
Time of file creation expressed in nanoseconds as an integer.
This attribute is not always available, and may raise
:exc:`AttributeError`.

.. versionadded:: 3.12

.. 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` and :attr:`st_birthtime` attributes
depend on the operating system and the file system. For example, on
Windows systems using the 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
systems do not provide nanosecond precision. On systems that do
provide nanosecond precision, the floating-point object used to
store :attr:`st_atime`, :attr:`st_mtime`, and :attr:`st_ctime`
cannot preserve all of it, and as such will be slightly inexact.
If you need the exact timestamps you should always use
:attr:`st_atime_ns`, :attr:`st_mtime_ns`, and :attr:`st_ctime_ns`.
:attr:`st_ctime_ns` and :attr:`st_birthtime_ns` are always expressed in
nanoseconds, many systems do not provide nanosecond precision. On
systems that do provide nanosecond precision, the floating-point object
used to store :attr:`st_atime`, :attr:`st_mtime`, :attr:`st_ctime` and
:attr:`st_birthtime` cannot preserve all of it, and as such will be
slightly inexact. If you need the exact timestamps you should always use
:attr:`st_atime_ns`, :attr:`st_mtime_ns`, :attr:`st_ctime_ns` and
:attr:`st_birthtime_ns`.

On some Unix systems (such as Linux), the following attributes may also be
available:
Expand Down Expand Up @@ -3041,10 +3068,6 @@ features:

File generation number.

.. attribute:: st_birthtime

Time of file creation.

On Solaris and derivatives, the following attributes may also be
available:

Expand Down Expand Up @@ -3117,6 +3140,25 @@ features:
files as :const:`S_IFCHR`, :const:`S_IFIFO` or :const:`S_IFBLK`
as appropriate.

.. versionchanged:: 3.12
On Windows, :attr:`st_ctime` is deprecated. Eventually, it will
contain the last metadata change time, for consistency with other
platforms, but for now still contains creation time.
Use :attr:`st_birthtime` for the creation time.

.. versionchanged:: 3.12
On Windows, :attr:`st_ino` may now be up to 128 bits, depending
on the file system. Previously it would not be above 64 bits, and
larger file identifiers would be arbitrarily packed.

.. versionchanged:: 3.12
On Windows, :attr:`st_rdev` no longer returns a value. Previously
it would contain the same as :attr:`st_dev`, which was incorrect.

.. versionadded:: 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
Expand Down
16 changes: 16 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,16 @@ os
functions on Windows for enumerating drives, volumes and mount points.
(Contributed by Steve Dower in :gh:`102519`.)

* :func:`os.stat` and :func:`os.lstat` are now more accurate on Windows.
The ``st_birthtime`` field will now be filled with the creation time
of the file, and ``st_ctime`` is deprecated but still contains the
creation time (but in the future will return the last metadata change,
for consistency with other platforms). ``st_dev`` may be up to 64 bits
and ``st_ino`` up to 128 bits depending on your file system, and
``st_rdev`` is always set to zero rather than incorrect values.
Both functions may be significantly faster on newer releases of
Windows. (Contributed by Steve Dower in :gh:`99726`.)

os.path
-------

Expand Down Expand Up @@ -469,6 +479,12 @@ Deprecated
warning at compile time. This field will be removed in Python 3.14.
(Contributed by Ramvikrams and Kumar Aditya in :gh:`101193`. PEP by Ken Jin.)

* The ``st_ctime`` fields return by :func:`os.stat` and :func:`os.lstat` on
Windows are deprecated. In a future release, they will contain the last
metadata change time, consistent with other platforms. For now, they still
contain the creation time, which is also available in the new ``st_birthtime``
field. (Contributed by Steve Dower in :gh:`99726`.)

Pending Removal in Python 3.13
------------------------------

Expand Down
5 changes: 4 additions & 1 deletion Include/internal/pycore_fileutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ PyAPI_FUNC(PyObject *) _Py_device_encoding(int);

#ifdef MS_WINDOWS
struct _Py_stat_struct {
unsigned long st_dev;
uint64_t st_dev;
uint64_t st_ino;
unsigned short st_mode;
int st_nlink;
Expand All @@ -80,8 +80,11 @@ struct _Py_stat_struct {
int st_mtime_nsec;
time_t st_ctime;
int st_ctime_nsec;
time_t st_birthtime;
int st_birthtime_nsec;
unsigned long st_file_attributes;
unsigned long st_reparse_tag;
uint64_t st_ino_high;
};
#else
# define _Py_stat_struct stat
Expand Down
80 changes: 80 additions & 0 deletions Include/internal/pycore_fileutils_windows.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#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;
ULONG Reserved;
FILE_ID_128 FileId128;
LARGE_INTEGER VolumeSerialNumber;
} 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
12 changes: 11 additions & 1 deletion Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,15 @@ def trunc(x): return x
nanosecondy = getattr(result, name + "_ns") // 10000
self.assertAlmostEqual(floaty, nanosecondy, delta=2)

# Ensure both birthtime and birthtime_ns roughly agree, if present
try:
floaty = int(result.st_birthtime * 100000)
nanosecondy = result.st_birthtime_ns // 10000
except AttributeError:
pass
else:
self.assertAlmostEqual(floaty, nanosecondy, delta=2)

try:
result[200]
self.fail("No exception raised")
Expand Down Expand Up @@ -4234,7 +4243,8 @@ def assert_stat_equal(self, stat1, stat2, skip_fields):
for attr in dir(stat1):
if not attr.startswith("st_"):
continue
if attr in ("st_dev", "st_ino", "st_nlink"):
if attr in ("st_dev", "st_ino", "st_nlink", "st_ctime",
"st_ctime_ns"):
continue
self.assertEqual(getattr(stat1, attr),
getattr(stat2, attr),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improves correctness of stat results for Windows, and uses faster API when
available
Loading

0 comments on commit ea7d6fe

Please sign in to comment.