From 53b28f4806a40c243e305610e69c6c29cc03eab5 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 24 Sep 2025 03:06:19 +0100 Subject: [PATCH 1/6] GH-139174: Add `pathlib.Path.info.stat()` Add `stat()` method to `pathlib.Path.info` that returns a (possibly cached) `os.stat_result` object. We don't add it to `pathlib.types.PathInfo` because it's too specific to local filesystem paths. This requires a bit of reworking of the docs to explain! Rename `pathlib._Info` to `pathlib.Info` and document it, including the new `stat()` method. Ensure it can't be instantiated by users. Move the existing docs for `pathlib.types` to a new page. --- Doc/library/filesys.rst | 1 + Doc/library/pathlib.rst | 117 ++++++++++-------- Doc/library/pathlib.types.rst | 58 +++++++++ Doc/whatsnew/3.15.rst | 11 ++ Lib/pathlib/__init__.py | 43 ++++--- Lib/test/test_pathlib/test_pathlib.py | 25 ++++ ...-09-24-03-14-49.gh-issue-139174.05BpLG.rst | 5 + 7 files changed, 186 insertions(+), 74 deletions(-) create mode 100644 Doc/library/pathlib.types.rst create mode 100644 Misc/NEWS.d/next/Library/2025-09-24-03-14-49.gh-issue-139174.05BpLG.rst diff --git a/Doc/library/filesys.rst b/Doc/library/filesys.rst index f1ea4761af7cb1..2b3ee8b0298db1 100644 --- a/Doc/library/filesys.rst +++ b/Doc/library/filesys.rst @@ -13,6 +13,7 @@ in this chapter is: .. toctree:: pathlib.rst + pathlib.types.rst os.path.rst stat.rst filecmp.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 79e0b7f09eaa77..649580c06ff1d1 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1173,7 +1173,7 @@ Querying file type and status .. attribute:: Path.info - A :class:`~pathlib.types.PathInfo` object that supports querying file type + An :class:`Info` object that supports querying file type information. The object exposes methods that cache their results, which can help reduce the number of system calls needed when switching on file type. For example:: @@ -1202,6 +1202,10 @@ Querying file type and status .. versionadded:: 3.14 + .. versionchanged:: next + Value is specifically a :class:`Info` object, rather than an unnamed + implementation of the :class:`pathlib.types.PathInfo` protocol. + Reading and writing files ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1750,6 +1754,64 @@ Permissions and ownership symbolic link's mode is changed rather than its target's. +Path information +---------------- + +.. class:: Info + + Class that supports fetching and caching information about a local + filesystem path. An instance of this class is available from + :attr:`Path.info`; do not instantiate it directly. + + This is an implementation of the :class:`pathlib.types.PathInfo` protocol + with an additional method specific to local paths (:meth:`~Info.stat`.) + + .. versionadded:: next + + .. method:: exists(*, follow_symlinks=True) + + Return ``True`` if the path is an existing file or directory, or any + other kind of file; return ``False`` if the path doesn't exist. + + If *follow_symlinks* is ``False``, return ``True`` for symlinks without + checking if their targets exist. + + .. method:: is_dir(*, follow_symlinks=True) + + Return ``True`` if the path is a directory, or a symbolic link pointing + to a directory; return ``False`` if the path is (or points to) any other + kind of file, or if it doesn't exist. + + If *follow_symlinks* is ``False``, return ``True`` only if the path + is a directory (without following symlinks); return ``False`` if the + path is any other kind of file, or if it doesn't exist. + + .. method:: is_file(*, follow_symlinks=True) + + Return ``True`` if the path is a file, or a symbolic link pointing to + a file; return ``False`` if the path is (or points to) a directory or + other non-file, or if it doesn't exist. + + If *follow_symlinks* is ``False``, return ``True`` only if the path + is a file (without following symlinks); return ``False`` if the path + is a directory or other non-file, or if it doesn't exist. + + .. method:: is_symlink() + + Return ``True`` if the path is a symbolic link (even if broken); return + ``False`` if the path is a directory or any kind of file, or if it + doesn't exist. + + .. method:: stat(*, follow_symlinks=True) + + Return an :class:`os.stat_result` object containing low-level + information about this path, like :func:`os.stat`. The result is cached + on the :class:`Info` object. + + If *follow_symlinks* is ``False`` and this path is a symlink, return + information about the symlink rather than its target. + + .. _pathlib-pattern-language: Pattern language @@ -1925,56 +1987,3 @@ Below is a table mapping various :mod:`os` functions to their corresponding .. [4] :func:`os.walk` always follows symlinks when categorizing paths into *dirnames* and *filenames*, whereas :meth:`Path.walk` categorizes all symlinks into *filenames* when *follow_symlinks* is false (the default.) - - -Protocols ---------- - -.. module:: pathlib.types - :synopsis: pathlib types for static type checking - - -The :mod:`pathlib.types` module provides types for static type checking. - -.. versionadded:: 3.14 - - -.. class:: PathInfo() - - A :class:`typing.Protocol` describing the - :attr:`Path.info ` attribute. Implementations may - return cached results from their methods. - - .. method:: exists(*, follow_symlinks=True) - - Return ``True`` if the path is an existing file or directory, or any - other kind of file; return ``False`` if the path doesn't exist. - - If *follow_symlinks* is ``False``, return ``True`` for symlinks without - checking if their targets exist. - - .. method:: is_dir(*, follow_symlinks=True) - - Return ``True`` if the path is a directory, or a symbolic link pointing - to a directory; return ``False`` if the path is (or points to) any other - kind of file, or if it doesn't exist. - - If *follow_symlinks* is ``False``, return ``True`` only if the path - is a directory (without following symlinks); return ``False`` if the - path is any other kind of file, or if it doesn't exist. - - .. method:: is_file(*, follow_symlinks=True) - - Return ``True`` if the path is a file, or a symbolic link pointing to - a file; return ``False`` if the path is (or points to) a directory or - other non-file, or if it doesn't exist. - - If *follow_symlinks* is ``False``, return ``True`` only if the path - is a file (without following symlinks); return ``False`` if the path - is a directory or other non-file, or if it doesn't exist. - - .. method:: is_symlink() - - Return ``True`` if the path is a symbolic link (even if broken); return - ``False`` if the path is a directory or any kind of file, or if it - doesn't exist. diff --git a/Doc/library/pathlib.types.rst b/Doc/library/pathlib.types.rst new file mode 100644 index 00000000000000..11fb901a8477d9 --- /dev/null +++ b/Doc/library/pathlib.types.rst @@ -0,0 +1,58 @@ +:mod:`!pathlib.types` --- Types for virtual filesystem paths +============================================================ + +.. module:: pathlib + :synopsis: pathlib types for virtual filesystem paths + +.. versionadded:: 3.14 + +**Source code:** :source:`Lib/pathlib/types.py` + +-------------- + +The :mod:`pathlib.types` module provides protocols for user-defined types that +resemble :class:`pathlib.Path` and its associated classes. This module +includes type annotations, so it's also useful for static type checking. + +.. versionadded:: 3.14 + + +.. class:: PathInfo() + + A :class:`typing.Protocol` that supports fetching and caching information + about a path. :class:`pathlib.Info` is an implementation of this protocol + with additional methods specific to local filesystems. + + .. method:: exists(*, follow_symlinks=True) + + Return ``True`` if the path is an existing file or directory, or any + other kind of file; return ``False`` if the path doesn't exist. + + If *follow_symlinks* is ``False``, return ``True`` for symlinks without + checking if their targets exist. + + .. method:: is_dir(*, follow_symlinks=True) + + Return ``True`` if the path is a directory, or a symbolic link pointing + to a directory; return ``False`` if the path is (or points to) any other + kind of file, or if it doesn't exist. + + If *follow_symlinks* is ``False``, return ``True`` only if the path + is a directory (without following symlinks); return ``False`` if the + path is any other kind of file, or if it doesn't exist. + + .. method:: is_file(*, follow_symlinks=True) + + Return ``True`` if the path is a file, or a symbolic link pointing to + a file; return ``False`` if the path is (or points to) a directory or + other non-file, or if it doesn't exist. + + If *follow_symlinks* is ``False``, return ``True`` only if the path + is a file (without following symlinks); return ``False`` if the path + is a directory or other non-file, or if it doesn't exist. + + .. method:: is_symlink() + + Return ``True`` if the path is a symbolic link (even if broken); return + ``False`` if the path is a directory or any kind of file, or if it + doesn't exist. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7b146621dddcfa..0bcacc707d56ed 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -407,6 +407,17 @@ os.path (Contributed by Petr Viktorin for :cve:`2025-4517`.) +pathlib +------- + +* Add :class:`pathlib.Info` type, which queries and caches path metadata for + local filesystem paths. This type is used by the :attr:`pathlib.Path.info` + attribute, which consequently gains a :meth:`~pathlib.Info.stat` method; + this method returns an :class:`os.stat_result` object with low-level details + about the path. + (Contributed by Barney Gale in :gh:`139174`.) + + resource -------- diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 8a892102cc00ea..8253ee41adf746 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -613,41 +613,44 @@ class PureWindowsPath(PurePath): __slots__ = () -class _Info: +class Info: + """Implementation of pathlib.types.PathInfo that provides cached + information about a local filesystem path. Don't try to construct it + yourself. + """ __slots__ = ('_path',) def __init__(self, path): + if type(self) is Info: + raise TypeError('Info cannot be directly instantiated; ' + 'use Path.info instead.') self._path = path - def __repr__(self): - path_type = "WindowsPath" if os.name == "nt" else "PosixPath" - return f"<{path_type}.info>" - - def _stat(self, *, follow_symlinks=True): + def stat(self, *, follow_symlinks=True): """Return the status as an os.stat_result.""" raise NotImplementedError def _posix_permissions(self, *, follow_symlinks=True): """Return the POSIX file permissions.""" - return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode) + return S_IMODE(self.stat(follow_symlinks=follow_symlinks).st_mode) def _file_id(self, *, follow_symlinks=True): """Returns the identifier of the file.""" - st = self._stat(follow_symlinks=follow_symlinks) + st = self.stat(follow_symlinks=follow_symlinks) return st.st_dev, st.st_ino def _access_time_ns(self, *, follow_symlinks=True): """Return the access time in nanoseconds.""" - return self._stat(follow_symlinks=follow_symlinks).st_atime_ns + return self.stat(follow_symlinks=follow_symlinks).st_atime_ns def _mod_time_ns(self, *, follow_symlinks=True): """Return the modify time in nanoseconds.""" - return self._stat(follow_symlinks=follow_symlinks).st_mtime_ns + return self.stat(follow_symlinks=follow_symlinks).st_mtime_ns if hasattr(os.stat_result, 'st_flags'): def _bsd_flags(self, *, follow_symlinks=True): """Return the flags.""" - return self._stat(follow_symlinks=follow_symlinks).st_flags + return self.stat(follow_symlinks=follow_symlinks).st_flags if hasattr(os, 'listxattr'): def _xattrs(self, *, follow_symlinks=True): @@ -666,7 +669,7 @@ def _xattrs(self, *, follow_symlinks=True): _STAT_RESULT_ERROR = [] # falsy sentinel indicating stat() failed. -class _StatResultInfo(_Info): +class _StatResultInfo(Info): """Implementation of pathlib.types.PathInfo that provides status information by querying a wrapped os.stat_result object. Don't try to construct it yourself.""" @@ -677,7 +680,7 @@ def __init__(self, path): self._stat_result = None self._lstat_result = None - def _stat(self, *, follow_symlinks=True): + def stat(self, *, follow_symlinks=True): """Return the status as an os.stat_result.""" if follow_symlinks: if not self._stat_result: @@ -705,7 +708,7 @@ def exists(self, *, follow_symlinks=True): if self._lstat_result is _STAT_RESULT_ERROR: return False try: - self._stat(follow_symlinks=follow_symlinks) + self.stat(follow_symlinks=follow_symlinks) except (OSError, ValueError): return False return True @@ -719,7 +722,7 @@ def is_dir(self, *, follow_symlinks=True): if self._lstat_result is _STAT_RESULT_ERROR: return False try: - st = self._stat(follow_symlinks=follow_symlinks) + st = self.stat(follow_symlinks=follow_symlinks) except (OSError, ValueError): return False return S_ISDIR(st.st_mode) @@ -733,7 +736,7 @@ def is_file(self, *, follow_symlinks=True): if self._lstat_result is _STAT_RESULT_ERROR: return False try: - st = self._stat(follow_symlinks=follow_symlinks) + st = self.stat(follow_symlinks=follow_symlinks) except (OSError, ValueError): return False return S_ISREG(st.st_mode) @@ -743,13 +746,13 @@ def is_symlink(self): if self._lstat_result is _STAT_RESULT_ERROR: return False try: - st = self._stat(follow_symlinks=False) + st = self.stat(follow_symlinks=False) except (OSError, ValueError): return False return S_ISLNK(st.st_mode) -class _DirEntryInfo(_Info): +class _DirEntryInfo(Info): """Implementation of pathlib.types.PathInfo that provides status information by querying a wrapped os.DirEntry object. Don't try to construct it yourself.""" @@ -759,7 +762,7 @@ def __init__(self, entry): super().__init__(entry.path) self._entry = entry - def _stat(self, *, follow_symlinks=True): + def stat(self, *, follow_symlinks=True): """Return the status as an os.stat_result.""" return self._entry.stat(follow_symlinks=follow_symlinks) @@ -768,7 +771,7 @@ def exists(self, *, follow_symlinks=True): if not follow_symlinks: return True try: - self._stat(follow_symlinks=follow_symlinks) + self.stat(follow_symlinks=follow_symlinks) except OSError: return False return True diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index a1105aae6351b6..53ee5f341a042e 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -6,6 +6,7 @@ import errno import ntpath import pathlib +import pathlib.types import pickle import posixpath import socket @@ -2522,6 +2523,11 @@ def test_symlink_to_unsupported(self): with self.assertRaises(pathlib.UnsupportedOperation): q.symlink_to(p) + def test_info(self): + p = self.cls(self.base) + self.assertIsInstance(p.info, pathlib.Info) + self.assertIsInstance(p.info, pathlib.types.PathInfo) + def test_info_exists_caching(self): p = self.cls(self.base) q = p / 'myfile' @@ -2562,6 +2568,19 @@ def test_info_is_symlink_caching(self): q.unlink() self.assertTrue(q.info.is_symlink()) + def test_info_stat(self): + p = self.cls(self.base) + q = p / 'myfile' + self.assertRaises(FileNotFoundError, q.info.stat) + self.assertRaises(FileNotFoundError, q.info.stat) + q.write_text('hullo') + st = os.stat(q) + self.assertTrue(os.path.samestat(st, q.info.stat())) + q.unlink() + self.assertTrue(os.path.samestat(st, q.info.stat())) + q = p / 'mylink' # same path, new instance. + self.assertRaises(FileNotFoundError, q.info.stat) + def test_stat(self): statA = self.cls(self.base).joinpath('fileA').stat() statB = self.cls(self.base).joinpath('dirB', 'fileB').stat() @@ -3671,5 +3690,11 @@ def test_rtruediv(self): 10 / pathlib.PurePath("test") +class InfoTest(unittest.TestCase): + def test_info_not_instantiable(self): + with self.assertRaises(TypeError): + pathlib.Info('foo') + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2025-09-24-03-14-49.gh-issue-139174.05BpLG.rst b/Misc/NEWS.d/next/Library/2025-09-24-03-14-49.gh-issue-139174.05BpLG.rst new file mode 100644 index 00000000000000..b365906a1089a5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-24-03-14-49.gh-issue-139174.05BpLG.rst @@ -0,0 +1,5 @@ +Add :class:`pathlib.Info` type, which queries and caches path metadata for +local filesystem paths. This type is used by the :attr:`pathlib.Path.info` +attribute, which consequently gains a :meth:`~pathlib.Info.stat` method; +this method returns an :class:`os.stat_result` object with low-level details +about the path. From e335864f2a1d79a34435dedf0d074123efb3a33b Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 24 Sep 2025 03:23:33 +0100 Subject: [PATCH 2/6] Fix '.. module::' on new page --- Doc/library/pathlib.types.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/pathlib.types.rst b/Doc/library/pathlib.types.rst index 11fb901a8477d9..b53a74a50528b4 100644 --- a/Doc/library/pathlib.types.rst +++ b/Doc/library/pathlib.types.rst @@ -1,7 +1,7 @@ :mod:`!pathlib.types` --- Types for virtual filesystem paths ============================================================ -.. module:: pathlib +.. module:: pathlib.types :synopsis: pathlib types for virtual filesystem paths .. versionadded:: 3.14 From 02cfee1dc1a3e2a45b0785c930dce6da2939593d Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 24 Sep 2025 03:53:17 +0100 Subject: [PATCH 3/6] Add note about missing Windows fields when backed by `os.DirEntry`. --- Doc/library/pathlib.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 649580c06ff1d1..cfb15e6414d450 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1811,6 +1811,11 @@ Path information If *follow_symlinks* is ``False`` and this path is a symlink, return information about the symlink rather than its target. + On Windows, if the path was generated by scanning a directory, then the + ``st_ino``, ``st_dev`` and ``st_nlink`` attributes of the + :class:`~os.stat_result` are always set to zero. Call :meth:`Path.stat` + to get these attributes. + .. _pathlib-pattern-language: From 1fa73e617b4729b93c95195d5510ba8025044331 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 24 Sep 2025 22:35:30 +0100 Subject: [PATCH 4/6] De-duplicate docs --- Doc/library/pathlib.rst | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index cfb15e6414d450..8f6cbf73f4b448 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1769,38 +1769,12 @@ Path information .. versionadded:: next .. method:: exists(*, follow_symlinks=True) - - Return ``True`` if the path is an existing file or directory, or any - other kind of file; return ``False`` if the path doesn't exist. - - If *follow_symlinks* is ``False``, return ``True`` for symlinks without - checking if their targets exist. - .. method:: is_dir(*, follow_symlinks=True) - - Return ``True`` if the path is a directory, or a symbolic link pointing - to a directory; return ``False`` if the path is (or points to) any other - kind of file, or if it doesn't exist. - - If *follow_symlinks* is ``False``, return ``True`` only if the path - is a directory (without following symlinks); return ``False`` if the - path is any other kind of file, or if it doesn't exist. - .. method:: is_file(*, follow_symlinks=True) - - Return ``True`` if the path is a file, or a symbolic link pointing to - a file; return ``False`` if the path is (or points to) a directory or - other non-file, or if it doesn't exist. - - If *follow_symlinks* is ``False``, return ``True`` only if the path - is a file (without following symlinks); return ``False`` if the path - is a directory or other non-file, or if it doesn't exist. - .. method:: is_symlink() - Return ``True`` if the path is a symbolic link (even if broken); return - ``False`` if the path is a directory or any kind of file, or if it - doesn't exist. + Return cached information about the path, as in + :class:`pathlib.types.PathInfo`. .. method:: stat(*, follow_symlinks=True) From cb3efe773ee42aa4f06217d0c9a69ce68206a980 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 24 Sep 2025 22:37:30 +0100 Subject: [PATCH 5/6] Remove duplicate '.. versionadded::' --- Doc/library/pathlib.types.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/Doc/library/pathlib.types.rst b/Doc/library/pathlib.types.rst index b53a74a50528b4..fc5ce1a06cbe6c 100644 --- a/Doc/library/pathlib.types.rst +++ b/Doc/library/pathlib.types.rst @@ -14,8 +14,6 @@ The :mod:`pathlib.types` module provides protocols for user-defined types that resemble :class:`pathlib.Path` and its associated classes. This module includes type annotations, so it's also useful for static type checking. -.. versionadded:: 3.14 - .. class:: PathInfo() From 1d3b87480f9c4bdf80af32915434b0fd53f32e1a Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 24 Sep 2025 23:02:15 +0100 Subject: [PATCH 6/6] Tiny clarification --- Doc/library/pathlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 8f6cbf73f4b448..2e20cb8ad7e5f6 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1773,7 +1773,7 @@ Path information .. method:: is_file(*, follow_symlinks=True) .. method:: is_symlink() - Return cached information about the path, as in + These methods return cached information about the path, as in :class:`pathlib.types.PathInfo`. .. method:: stat(*, follow_symlinks=True)