Skip to content

Commit

Permalink
Make Path.status public.
Browse files Browse the repository at this point in the history
  • Loading branch information
barneygale committed Dec 9, 2024
1 parent 5d92785 commit dc403c6
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 7 deletions.
72 changes: 72 additions & 0 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,34 @@ Querying file type and status
.. versionadded:: 3.5


.. attribute:: Path.status

A :class:`Status` object that supports querying file type information. The
object exposes methods like :meth:`~Status.is_dir` that cache their
results, which can help reduce the number of system calls needed when
switching on file type. Care must be taken to avoid incorrectly using
cached results::

>>> p = Path('setup.py')
>>> p.info.is_file()
True
>>> p.unlink()
>>> p.info.is_file() # returns stale info
True
>>> p = Path(p) # get fresh info
>>> p.info.is_file()
False

The value is a :class:`os.DirEntry` instance if the path was generated by
:meth:`Path.iterdir`. These objects are initialized with some information
about the file type; see the :func:`os.scandir` docs for more. In other
cases, this attribute is an instance of an internal pathlib class which
initially knows nothing about the file status. In either case, merely
accessing :attr:`Path.info` does not perform any filesystem queries.

.. versionadded:: 3.14


Reading and writing files
^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -1903,3 +1931,47 @@ 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:: Status()

A :class:`typing.Protocol` describing the :attr:`Path.status` attribute.
Implementations may return cached results from their methods.

.. method:: is_dir(*, follow_symlinks=True)

Return ``True`` if this status is a directory or a symbolic link
pointing to a directory; return ``False`` if the status is or points to
any other kind of file, or if it doesn’t exist anymore.

If *follow_symlinks* is ``False``, return ``True`` only if this status
is a directory (without following symlinks); return ``False`` if the
status is any other kind of file or if it doesn’t exist anymore.

.. method:: is_file(*, follow_symlinks=True)

Return ``True`` if this status is a file or a symbolic link pointing to
a file; return ``False`` if the status is or points to a directory or
other non-file, or if it doesn’t exist anymore.

If *follow_symlinks* is ``False``, return ``True`` only if this status
is a file (without following symlinks); return ``False`` if the status
is a directory or other other non-file, or if it doesn’t exist anymore.

.. method:: is_symlink()

Return ``True`` if this status is a symbolic link (even if broken);
return ``False`` if the status points to a directory or any kind of
file, or if it doesn’t exist anymore.
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,15 @@ pathlib

(Contributed by Barney Gale in :gh:`73991`.)

* Add :attr:`pathlib.Path.status` attribute, which stores an object
implementing the :class:`pathlib.types.Status` protocol (also new). The
object supports querying the file type and internally caching
:func:`~os.stat` results. Path objects generated by :meth:`Path.iterdir`
store :class:`os.DirEntry` objects, which are initialized with file type
information gleaned from scanning the parent directory.

(Contributed by Barney Gale in :gh:`125413`.)


pdb
---
Expand Down
11 changes: 6 additions & 5 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class PathGlobber(_GlobberBase):
@staticmethod
def scandir(path):
"""Like os.scandir(), but generates (entry, name, path) tuples."""
return ((child._status, child.name, child) for child in path.iterdir())
return ((child.status, child.name, child) for child in path.iterdir())

@staticmethod
def concat_path(path, text):
Expand Down Expand Up @@ -372,11 +372,12 @@ def _unsupported_msg(cls, attribute):
return f"{cls.__name__}.{attribute} is unsupported"

@property
def _status(self):
def status(self):
"""
An os.DirEntry-like object, if this path was generated by iterdir().
A Status object that exposes the file type and other file attributes
of this path.
"""
# TODO: make this public + abstract, delete PathBase.stat().
# TODO: make this abstract, delete PathBase.stat().
return self

def stat(self, *, follow_symlinks=True):
Expand Down Expand Up @@ -638,7 +639,7 @@ def walk(self, top_down=True, on_error=None, follow_symlinks=False):
try:
for child in path.iterdir():
try:
if child._status.is_dir(follow_symlinks=follow_symlinks):
if child.status.is_dir(follow_symlinks=follow_symlinks):
if not top_down:
paths.append(child)
dirnames.append(child.name)
Expand Down
74 changes: 74 additions & 0 deletions Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from errno import EXDEV
from glob import _StringGlobber
from itertools import chain
from stat import S_ISDIR, S_ISREG, S_ISLNK
from _collections_abc import Sequence

try:
Expand Down Expand Up @@ -58,6 +59,67 @@ def __repr__(self):
return "<{}.parents>".format(type(self._path).__name__)


class _PathStatus:
"""This object provides os.DirEntry-like access to the file type and file
attributes. Don't try to construct it yourself."""
__slots__ = ('_path', '_repr', '_link_mode', '_file_mode')

def __init__(self, path):
self._path = str(path)
self._repr = f"<{type(path).__name__}.info>"

def __repr__(self):
return self._repr

def _get_link_mode(self):
try:
return self._link_mode
except AttributeError:
try:
self._link_mode = os.lstat(self._path).st_mode
except (OSError, ValueError):
self._link_mode = 0
if not self.is_symlink():
# Not a symlink, so stat() will give the same result.
self._file_mode = self._link_mode
return self._link_mode

def _get_file_mode(self):
try:
return self._file_mode
except AttributeError:
try:
self._file_mode = os.stat(self._path).st_mode
except (OSError, ValueError):
self._file_mode = 0
return self._file_mode

def is_dir(self, *, follow_symlinks=True):
"""
Whether this path is a directory.
"""

if follow_symlinks:
return S_ISDIR(self._get_file_mode())
else:
return S_ISDIR(self._get_link_mode())

def is_file(self, *, follow_symlinks=True):
"""
Whether this path is a regular file.
"""
if follow_symlinks:
return S_ISREG(self._get_file_mode())
else:
return S_ISREG(self._get_link_mode())

def is_symlink(self):
"""
Whether this path is a symbolic link.
"""
return S_ISLNK(self._get_link_mode())


class PurePath(PurePathBase):
"""Base class for manipulating paths without I/O.
Expand Down Expand Up @@ -536,6 +598,18 @@ def __new__(cls, *args, **kwargs):
cls = WindowsPath if os.name == 'nt' else PosixPath
return object.__new__(cls)

@property
def status(self):
"""
A Status object that exposes the file type and other file attributes
of this path.
"""
try:
return self._status
except AttributeError:
self._status = _PathStatus(self)
return self._status

def stat(self, *, follow_symlinks=True):
"""
Return the result of the stat() system call on this path, like
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import unittest

from pathlib._abc import UnsupportedOperation, PurePathBase, PathBase
from pathlib._types import Parser, Status
from pathlib.types import Parser, Status
import posixpath

from test.support.os_helper import TESTFN
Expand Down Expand Up @@ -1901,7 +1901,7 @@ def test_iterdir_nodir(self):
def test_iterdir_status(self):
p = self.cls(self.base)
for child in p.iterdir():
entry = child._status
entry = child.status
self.assertIsInstance(entry, Status)
self.assertEqual(entry.is_dir(follow_symlinks=False),
child.is_dir(follow_symlinks=False))
Expand Down

0 comments on commit dc403c6

Please sign in to comment.