Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Doc/library/filesys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ in this chapter is:
.. toctree::

pathlib.rst
pathlib.types.rst
os.path.rst
stat.rst
filecmp.rst
Expand Down
96 changes: 42 additions & 54 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down Expand Up @@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -1750,6 +1754,43 @@ 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)
.. method:: is_dir(*, follow_symlinks=True)
.. method:: is_file(*, follow_symlinks=True)
.. method:: is_symlink()

These methods return cached information about the path, as in
:class:`pathlib.types.PathInfo`.

.. 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.

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:

Pattern language
Expand Down Expand Up @@ -1925,56 +1966,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 <pathlib.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.
56 changes: 56 additions & 0 deletions Doc/library/pathlib.types.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
:mod:`!pathlib.types` --- Types for virtual filesystem paths
============================================================

.. module:: pathlib.types
: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.


.. 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.
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------

Expand Down
43 changes: 23 additions & 20 deletions Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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."""
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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."""
Expand All @@ -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)

Expand All @@ -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
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import errno
import ntpath
import pathlib
import pathlib.types
import pickle
import posixpath
import socket
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -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.
Loading