Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-125413: Add pathlib.Path.info attribute #127730

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6046a27
GH-125413: pathlib ABCs: replace `_scandir()` with `_info`
barneygale Dec 7, 2024
a57c4a8
Merge branch 'main' into gh-125413-info
barneygale Dec 9, 2024
76ef028
Rename `_info` to `_status`
barneygale Dec 9, 2024
5d92785
Add `Status` protocol.
barneygale Dec 9, 2024
dc403c6
Make `Path.status` public.
barneygale Dec 9, 2024
5a128b9
Fix docs typos
barneygale Dec 9, 2024
cac77a6
Docs fixes
barneygale Dec 9, 2024
6e09ada
Fix whatsnew
barneygale Dec 9, 2024
1d8713e
Fix _PathStatus repr, exception handling
barneygale Dec 9, 2024
f8ffbbd
Docs improvements
barneygale Dec 9, 2024
0a86e68
Move PathGlobber into glob.py, now that it uses the public path inter…
barneygale Dec 9, 2024
b0b621d
Simplify _PathStatus implementation a little
barneygale Dec 10, 2024
ef650fd
Add some tests
barneygale Dec 10, 2024
7b990c6
Add news
barneygale Dec 10, 2024
cf1073c
Docs tweaks
barneygale Dec 10, 2024
28bcf00
Merge branch 'main' into gh-125413-info
barneygale Dec 11, 2024
764b8ae
Wrap `os.DirEntry` in `_DirEntryStatus`
barneygale Dec 11, 2024
fa8931b
Add `Status.exists()`
barneygale Dec 11, 2024
2bb6221
Fix test name
barneygale Dec 11, 2024
923542b
Use status.exists() in docs example
barneygale Dec 11, 2024
68377c4
Docs editing
barneygale Dec 12, 2024
4f3f434
Few more test cases
barneygale Dec 12, 2024
2f4da5d
Merge branch 'main' into gh-125413-info
barneygale Dec 12, 2024
a7cffe7
Merge branch 'main' into gh-125413-info
barneygale Dec 12, 2024
dc6edc8
Merge branch 'main' into gh-125413-info
barneygale Dec 12, 2024
530771d
Suppress OSErrors
barneygale Dec 17, 2024
89ff6d4
Add Windows implementation using os.path.isdir() etc
barneygale Dec 17, 2024
592603b
Docstrings
barneygale Dec 17, 2024
bd6332a
Optimise Windows implementation a bit
barneygale Dec 17, 2024
f0ee0e9
More tidying of _PathStatus and friends
barneygale Dec 17, 2024
5ae8b06
`status` --> `info`
barneygale Dec 21, 2024
6e25d2d
`Parser` --> `_PathParser`
barneygale Dec 21, 2024
c93237d
Merge branch 'main' into gh-125413-info
barneygale Dec 22, 2024
2624363
Merge branch 'main' into gh-125413-info
barneygale Dec 29, 2024
662fd2d
Merge branch 'main' into gh-125413-info
barneygale Jan 5, 2025
dbc312c
Merge branch 'main' into gh-125413-info
barneygale Jan 8, 2025
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
Prev Previous commit
Next Next commit
Make Path.status public.
  • Loading branch information
barneygale committed Dec 9, 2024
commit dc403c6891a1aabcd1542437ab58269b10265df8
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 @@
.. versionadded:: 3.5


.. attribute:: Path.status

A :class:`Status` object that supports querying file type information. The

Check warning on line 1182 in Doc/library/pathlib.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:class reference target not found: Status [ref.class]

Check warning on line 1182 in Doc/library/pathlib.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:meth reference target not found: Status.is_dir [ref.meth]
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

Check warning on line 1198 in Doc/library/pathlib.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:attr reference target not found: Path.info [ref.attr]
: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 @@
.. [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.

Check warning on line 1950 in Doc/library/pathlib.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:attr reference target not found: Path.status [ref.attr]
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 @@

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

* Add :attr:`pathlib.Path.status` attribute, which stores an object

Check warning on line 542 in Doc/whatsnew/3.14.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:meth reference target not found: Path.iterdir [ref.meth]
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
Loading