From 6046a2715c139e29177a4e4cd217e16299d790f2 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 7 Dec 2024 23:06:16 +0000 Subject: [PATCH 01/29] GH-125413: pathlib ABCs: replace `_scandir()` with `_info` When a path object is generated by `PathBase.iterdir()`, then its `_info` attribute now stores a `os.DirEntry`-like object that can be used to query the file type. This removes any need for a `_scandir()` method. Currently the `_info` attribute is private and only guaranteed to be populated in paths from `iterdir()`. Later on, I'm hoping to rename it to `info` and ensure that it's populated for all kinds of paths (this probably involves adding a `pathlib.FileInfo` class.) In the pathlib ABCs, `info` will replace `stat()` as the lowest-level abstract file status querying mechanism. --- Lib/glob.py | 31 +++++++--------- Lib/pathlib/_abc.py | 45 ++++++++++++----------- Lib/pathlib/_local.py | 21 +++++------ Lib/test/test_pathlib/test_pathlib_abc.py | 27 +++++++------- 4 files changed, 60 insertions(+), 64 deletions(-) diff --git a/Lib/glob.py b/Lib/glob.py index 690ab1b8b9fb1d..850d1e9f1659f2 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -348,7 +348,7 @@ def lexists(path): @staticmethod def scandir(path): - """Implements os.scandir(). + """Like os.scandir(), but generates (entry, name, path) tuples. """ raise NotImplementedError @@ -425,23 +425,18 @@ def wildcard_selector(self, part, parts): def select_wildcard(path, exists=False): try: - # We must close the scandir() object before proceeding to - # avoid exhausting file descriptors when globbing deep trees. - with self.scandir(path) as scandir_it: - entries = list(scandir_it) + entries = self.scandir(path) except OSError: pass else: - prefix = self.add_slash(path) - for entry in entries: - if match is None or match(entry.name): + for entry, entry_name, entry_path in entries: + if match is None or match(entry_name): if dir_only: try: if not entry.is_dir(): continue except OSError: continue - entry_path = self.concat_path(prefix, entry.name) if dir_only: yield from select_next(entry_path, exists=True) else: @@ -483,15 +478,11 @@ def select_recursive(path, exists=False): def select_recursive_step(stack, match_pos): path = stack.pop() try: - # We must close the scandir() object before proceeding to - # avoid exhausting file descriptors when globbing deep trees. - with self.scandir(path) as scandir_it: - entries = list(scandir_it) + entries = self.scandir(path) except OSError: pass else: - prefix = self.add_slash(path) - for entry in entries: + for entry, _entry_name, entry_path in entries: is_dir = False try: if entry.is_dir(follow_symlinks=follow_symlinks): @@ -500,7 +491,6 @@ def select_recursive_step(stack, match_pos): pass if is_dir or not dir_only: - entry_path = self.concat_path(prefix, entry.name) if match is None or match(str(entry_path), match_pos): if dir_only: yield from select_next(entry_path, exists=True) @@ -528,9 +518,16 @@ class _StringGlobber(_GlobberBase): """Provides shell-style pattern matching and globbing for string paths. """ lexists = staticmethod(os.path.lexists) - scandir = staticmethod(os.scandir) concat_path = operator.add + @staticmethod + def scandir(path): + # We must close the scandir() object before proceeding to + # avoid exhausting file descriptors when globbing deep trees. + with os.scandir(path) as scandir_it: + entries = list(scandir_it) + return ((entry, entry.name, entry.path) for entry in entries) + if os.name == 'nt': @staticmethod def add_slash(pathname): diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 820970fcd5889b..a6c1b36c20fe18 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -93,7 +93,11 @@ class PathGlobber(_GlobberBase): lexists = operator.methodcaller('exists', follow_symlinks=False) add_slash = operator.methodcaller('joinpath', '') - scandir = operator.methodcaller('_scandir') + + @staticmethod + def scandir(path): + """Like os.scandir(), but generates (entry, name, path) tuples.""" + return ((child._info, child.name, child) for child in path.iterdir()) @staticmethod def concat_path(path, text): @@ -419,6 +423,14 @@ class PathBase(PurePathBase): def _unsupported_msg(cls, attribute): return f"{cls.__name__}.{attribute} is unsupported" + @property + def _info(self): + """ + An os.DirEntry-like object, if this path was generated by iterdir(). + """ + # TODO: make this public + abstract, delete PathBase.stat(). + return self + def stat(self, *, follow_symlinks=True): """ Return the result of the stat() system call on this path, like @@ -620,15 +632,6 @@ def write_text(self, data, encoding=None, errors=None, newline=None): with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f: return f.write(data) - def _scandir(self): - """Yield os.DirEntry-like objects of the directory contents. - - The children are yielded in arbitrary order, and the - special entries '.' and '..' are not included. - """ - import contextlib - return contextlib.nullcontext(self.iterdir()) - def iterdir(self): """Yield path objects of the directory contents. @@ -685,18 +688,16 @@ def walk(self, top_down=True, on_error=None, follow_symlinks=False): if not top_down: paths.append((path, dirnames, filenames)) try: - with path._scandir() as entries: - for entry in entries: - name = entry.name - try: - if entry.is_dir(follow_symlinks=follow_symlinks): - if not top_down: - paths.append(path.joinpath(name)) - dirnames.append(name) - else: - filenames.append(name) - except OSError: - filenames.append(name) + for child in path.iterdir(): + try: + if child._info.is_dir(follow_symlinks=follow_symlinks): + if not top_down: + paths.append(child) + dirnames.append(child.name) + else: + filenames.append(child.name) + except OSError: + filenames.append(child.name) except OSError as error: if on_error is not None: on_error(error) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 250bc12956f5bc..79400985a8a725 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -524,7 +524,7 @@ class Path(PathBase, PurePath): object. You can also instantiate a PosixPath or WindowsPath directly, but cannot instantiate a WindowsPath on a POSIX system or vice versa. """ - __slots__ = () + __slots__ = ('_info',) as_uri = PurePath.as_uri @classmethod @@ -635,13 +635,11 @@ def _filter_trailing_slash(self, paths): path_str = path_str[:-1] yield path_str - def _scandir(self): - """Yield os.DirEntry-like objects of the directory contents. - - The children are yielded in arbitrary order, and the - special entries '.' and '..' are not included. - """ - return os.scandir(self) + def _from_dir_entry(self, dir_entry, path_str): + path = self.with_segments(path_str) + path._str = path_str + path._info = dir_entry + return path def iterdir(self): """Yield path objects of the directory contents. @@ -651,10 +649,11 @@ def iterdir(self): """ root_dir = str(self) with os.scandir(root_dir) as scandir_it: - paths = [entry.path for entry in scandir_it] + entries = list(scandir_it) if root_dir == '.': - paths = map(self._remove_leading_dot, paths) - return map(self._from_parsed_string, paths) + return (self._from_dir_entry(e, e.name) for e in entries) + else: + return (self._from_dir_entry(e, e.path) for e in entries) def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False): """Iterate over this subtree and yield all existing files (of any diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index bf9ae6cc8a2433..3038cbf8cac727 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -1958,21 +1958,20 @@ def test_iterdir_nodir(self): self.assertIn(cm.exception.errno, (errno.ENOTDIR, errno.ENOENT, errno.EINVAL)) - def test_scandir(self): + def test_iterdir_info(self): p = self.cls(self.base) - with p._scandir() as entries: - self.assertTrue(list(entries)) - with p._scandir() as entries: - for entry in entries: - child = p / entry.name - self.assertIsNotNone(entry) - self.assertEqual(entry.name, child.name) - self.assertEqual(entry.is_symlink(), - child.is_symlink()) - self.assertEqual(entry.is_dir(follow_symlinks=False), - child.is_dir(follow_symlinks=False)) - if entry.name != 'brokenLinkLoop': - self.assertEqual(entry.is_dir(), child.is_dir()) + for child in p.iterdir(): + entry = child._info + self.assertIsNotNone(entry) + self.assertEqual(entry.is_dir(follow_symlinks=False), + child.is_dir(follow_symlinks=False)) + self.assertEqual(entry.is_file(follow_symlinks=False), + child.is_file(follow_symlinks=False)) + self.assertEqual(entry.is_symlink(), + child.is_symlink()) + if child.name != 'brokenLinkLoop': + self.assertEqual(entry.is_dir(), child.is_dir()) + self.assertEqual(entry.is_file(), child.is_file()) def test_glob_common(self): def _check(glob, expected): From 76ef028fbbcc36c08d50533a2b3812d0025c1c31 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 19:52:42 +0000 Subject: [PATCH 02/29] Rename `_info` to `_status` --- Lib/pathlib/_abc.py | 6 +++--- Lib/pathlib/_local.py | 4 ++-- Lib/test/test_pathlib/test_pathlib_abc.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 0d3adde8cf90b5..61c17b470e8262 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -45,7 +45,7 @@ class PathGlobber(_GlobberBase): @staticmethod def scandir(path): """Like os.scandir(), but generates (entry, name, path) tuples.""" - return ((child._info, 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): @@ -372,7 +372,7 @@ def _unsupported_msg(cls, attribute): return f"{cls.__name__}.{attribute} is unsupported" @property - def _info(self): + def _status(self): """ An os.DirEntry-like object, if this path was generated by iterdir(). """ @@ -638,7 +638,7 @@ def walk(self, top_down=True, on_error=None, follow_symlinks=False): try: for child in path.iterdir(): try: - if child._info.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) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index b7218c4b987e82..08794c19be1cdc 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -524,7 +524,7 @@ class Path(PathBase, PurePath): object. You can also instantiate a PosixPath or WindowsPath directly, but cannot instantiate a WindowsPath on a POSIX system or vice versa. """ - __slots__ = ('_info',) + __slots__ = ('_status',) as_uri = PurePath.as_uri @classmethod @@ -638,7 +638,7 @@ def _filter_trailing_slash(self, paths): def _from_dir_entry(self, dir_entry, path_str): path = self.with_segments(path_str) path._str = path_str - path._info = dir_entry + path._status = dir_entry return path def iterdir(self): diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index bbc4489c74160c..d2a4b7b9011c0b 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -1898,10 +1898,10 @@ def test_iterdir_nodir(self): self.assertIn(cm.exception.errno, (errno.ENOTDIR, errno.ENOENT, errno.EINVAL)) - def test_iterdir_info(self): + def test_iterdir_status(self): p = self.cls(self.base) for child in p.iterdir(): - entry = child._info + entry = child._status self.assertIsNotNone(entry) self.assertEqual(entry.is_dir(follow_symlinks=False), child.is_dir(follow_symlinks=False)) From 5d927855d9b306cd9c28692c3d6e80dd8a2413ef Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 19:58:31 +0000 Subject: [PATCH 03/29] Add `Status` protocol. --- Lib/pathlib/_types.py | 10 ++++++++++ Lib/test/test_pathlib/test_pathlib_abc.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Lib/pathlib/_types.py b/Lib/pathlib/_types.py index 60df94d0b46049..d39da77a37fec4 100644 --- a/Lib/pathlib/_types.py +++ b/Lib/pathlib/_types.py @@ -20,3 +20,13 @@ def splitdrive(self, path: str) -> tuple[str, str]: ... def splitext(self, path: str) -> tuple[str, str]: ... def normcase(self, path: str) -> str: ... def isabs(self, path: str) -> bool: ... + + +@runtime_checkable +class Status(Protocol): + """Protocol for path statuses, which support querying the file type. + Methods may return cached results. + """ + def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... + def is_file(self, *, follow_symlinks: bool = True) -> bool: ... + def is_symlink(self) -> bool: ... diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index d2a4b7b9011c0b..b7734bfe1cca45 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -6,7 +6,7 @@ import unittest from pathlib._abc import UnsupportedOperation, PurePathBase, PathBase -from pathlib._types import Parser +from pathlib._types import Parser, Status import posixpath from test.support.os_helper import TESTFN @@ -1902,7 +1902,7 @@ def test_iterdir_status(self): p = self.cls(self.base) for child in p.iterdir(): entry = child._status - self.assertIsNotNone(entry) + self.assertIsInstance(entry, Status) self.assertEqual(entry.is_dir(follow_symlinks=False), child.is_dir(follow_symlinks=False)) self.assertEqual(entry.is_file(follow_symlinks=False), From dc403c6891a1aabcd1542437ab58269b10265df8 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 20:55:23 +0000 Subject: [PATCH 04/29] Make `Path.status` public. --- Doc/library/pathlib.rst | 72 ++++++++++++++++++++++ Doc/whatsnew/3.14.rst | 9 +++ Lib/pathlib/_abc.py | 11 ++-- Lib/pathlib/_local.py | 74 +++++++++++++++++++++++ Lib/pathlib/{_types.py => types.py} | 0 Lib/test/test_pathlib/test_pathlib_abc.py | 4 +- 6 files changed, 163 insertions(+), 7 deletions(-) rename Lib/pathlib/{_types.py => types.py} (100%) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 4b48880d6d9a18..a956f459acf603 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -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 ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -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. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index b71d31f9742fe0..0ad8d71384c66e 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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 --- diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 61c17b470e8262..44e431b46d31f6 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -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): @@ -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): @@ -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) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 08794c19be1cdc..5e4e0eb4cfcdb7 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -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: @@ -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. @@ -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 diff --git a/Lib/pathlib/_types.py b/Lib/pathlib/types.py similarity index 100% rename from Lib/pathlib/_types.py rename to Lib/pathlib/types.py diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index b7734bfe1cca45..a0deef46056c2d 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -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 @@ -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)) From 5a128b98f9ef517c9bfb4e4cfdd2d4b011e90478 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 21:00:50 +0000 Subject: [PATCH 05/29] Fix docs typos --- Doc/library/pathlib.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index a956f459acf603..7fd8f0fe5ddf52 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1186,13 +1186,13 @@ Querying file type and status cached results:: >>> p = Path('setup.py') - >>> p.info.is_file() + >>> p.status.is_file() True >>> p.unlink() - >>> p.info.is_file() # returns stale info + >>> p.status.is_file() # returns stale status True - >>> p = Path(p) # get fresh info - >>> p.info.is_file() + >>> p = Path(p) # get fresh status + >>> p.status.is_file() False The value is a :class:`os.DirEntry` instance if the path was generated by @@ -1200,7 +1200,7 @@ Querying file type and status 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. + accessing :attr:`Path.status` does not perform any filesystem queries. .. versionadded:: 3.14 From cac77a66d780796bd8c7175372691b9f150456fe Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 21:11:18 +0000 Subject: [PATCH 06/29] Docs fixes --- Doc/library/pathlib.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 7fd8f0fe5ddf52..041cc25057dcbc 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1179,11 +1179,10 @@ Querying file type and status .. 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:: + A :class:`~pathlib.types.Status` 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. + Care must be taken to avoid incorrectly using cached results:: >>> p = Path('setup.py') >>> p.status.is_file() @@ -1947,8 +1946,9 @@ The :mod:`pathlib.types` module provides types for static type checking. .. class:: Status() - A :class:`typing.Protocol` describing the :attr:`Path.status` attribute. - Implementations may return cached results from their methods. + A :class:`typing.Protocol` describing the + :attr:`Path.status ` attribute. Implementations may + return cached results from their methods. .. method:: is_dir(*, follow_symlinks=True) From 6e09adaf1611adfd243b1433fa14318ce915a95f Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 21:15:55 +0000 Subject: [PATCH 07/29] Fix whatsnew --- Doc/whatsnew/3.14.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 0ad8d71384c66e..f0985422be9e53 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -542,9 +542,10 @@ pathlib * 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. + :func:`~os.stat` results. Path objects generated by + :meth:`~pathlib.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`.) From 1d8713eb18ea7d2c8b1e930dfa5c9c5ec261da21 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 22:32:54 +0000 Subject: [PATCH 08/29] Fix _PathStatus repr, exception handling --- Lib/pathlib/_local.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 5e4e0eb4cfcdb7..872d57b626e12a 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -66,7 +66,7 @@ class _PathStatus: def __init__(self, path): self._path = str(path) - self._repr = f"<{type(path).__name__}.info>" + self._repr = f"<{type(path).__name__}.status>" def __repr__(self): return self._repr @@ -77,7 +77,7 @@ def _get_link_mode(self): except AttributeError: try: self._link_mode = os.lstat(self._path).st_mode - except (OSError, ValueError): + except FileNotFoundError: self._link_mode = 0 if not self.is_symlink(): # Not a symlink, so stat() will give the same result. @@ -90,7 +90,7 @@ def _get_file_mode(self): except AttributeError: try: self._file_mode = os.stat(self._path).st_mode - except (OSError, ValueError): + except FileNotFoundError: self._file_mode = 0 return self._file_mode From f8ffbbd8e10403113a9e9deae7b03fcabc16ab46 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 22:58:22 +0000 Subject: [PATCH 09/29] Docs improvements --- Doc/library/pathlib.rst | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 041cc25057dcbc..32d80f29e09d5b 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1182,17 +1182,17 @@ Querying file type and status A :class:`~pathlib.types.Status` 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. - Care must be taken to avoid incorrectly using cached results:: - - >>> p = Path('setup.py') - >>> p.status.is_file() - True - >>> p.unlink() - >>> p.status.is_file() # returns stale status - True - >>> p = Path(p) # get fresh status - >>> p.status.is_file() - False + For example:: + + >>> p = Path('src') + >>> if p.status.is_symlink(): + ... print('symlink') + ... elif p.status.is_dir(): + ... print('directory') + ... else: + ... print('other') + ... + directory The value is a :class:`os.DirEntry` instance if the path was generated by :meth:`Path.iterdir`. These objects are initialized with some information @@ -1201,6 +1201,11 @@ Querying file type and status initially knows nothing about the file status. In either case, merely accessing :attr:`Path.status` does not perform any filesystem queries. + To fetch up-to-date information, it's best to use :meth:`Path.is_dir`, + :meth:`~Path.is_file` and :meth:`~Path.is_symlink` rather than this + attribute. There is no method to reset the cache; instead you can create + a new path object with an empty status cache via ``p = Path(p)``. + .. versionadded:: 3.14 From 0a86e68b4e31734f30a3c8e396fc56d13a9120fe Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 9 Dec 2024 23:30:32 +0000 Subject: [PATCH 10/29] Move PathGlobber into glob.py, now that it uses the public path interface --- Lib/glob.py | 16 ++++++++++++++++ Lib/pathlib/_abc.py | 24 ++---------------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/Lib/glob.py b/Lib/glob.py index 850d1e9f1659f2..0abc17824808a6 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -541,3 +541,19 @@ def add_slash(pathname): if not pathname or pathname[-1] == '/': return pathname return f'{pathname}/' + + +class _PathGlobber(_GlobberBase): + """Provides shell-style pattern matching and globbing for pathlib paths. + """ + + lexists = operator.methodcaller('exists', follow_symlinks=False) + add_slash = operator.methodcaller('joinpath', '') + + @staticmethod + def scandir(path): + return ((child.status, child.name, child) for child in path.iterdir()) + + @staticmethod + def concat_path(path, text): + return path.with_segments(str(path) + text) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 44e431b46d31f6..e6f548359460b3 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -12,10 +12,9 @@ """ import functools -import operator import posixpath from errno import EINVAL -from glob import _GlobberBase, _no_recurse_symlinks +from glob import _PathGlobber, _no_recurse_symlinks from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO from pathlib._os import copyfileobj @@ -34,25 +33,6 @@ def _is_case_sensitive(parser): return parser.normcase('Aa') == 'Aa' -class PathGlobber(_GlobberBase): - """ - Class providing shell-style globbing for path objects. - """ - - lexists = operator.methodcaller('exists', follow_symlinks=False) - add_slash = operator.methodcaller('joinpath', '') - - @staticmethod - def scandir(path): - """Like os.scandir(), but generates (entry, name, path) tuples.""" - return ((child.status, child.name, child) for child in path.iterdir()) - - @staticmethod - def concat_path(path, text): - """Appends text to the given path.""" - return path.with_segments(str(path) + text) - - class PurePathBase: """Base class for pure path objects. @@ -68,7 +48,7 @@ class PurePathBase: '_raw_paths', ) parser = posixpath - _globber = PathGlobber + _globber = _PathGlobber def __init__(self, *args): for arg in args: From b0b621df0263267cf4c68cfcc63834a5ca9c4fe2 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 10 Dec 2024 03:10:34 +0000 Subject: [PATCH 11/29] Simplify _PathStatus implementation a little --- Lib/pathlib/_local.py | 52 +++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 872d57b626e12a..32b6e9b3eabfe2 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -62,62 +62,50 @@ def __repr__(self): 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') + __slots__ = ('_path', '_repr', '_mode') def __init__(self, path): self._path = str(path) self._repr = f"<{type(path).__name__}.status>" + self._mode = [None, None] 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 FileNotFoundError: - 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: + def _get_mode(self, *, follow_symlinks=True): + idx = int(follow_symlinks) + mode = self._mode[idx] + if mode is None: try: - self._file_mode = os.stat(self._path).st_mode - except FileNotFoundError: - self._file_mode = 0 - return self._file_mode + st = os.stat(self._path, follow_symlinks=follow_symlinks) + except (FileNotFoundError, ValueError): + mode = 0 + else: + mode = st.st_mode + if follow_symlinks or S_ISLNK(mode): + self._mode[idx] = mode + else: + # Not a symlink, so stat() will give the same result + self._mode = [mode, mode] + return 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()) + return S_ISDIR(self._get_mode(follow_symlinks=follow_symlinks)) 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()) + return S_ISREG(self._get_mode(follow_symlinks=follow_symlinks)) def is_symlink(self): """ Whether this path is a symbolic link. """ - return S_ISLNK(self._get_link_mode()) + return S_ISLNK(self._get_mode(follow_symlinks=False)) class PurePath(PurePathBase): From ef650fd4774ddc725c9bbc37f01926dac5878d76 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 10 Dec 2024 03:11:00 +0000 Subject: [PATCH 12/29] Add some tests --- Lib/test/test_pathlib/test_pathlib.py | 45 +++++++++++++++++++ Lib/test/test_pathlib/test_pathlib_abc.py | 54 +++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index ce0f4748c860b1..346f8b2f17a65b 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -1769,6 +1769,51 @@ def test_symlink_to_unsupported(self): with self.assertRaises(pathlib.UnsupportedOperation): q.symlink_to(p) + def test_status_is_dir_caching(self): + p = self.cls(self.base) + q = p / 'mydir' + self.assertFalse(q.status.is_dir()) + self.assertFalse(q.status.is_dir(follow_symlinks=False)) + q.mkdir() + self.assertFalse(q.status.is_dir()) + self.assertFalse(q.status.is_dir(follow_symlinks=False)) + + q = p / 'mydir' # same path, new instance. + self.assertTrue(q.status.is_dir()) + self.assertTrue(q.status.is_dir(follow_symlinks=False)) + q.rmdir() + self.assertTrue(q.status.is_dir()) + self.assertTrue(q.status.is_dir(follow_symlinks=False)) + + def test_status_is_file_caching(self): + p = self.cls(self.base) + q = p / 'myfile' + self.assertFalse(q.status.is_file()) + self.assertFalse(q.status.is_file(follow_symlinks=False)) + q.write_text('hullo') + self.assertFalse(q.status.is_file()) + self.assertFalse(q.status.is_file(follow_symlinks=False)) + + q = p / 'myfile' # same path, new instance. + self.assertTrue(q.status.is_file()) + self.assertTrue(q.status.is_file(follow_symlinks=False)) + q.unlink() + self.assertTrue(q.status.is_file()) + self.assertTrue(q.status.is_file(follow_symlinks=False)) + + @needs_symlinks + def test_status_is_symlink_caching(self): + p = self.cls(self.base) + q = p / 'mylink' + self.assertFalse(q.status.is_symlink()) + q.symlink_to('blah') + self.assertFalse(q.status.is_symlink()) + + q = p / 'mylink' # same path, new instance. + self.assertTrue(q.status.is_symlink()) + q.unlink() + self.assertTrue(q.status.is_symlink()) + @needs_symlinks def test_stat_no_follow_symlinks(self): p = self.cls(self.base) / 'linkA' diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index a0deef46056c2d..57b0fb4a48d55a 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -2037,6 +2037,60 @@ def test_rglob_windows(self): self.assertEqual(set(p.rglob("FILEd")), { P(self.base, "dirC/dirD/fileD") }) self.assertEqual(set(p.rglob("*\\")), { P(self.base, "dirC/dirD/") }) + def test_status_is_dir(self): + p = self.cls(self.base) + self.assertTrue((p / 'dirA').status.is_dir()) + self.assertTrue((p / 'dirA').status.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'fileA').status.is_dir()) + self.assertFalse((p / 'fileA').status.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'non-existing').status.is_dir()) + self.assertFalse((p / 'non-existing').status.is_dir(follow_symlinks=False)) + if self.can_symlink: + self.assertFalse((p / 'linkA').status.is_dir()) + self.assertFalse((p / 'linkA').status.is_dir(follow_symlinks=False)) + self.assertTrue((p / 'linkB').status.is_dir()) + self.assertFalse((p / 'linkB').status.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'brokenLink').status.is_dir()) + self.assertFalse((p / 'brokenLink').status.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'dirA\udfff').status.is_dir()) + self.assertFalse((p / 'dirA\udfff').status.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'dirA\x00').status.is_dir()) + self.assertFalse((p / 'dirA\x00').status.is_dir(follow_symlinks=False)) + + def test_status_is_file(self): + p = self.cls(self.base) + self.assertTrue((p / 'fileA').status.is_file()) + self.assertTrue((p / 'fileA').status.is_file(follow_symlinks=False)) + self.assertFalse((p / 'dirA').status.is_file()) + self.assertFalse((p / 'dirA').status.is_file(follow_symlinks=False)) + self.assertFalse((p / 'non-existing').status.is_file()) + self.assertFalse((p / 'non-existing').status.is_file(follow_symlinks=False)) + if self.can_symlink: + self.assertTrue((p / 'linkA').status.is_file()) + self.assertFalse((p / 'linkA').status.is_file(follow_symlinks=False)) + self.assertFalse((p / 'linkB').status.is_file()) + self.assertFalse((p / 'linkB').status.is_file(follow_symlinks=False)) + self.assertFalse((p / 'brokenLink').status.is_file()) + self.assertFalse((p / 'brokenLink').status.is_file(follow_symlinks=False)) + self.assertFalse((p / 'fileA\udfff').status.is_file()) + self.assertFalse((p / 'fileA\udfff').status.is_file(follow_symlinks=False)) + self.assertFalse((p / 'fileA\x00').status.is_file()) + self.assertFalse((p / 'fileA\x00').status.is_file(follow_symlinks=False)) + + def test_status_is_symlink(self): + p = self.cls(self.base) + self.assertFalse((p / 'fileA').status.is_symlink()) + self.assertFalse((p / 'dirA').status.is_symlink()) + self.assertFalse((p / 'non-existing').status.is_symlink()) + if self.can_symlink: + self.assertTrue((p / 'linkA').status.is_symlink()) + self.assertTrue((p / 'linkB').status.is_symlink()) + self.assertTrue((p / 'brokenLink').status.is_symlink()) + self.assertFalse((p / 'linkA\udfff').status.is_symlink()) + self.assertFalse((p / 'linkA\x00').status.is_symlink()) + self.assertFalse((p / 'fileA\udfff').status.is_symlink()) + self.assertFalse((p / 'fileA\x00').status.is_symlink()) + def test_stat(self): statA = self.cls(self.base).joinpath('fileA').stat() statB = self.cls(self.base).joinpath('dirB', 'fileB').stat() From 7b990c605d0bf72a39646a78adcc17063c626ffd Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 10 Dec 2024 19:39:39 +0000 Subject: [PATCH 13/29] Add news --- .../Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst diff --git a/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst b/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst new file mode 100644 index 00000000000000..264a0aaba25bdd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst @@ -0,0 +1,7 @@ +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:`~pathlib.Path.iterdir` store :class:`os.DirEntry` objects, which are +initialized with file type information gleaned from scanning the parent +directory. From cf1073ceed6feed2a24c0ba15f0504957bb6d6c8 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 10 Dec 2024 19:40:34 +0000 Subject: [PATCH 14/29] Docs tweaks --- Doc/library/pathlib.rst | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 32d80f29e09d5b..77a808577e7561 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1194,16 +1194,17 @@ Querying file type and status ... directory - 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.status` does not perform any filesystem queries. - - To fetch up-to-date information, it's best to use :meth:`Path.is_dir`, - :meth:`~Path.is_file` and :meth:`~Path.is_symlink` rather than this - attribute. There is no method to reset the cache; instead you can create + If the path was generated from :meth:`Path.iterdir` then this attribute is + an :class:`os.DirEntry` instance. These objects are initialized with some + information about the file type; see the :func:`os.scandir` docs for + details. 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.status` does not perform any + filesystem queries. + + To fetch up-to-date information, it's best to call :meth:`Path.is_dir`, + :meth:`~Path.is_file` and :meth:`~Path.is_symlink` rather than methods of + this attribute. There is no way to reset the cache; instead you can create a new path object with an empty status cache via ``p = Path(p)``. .. versionadded:: 3.14 @@ -1959,24 +1960,24 @@ The :mod:`pathlib.types` module provides types for static type checking. 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. + any other kind of file, or if it doesn’t exist. 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. + status is any other kind of file or if it doesn’t exist. .. 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. + other non-file, or if it doesn’t exist. 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. + is a directory or other other non-file, or if it doesn’t exist. .. 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. + file, or if it doesn’t exist. From 764b8aeb2a62970c0182be65adafe33210185b11 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 11 Dec 2024 22:57:23 +0000 Subject: [PATCH 15/29] Wrap `os.DirEntry` in `_DirEntryStatus` --- Doc/library/pathlib.rst | 7 ++--- Doc/whatsnew/3.14.rst | 5 ++- Lib/pathlib/_local.py | 31 ++++++++++++++++++- ...-12-10-19-39-35.gh-issue-125413.wOb4yr.rst | 5 ++- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 77a808577e7561..4aaeef3a7b38a3 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1195,11 +1195,8 @@ Querying file type and status directory If the path was generated from :meth:`Path.iterdir` then this attribute is - an :class:`os.DirEntry` instance. These objects are initialized with some - information about the file type; see the :func:`os.scandir` docs for - details. 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.status` does not perform any + initialized with information about the file type gleaned from scanning the + parent. Merely accessing :attr:`Path.status` does not perform any filesystem queries. To fetch up-to-date information, it's best to call :meth:`Path.is_dir`, diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index f0985422be9e53..d6c58a2f4d1dc2 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -543,9 +543,8 @@ pathlib 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:`~pathlib.Path.iterdir` store :class:`os.DirEntry` objects, which are - initialized with file type information gleaned from scanning the parent - directory. + :meth:`~pathlib.Path.iterdir` are initialized with file type information + gleaned from scanning the parent directory. (Contributed by Barney Gale in :gh:`125413`.) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 5facf6274647d8..12a5ac4a258cc7 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -108,6 +108,35 @@ def is_symlink(self): return S_ISLNK(self._get_mode(follow_symlinks=False)) +class _DirEntryStatus: + __slots__ = ('_entry', '_repr') + + def __init__(self, path, entry): + self._entry = entry + self._repr = f"<{type(path).__name__}.status>" + + def __repr__(self): + return self._repr + + def is_dir(self, *, follow_symlinks=True): + """ + Whether this path is a directory. + """ + return self._entry.is_dir(follow_symlinks=follow_symlinks) + + def is_file(self, *, follow_symlinks=True): + """ + Whether this path is a regular file. + """ + return self._entry.is_file(follow_symlinks=follow_symlinks) + + def is_symlink(self): + """ + Whether this path is a symbolic link. + """ + return self._entry.is_symlink() + + class PurePath(PurePathBase): """Base class for manipulating paths without I/O. @@ -762,7 +791,7 @@ def _filter_trailing_slash(self, paths): def _from_dir_entry(self, dir_entry, path_str): path = self.with_segments(path_str) path._str = path_str - path._status = dir_entry + path._status = _DirEntryStatus(path, dir_entry) return path def iterdir(self): diff --git a/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst b/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst index 264a0aaba25bdd..c54a8b3a70248b 100644 --- a/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst +++ b/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst @@ -2,6 +2,5 @@ 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:`~pathlib.Path.iterdir` store :class:`os.DirEntry` objects, which are -initialized with file type information gleaned from scanning the parent -directory. +:meth:`~pathlib.Path.iterdir` are initialized with file type information +gleaned from scanning the parent directory. From fa8931b816bcd34412f9804edb97a319459e79c1 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 11 Dec 2024 23:20:10 +0000 Subject: [PATCH 16/29] Add `Status.exists()` --- Doc/library/pathlib.rst | 8 ++++++ Lib/pathlib/_local.py | 17 ++++++++++++ Lib/pathlib/types.py | 1 + Lib/test/test_pathlib/test_pathlib.py | 16 +++++++++++ Lib/test/test_pathlib/test_pathlib_abc.py | 33 ++++++++++++++++++++++- 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 4aaeef3a7b38a3..e719567cc3e72e 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1953,6 +1953,14 @@ The :mod:`pathlib.types` module provides types for static type checking. :attr:`Path.status ` attribute. Implementations may return cached results from their methods. + .. method:: exists(*, follow_symlinks=True) + + Return ``True`` if this path status is for an existing file or + directory, or any other kind of file. + + If *follow_symlinks* is ``False``, return ``True`` for symlinks without + checking if their targets exist. + .. method:: is_dir(*, follow_symlinks=True) Return ``True`` if this status is a directory or a symbolic link diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 12a5ac4a258cc7..ed8ea787581189 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -89,6 +89,12 @@ def _get_mode(self, *, follow_symlinks=True): self._mode = [mode, mode] return mode + def exists(self, *, follow_symlinks=True): + """ + Whether this path exists + """ + return self._get_mode(follow_symlinks=follow_symlinks) > 0 + def is_dir(self, *, follow_symlinks=True): """ Whether this path is a directory. @@ -118,6 +124,17 @@ def __init__(self, path, entry): def __repr__(self): return self._repr + def exists(self, *, follow_symlinks=True): + """ + Whether this path exists + """ + if follow_symlinks: + try: + self._entry.stat() + except FileNotFoundError: + return False + return True + def is_dir(self, *, follow_symlinks=True): """ Whether this path is a directory. diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index d39da77a37fec4..f710e1d8341325 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -27,6 +27,7 @@ class Status(Protocol): """Protocol for path statuses, which support querying the file type. Methods may return cached results. """ + def exists(self, *, follow_symlinks: bool = True) -> bool: ... def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... def is_file(self, *, follow_symlinks: bool = True) -> bool: ... def is_symlink(self) -> bool: ... diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 701d2b9455377b..ad5fc5d6e48e57 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -1769,6 +1769,22 @@ def test_symlink_to_unsupported(self): with self.assertRaises(pathlib.UnsupportedOperation): q.symlink_to(p) + def test_status_exists_caching(self): + p = self.cls(self.base) + q = p / 'myfile' + self.assertFalse(q.status.exists()) + self.assertFalse(q.status.exists(follow_symlinks=False)) + q.write_text('hullo') + self.assertFalse(q.status.exists()) + self.assertFalse(q.status.exists(follow_symlinks=False)) + + q = p / 'myfile' # same path, new instance. + self.assertTrue(q.status.exists()) + self.assertTrue(q.status.exists(follow_symlinks=False)) + q.unlink() + self.assertTrue(q.status.exists()) + self.assertTrue(q.status.exists(follow_symlinks=False)) + def test_status_is_dir_caching(self): p = self.cls(self.base) q = p / 'mydir' diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 2dd2df5ea9216e..232a98a87668f9 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -1876,13 +1876,23 @@ def test_iterdir_status(self): for child in p.iterdir(): entry = child.status self.assertIsInstance(entry, Status) + self.assertTrue(entry.exists(follow_symlinks=False)) self.assertEqual(entry.is_dir(follow_symlinks=False), child.is_dir(follow_symlinks=False)) self.assertEqual(entry.is_file(follow_symlinks=False), child.is_file(follow_symlinks=False)) self.assertEqual(entry.is_symlink(), child.is_symlink()) - if child.name != 'brokenLinkLoop': + if child.name == 'brokenLink': + self.assertFalse(entry.exists()) + self.assertFalse(entry.is_dir()) + self.assertFalse(entry.is_file()) + elif child.name == 'brokenLinkLoop': + self.assertRaises(OSError, entry.exists) + self.assertRaises(OSError, entry.is_dir) + self.assertRaises(OSError, entry.is_file) + else: + self.assertTrue(entry.exists()) self.assertEqual(entry.is_dir(), child.is_dir()) self.assertEqual(entry.is_file(), child.is_file()) @@ -2010,6 +2020,27 @@ def test_rglob_windows(self): self.assertEqual(set(p.rglob("FILEd")), { P(self.base, "dirC/dirD/fileD") }) self.assertEqual(set(p.rglob("*\\")), { P(self.base, "dirC/dirD/") }) + def test_exists(self): + p = self.cls(self.base) + self.assertTrue(p.status.exists()) + self.assertTrue((p / 'dirA').status.exists()) + self.assertTrue((p / 'dirA').status.exists(follow_symlinks=False)) + self.assertTrue((p / 'fileA').status.exists()) + self.assertTrue((p / 'fileA').status.exists(follow_symlinks=False)) + self.assertFalse((p / 'non-existing').status.exists()) + self.assertFalse((p / 'non-existing').status.exists(follow_symlinks=False)) + if self.can_symlink: + self.assertTrue((p / 'linkA').status.exists()) + self.assertTrue((p / 'linkA').status.exists(follow_symlinks=False)) + self.assertTrue((p / 'linkB').status.exists()) + self.assertTrue((p / 'linkB').status.exists(follow_symlinks=True)) + self.assertFalse((p / 'brokenLink').status.exists()) + self.assertTrue((p / 'brokenLink').status.exists(follow_symlinks=False)) + self.assertFalse((p / 'fileA\udfff').status.exists()) + self.assertFalse((p / 'fileA\udfff').status.exists(follow_symlinks=False)) + self.assertFalse((p / 'fileA\x00').status.exists()) + self.assertFalse((p / 'fileA\x00').status.exists(follow_symlinks=False)) + def test_status_is_dir(self): p = self.cls(self.base) self.assertTrue((p / 'dirA').status.is_dir()) From 2bb622141bac7ba8220e4a5bc91a92377178bcb6 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 11 Dec 2024 23:22:48 +0000 Subject: [PATCH 17/29] Fix test name --- Lib/test/test_pathlib/test_pathlib_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 232a98a87668f9..1ce8580b1ffbf2 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -2020,7 +2020,7 @@ def test_rglob_windows(self): self.assertEqual(set(p.rglob("FILEd")), { P(self.base, "dirC/dirD/fileD") }) self.assertEqual(set(p.rglob("*\\")), { P(self.base, "dirC/dirD/") }) - def test_exists(self): + def test_status_exists(self): p = self.cls(self.base) self.assertTrue(p.status.exists()) self.assertTrue((p / 'dirA').status.exists()) From 923542b0606efd1f240bebf77cae371e4989b8b7 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 11 Dec 2024 23:44:32 +0000 Subject: [PATCH 18/29] Use status.exists() in docs example --- Doc/library/pathlib.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index e719567cc3e72e..f29ef25d9820ff 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1189,8 +1189,10 @@ Querying file type and status ... print('symlink') ... elif p.status.is_dir(): ... print('directory') + ... elif p.status.exists(): + ... print('something else') ... else: - ... print('other') + ... print('not found') ... directory From 68377c439d7523965306be754f99f824998459fc Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 12 Dec 2024 00:36:52 +0000 Subject: [PATCH 19/29] Docs editing --- Doc/library/pathlib.rst | 38 +++++++++++++++++++------------------- Lib/pathlib/_local.py | 7 +++++-- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index f29ef25d9820ff..fbb17721421333 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1197,9 +1197,9 @@ Querying file type and status directory If the path was generated from :meth:`Path.iterdir` then this attribute is - initialized with information about the file type gleaned from scanning the - parent. Merely accessing :attr:`Path.status` does not perform any - filesystem queries. + initialized with some information about the file type gleaned from scanning + the parent directory. Merely accessing :attr:`Path.status` does not perform + any filesystem queries. To fetch up-to-date information, it's best to call :meth:`Path.is_dir`, :meth:`~Path.is_file` and :meth:`~Path.is_symlink` rather than methods of @@ -1957,34 +1957,34 @@ The :mod:`pathlib.types` module provides types for static type checking. .. method:: exists(*, follow_symlinks=True) - Return ``True`` if this path status is for an existing file or - directory, or any other kind of file. + 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 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. + 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 this status + If *follow_symlinks* is ``False``, return ``True`` only if the path is a directory (without following symlinks); return ``False`` if the - status is any other kind of file or if it doesn’t exist. + path is any other kind of file, or if it doesn't exist. .. 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. + 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 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. + 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 other non-file, or if it doesn't exist. .. 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. + 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/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index ed8ea787581189..74f61e894f6a3b 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -60,8 +60,8 @@ def __repr__(self): class _PathStatus: - """This object provides os.DirEntry-like access to the file type and file - attributes. Don't try to construct it yourself.""" + """Implementation of pathlib.types.Status that provides file status + information. Don't try to construct it yourself.""" __slots__ = ('_path', '_repr', '_mode') def __init__(self, path): @@ -115,6 +115,9 @@ def is_symlink(self): class _DirEntryStatus: + """Implementation of pathlib.types.Status that provides file status + information by querying a wrapped os.DirEntry object. Don't try to + construct it yourself.""" __slots__ = ('_entry', '_repr') def __init__(self, path, entry): From 4f3f4345e2f8f618028062927c42674d5a52db3d Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 12 Dec 2024 00:39:30 +0000 Subject: [PATCH 20/29] Few more test cases --- Lib/test/test_pathlib/test_pathlib_abc.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 1ce8580b1ffbf2..8fe24c97ff3d92 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -2036,6 +2036,8 @@ def test_status_exists(self): self.assertTrue((p / 'linkB').status.exists(follow_symlinks=True)) self.assertFalse((p / 'brokenLink').status.exists()) self.assertTrue((p / 'brokenLink').status.exists(follow_symlinks=False)) + self.assertRaises(OSError, (p / 'brokenLinkLoop').status.exists) + self.assertTrue((p / 'brokenLinkLoop').status.exists(follow_symlinks=False)) self.assertFalse((p / 'fileA\udfff').status.exists()) self.assertFalse((p / 'fileA\udfff').status.exists(follow_symlinks=False)) self.assertFalse((p / 'fileA\x00').status.exists()) @@ -2056,6 +2058,8 @@ def test_status_is_dir(self): self.assertFalse((p / 'linkB').status.is_dir(follow_symlinks=False)) self.assertFalse((p / 'brokenLink').status.is_dir()) self.assertFalse((p / 'brokenLink').status.is_dir(follow_symlinks=False)) + self.assertRaises(OSError, (p / 'brokenLinkLoop').status.is_dir) + self.assertFalse((p / 'brokenLinkLoop').status.is_dir(follow_symlinks=False)) self.assertFalse((p / 'dirA\udfff').status.is_dir()) self.assertFalse((p / 'dirA\udfff').status.is_dir(follow_symlinks=False)) self.assertFalse((p / 'dirA\x00').status.is_dir()) @@ -2076,6 +2080,8 @@ def test_status_is_file(self): self.assertFalse((p / 'linkB').status.is_file(follow_symlinks=False)) self.assertFalse((p / 'brokenLink').status.is_file()) self.assertFalse((p / 'brokenLink').status.is_file(follow_symlinks=False)) + self.assertRaises(OSError, (p / 'brokenLinkLoop').status.is_file) + self.assertFalse((p / 'brokenLinkLoop').status.is_file(follow_symlinks=False)) self.assertFalse((p / 'fileA\udfff').status.is_file()) self.assertFalse((p / 'fileA\udfff').status.is_file(follow_symlinks=False)) self.assertFalse((p / 'fileA\x00').status.is_file()) @@ -2092,6 +2098,7 @@ def test_status_is_symlink(self): self.assertTrue((p / 'brokenLink').status.is_symlink()) self.assertFalse((p / 'linkA\udfff').status.is_symlink()) self.assertFalse((p / 'linkA\x00').status.is_symlink()) + self.assertTrue((p / 'brokenLinkLoop').status.is_symlink()) self.assertFalse((p / 'fileA\udfff').status.is_symlink()) self.assertFalse((p / 'fileA\x00').status.is_symlink()) From 530771da8f2008fdbbe4d6882fb5067b759497e1 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 17 Dec 2024 13:07:46 +0000 Subject: [PATCH 21/29] Suppress OSErrors --- Lib/pathlib/_local.py | 19 +++++++++++++----- Lib/test/test_pathlib/test_pathlib_abc.py | 24 +++++++---------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 56e45eb20e05eb..fd39f851403f36 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -85,7 +85,7 @@ def _get_mode(self, *, follow_symlinks=True): if mode is None: try: st = os.stat(self._path, follow_symlinks=follow_symlinks) - except (FileNotFoundError, ValueError): + except (OSError, ValueError): mode = 0 else: mode = st.st_mode @@ -141,7 +141,7 @@ def exists(self, *, follow_symlinks=True): if follow_symlinks: try: self._entry.stat() - except FileNotFoundError: + except OSError: return False return True @@ -149,19 +149,28 @@ def is_dir(self, *, follow_symlinks=True): """ Whether this path is a directory. """ - return self._entry.is_dir(follow_symlinks=follow_symlinks) + try: + return self._entry.is_dir(follow_symlinks=follow_symlinks) + except OSError: + return False def is_file(self, *, follow_symlinks=True): """ Whether this path is a regular file. """ - return self._entry.is_file(follow_symlinks=follow_symlinks) + try: + return self._entry.is_file(follow_symlinks=follow_symlinks) + except OSError: + return False def is_symlink(self): """ Whether this path is a symbolic link. """ - return self._entry.is_symlink() + try: + return self._entry.is_symlink() + except OSError: + return False class PurePath(PurePathBase): diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 444187aae183eb..abdec441f574f5 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -1858,25 +1858,15 @@ def test_iterdir_status(self): for child in p.iterdir(): entry = child.status self.assertIsInstance(entry, Status) + self.assertEqual(entry.exists(), child.exists()) + self.assertEqual(entry.is_dir(), child.is_dir()) + self.assertEqual(entry.is_file(), child.is_file()) + self.assertEqual(entry.is_symlink(), child.is_symlink()) self.assertTrue(entry.exists(follow_symlinks=False)) self.assertEqual(entry.is_dir(follow_symlinks=False), child.is_dir(follow_symlinks=False)) self.assertEqual(entry.is_file(follow_symlinks=False), child.is_file(follow_symlinks=False)) - self.assertEqual(entry.is_symlink(), - child.is_symlink()) - if child.name == 'brokenLink': - self.assertFalse(entry.exists()) - self.assertFalse(entry.is_dir()) - self.assertFalse(entry.is_file()) - elif child.name == 'brokenLinkLoop': - self.assertRaises(OSError, entry.exists) - self.assertRaises(OSError, entry.is_dir) - self.assertRaises(OSError, entry.is_file) - else: - self.assertTrue(entry.exists()) - self.assertEqual(entry.is_dir(), child.is_dir()) - self.assertEqual(entry.is_file(), child.is_file()) def test_glob_common(self): def _check(glob, expected): @@ -2018,7 +2008,7 @@ def test_status_exists(self): self.assertTrue((p / 'linkB').status.exists(follow_symlinks=True)) self.assertFalse((p / 'brokenLink').status.exists()) self.assertTrue((p / 'brokenLink').status.exists(follow_symlinks=False)) - self.assertRaises(OSError, (p / 'brokenLinkLoop').status.exists) + self.assertFalse((p / 'brokenLinkLoop').status.exists()) self.assertTrue((p / 'brokenLinkLoop').status.exists(follow_symlinks=False)) self.assertFalse((p / 'fileA\udfff').status.exists()) self.assertFalse((p / 'fileA\udfff').status.exists(follow_symlinks=False)) @@ -2040,7 +2030,7 @@ def test_status_is_dir(self): self.assertFalse((p / 'linkB').status.is_dir(follow_symlinks=False)) self.assertFalse((p / 'brokenLink').status.is_dir()) self.assertFalse((p / 'brokenLink').status.is_dir(follow_symlinks=False)) - self.assertRaises(OSError, (p / 'brokenLinkLoop').status.is_dir) + self.assertFalse((p / 'brokenLinkLoop').status.is_dir()) self.assertFalse((p / 'brokenLinkLoop').status.is_dir(follow_symlinks=False)) self.assertFalse((p / 'dirA\udfff').status.is_dir()) self.assertFalse((p / 'dirA\udfff').status.is_dir(follow_symlinks=False)) @@ -2062,7 +2052,7 @@ def test_status_is_file(self): self.assertFalse((p / 'linkB').status.is_file(follow_symlinks=False)) self.assertFalse((p / 'brokenLink').status.is_file()) self.assertFalse((p / 'brokenLink').status.is_file(follow_symlinks=False)) - self.assertRaises(OSError, (p / 'brokenLinkLoop').status.is_file) + self.assertFalse((p / 'brokenLinkLoop').status.is_file()) self.assertFalse((p / 'brokenLinkLoop').status.is_file(follow_symlinks=False)) self.assertFalse((p / 'fileA\udfff').status.is_file()) self.assertFalse((p / 'fileA\udfff').status.is_file(follow_symlinks=False)) From 89ff6d4b7563d4e9b9c8dbc30da0e402a6dbc06a Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 17 Dec 2024 13:34:52 +0000 Subject: [PATCH 22/29] Add Windows implementation using os.path.isdir() etc --- Lib/pathlib/_local.py | 84 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index fd39f851403f36..deea4d4e4c8e28 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -66,25 +66,73 @@ def __repr__(self): return "<{}.parents>".format(type(self._path).__name__) -class _PathStatus: - """Implementation of pathlib.types.Status that provides file status - information. Don't try to construct it yourself.""" - __slots__ = ('_path', '_repr', '_mode') +class _PathStatusBase: + __slots__ = ('_path', '_repr') def __init__(self, path): self._path = str(path) self._repr = f"<{type(path).__name__}.status>" - self._mode = [None, None] + + def __fspath__(self): + return self._path def __repr__(self): return self._repr + +class _WindowsPathStatus(_PathStatusBase): + __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink') + + def exists(self, *, follow_symlinks=True): + if not follow_symlinks and self.is_symlink(): + return True + try: + return self._exists + except AttributeError: + self._exists = os.path.exists(self) + return self._exists + + def is_dir(self, *, follow_symlinks=True): + if not follow_symlinks and self.is_symlink(): + return False + try: + return self._is_dir + except AttributeError: + self._is_dir = os.path.isdir(self) + return self._is_dir + + def is_file(self, *, follow_symlinks=True): + if not follow_symlinks and self.is_symlink(): + return False + try: + return self._is_file + except AttributeError: + self._is_file = os.path.isfile(self) + return self._is_file + + def is_symlink(self): + try: + return self._is_symlink + except AttributeError: + self._is_symlink = os.path.islink(self) + return self._is_symlink + + +class _PosixPathStatus(_PathStatusBase): + """Implementation of pathlib.types.Status that provides file status + information. Don't try to construct it yourself.""" + __slots__ = ('_mode',) + + def __init__(self, path): + super().__init__(path) + self._mode = [None, None] + def _get_mode(self, *, follow_symlinks=True): idx = int(follow_symlinks) mode = self._mode[idx] if mode is None: try: - st = os.stat(self._path, follow_symlinks=follow_symlinks) + st = os.stat(self, follow_symlinks=follow_symlinks) except (OSError, ValueError): mode = 0 else: @@ -121,29 +169,35 @@ def is_symlink(self): return S_ISLNK(self._get_mode(follow_symlinks=False)) -class _DirEntryStatus: +_PathStatus = _WindowsPathStatus if os.name == 'nt' else _PosixPathStatus + + +class _DirEntryStatus(_PathStatusBase): """Implementation of pathlib.types.Status that provides file status information by querying a wrapped os.DirEntry object. Don't try to construct it yourself.""" - __slots__ = ('_entry', '_repr') + __slots__ = ('_entry', '_exists') def __init__(self, path, entry): + super().__init__(path) self._entry = entry - self._repr = f"<{type(path).__name__}.status>" - - def __repr__(self): - return self._repr def exists(self, *, follow_symlinks=True): """ Whether this path exists """ - if follow_symlinks: + if not follow_symlinks: + return True + try: + return self._exists + except AttributeError: try: self._entry.stat() except OSError: - return False - return True + self._exists = False + else: + self._exists = True + return self._exists def is_dir(self, *, follow_symlinks=True): """ From 592603bc4421605fed8f455b4c58881dbc5c47d3 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 17 Dec 2024 14:20:36 +0000 Subject: [PATCH 23/29] Docstrings --- Lib/pathlib/_local.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index deea4d4e4c8e28..8bf0dd7bc21a9e 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -81,9 +81,12 @@ def __repr__(self): class _WindowsPathStatus(_PathStatusBase): + """Implementation of pathlib.types.Status that provides file status + information for Windows paths. Don't try to construct it yourself.""" __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink') def exists(self, *, follow_symlinks=True): + """Whether this path exists.""" if not follow_symlinks and self.is_symlink(): return True try: @@ -93,6 +96,7 @@ def exists(self, *, follow_symlinks=True): return self._exists def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" if not follow_symlinks and self.is_symlink(): return False try: @@ -102,6 +106,7 @@ def is_dir(self, *, follow_symlinks=True): return self._is_dir def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" if not follow_symlinks and self.is_symlink(): return False try: @@ -111,6 +116,7 @@ def is_file(self, *, follow_symlinks=True): return self._is_file def is_symlink(self): + """Whether this path is a symbolic link.""" try: return self._is_symlink except AttributeError: @@ -120,7 +126,7 @@ def is_symlink(self): class _PosixPathStatus(_PathStatusBase): """Implementation of pathlib.types.Status that provides file status - information. Don't try to construct it yourself.""" + information for POSIX paths. Don't try to construct it yourself.""" __slots__ = ('_mode',) def __init__(self, path): @@ -145,27 +151,19 @@ def _get_mode(self, *, follow_symlinks=True): return mode def exists(self, *, follow_symlinks=True): - """ - Whether this path exists - """ + """Whether this path exists.""" return self._get_mode(follow_symlinks=follow_symlinks) > 0 def is_dir(self, *, follow_symlinks=True): - """ - Whether this path is a directory. - """ + """Whether this path is a directory.""" return S_ISDIR(self._get_mode(follow_symlinks=follow_symlinks)) def is_file(self, *, follow_symlinks=True): - """ - Whether this path is a regular file. - """ + """Whether this path is a regular file.""" return S_ISREG(self._get_mode(follow_symlinks=follow_symlinks)) def is_symlink(self): - """ - Whether this path is a symbolic link. - """ + """Whether this path is a symbolic link.""" return S_ISLNK(self._get_mode(follow_symlinks=False)) @@ -183,9 +181,7 @@ def __init__(self, path, entry): self._entry = entry def exists(self, *, follow_symlinks=True): - """ - Whether this path exists - """ + """Whether this path exists.""" if not follow_symlinks: return True try: @@ -200,27 +196,21 @@ def exists(self, *, follow_symlinks=True): return self._exists def is_dir(self, *, follow_symlinks=True): - """ - Whether this path is a directory. - """ + """Whether this path is a directory.""" try: return self._entry.is_dir(follow_symlinks=follow_symlinks) except OSError: return False def is_file(self, *, follow_symlinks=True): - """ - Whether this path is a regular file. - """ + """Whether this path is a regular file.""" try: return self._entry.is_file(follow_symlinks=follow_symlinks) except OSError: return False def is_symlink(self): - """ - Whether this path is a symbolic link. - """ + """Whether this path is a symbolic link.""" try: return self._entry.is_symlink() except OSError: From bd6332a4227b78f1b4f71ade75bec984aeecc9a3 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 17 Dec 2024 14:57:02 +0000 Subject: [PATCH 24/29] Optimise Windows implementation a bit --- Lib/pathlib/_local.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 8bf0dd7bc21a9e..cb7effa48c0dce 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -92,8 +92,12 @@ def exists(self, *, follow_symlinks=True): try: return self._exists except AttributeError: - self._exists = os.path.exists(self) - return self._exists + if os.path.exists(self): + self._exists = True + return True + else: + self._exists = self._is_dir = self._is_file = False + return False def is_dir(self, *, follow_symlinks=True): """Whether this path is a directory.""" @@ -102,8 +106,12 @@ def is_dir(self, *, follow_symlinks=True): try: return self._is_dir except AttributeError: - self._is_dir = os.path.isdir(self) - return self._is_dir + if os.path.isdir(self): + self._is_dir = self._exists = True + return True + else: + self._is_dir = False + return False def is_file(self, *, follow_symlinks=True): """Whether this path is a regular file.""" @@ -112,8 +120,12 @@ def is_file(self, *, follow_symlinks=True): try: return self._is_file except AttributeError: - self._is_file = os.path.isfile(self) - return self._is_file + if os.path.isfile(self): + self._is_file = self._exists = True + return True + else: + self._is_file = False + return False def is_symlink(self): """Whether this path is a symbolic link.""" From f0ee0e941d68d455f9b6309ed547f87b755af772 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 17 Dec 2024 15:14:32 +0000 Subject: [PATCH 25/29] More tidying of _PathStatus and friends --- Lib/pathlib/_local.py | 130 ++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 67 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index cb7effa48c0dce..732ae75f72bcc9 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -66,24 +66,67 @@ def __repr__(self): return "<{}.parents>".format(type(self._path).__name__) -class _PathStatusBase: - __slots__ = ('_path', '_repr') +class _StatusBase: + __slots__ = () - def __init__(self, path): - self._path = str(path) - self._repr = f"<{type(path).__name__}.status>" + def __repr__(self): + path_type = "WindowsPath" if os.name == "nt" else "PosixPath" + return f"<{path_type}.status>" - def __fspath__(self): - return self._path - def __repr__(self): - return self._repr +class _DirEntryStatus(_StatusBase): + """Implementation of pathlib.types.Status that provides file status + information by querying a wrapped os.DirEntry object. Don't try to + construct it yourself.""" + __slots__ = ('_entry', '_exists') + + def __init__(self, entry): + self._entry = entry + + def exists(self, *, follow_symlinks=True): + """Whether this path exists.""" + if not follow_symlinks: + return True + try: + return self._exists + except AttributeError: + try: + self._entry.stat() + except OSError: + self._exists = False + else: + self._exists = True + return self._exists + + def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" + try: + return self._entry.is_dir(follow_symlinks=follow_symlinks) + except OSError: + return False + + def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" + try: + return self._entry.is_file(follow_symlinks=follow_symlinks) + except OSError: + return False + + def is_symlink(self): + """Whether this path is a symbolic link.""" + try: + return self._entry.is_symlink() + except OSError: + return False -class _WindowsPathStatus(_PathStatusBase): +class _WindowsPathStatus(_StatusBase): """Implementation of pathlib.types.Status that provides file status information for Windows paths. Don't try to construct it yourself.""" - __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink') + __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') + + def __init__(self, path): + self._path = str(path) def exists(self, *, follow_symlinks=True): """Whether this path exists.""" @@ -92,7 +135,7 @@ def exists(self, *, follow_symlinks=True): try: return self._exists except AttributeError: - if os.path.exists(self): + if os.path.exists(self._path): self._exists = True return True else: @@ -106,7 +149,7 @@ def is_dir(self, *, follow_symlinks=True): try: return self._is_dir except AttributeError: - if os.path.isdir(self): + if os.path.isdir(self._path): self._is_dir = self._exists = True return True else: @@ -120,7 +163,7 @@ def is_file(self, *, follow_symlinks=True): try: return self._is_file except AttributeError: - if os.path.isfile(self): + if os.path.isfile(self._path): self._is_file = self._exists = True return True else: @@ -132,17 +175,17 @@ def is_symlink(self): try: return self._is_symlink except AttributeError: - self._is_symlink = os.path.islink(self) + self._is_symlink = os.path.islink(self._path) return self._is_symlink -class _PosixPathStatus(_PathStatusBase): +class _PosixPathStatus(_StatusBase): """Implementation of pathlib.types.Status that provides file status information for POSIX paths. Don't try to construct it yourself.""" - __slots__ = ('_mode',) + __slots__ = ('_path', '_mode') def __init__(self, path): - super().__init__(path) + self._path = str(path) self._mode = [None, None] def _get_mode(self, *, follow_symlinks=True): @@ -150,7 +193,7 @@ def _get_mode(self, *, follow_symlinks=True): mode = self._mode[idx] if mode is None: try: - st = os.stat(self, follow_symlinks=follow_symlinks) + st = os.stat(self._path, follow_symlinks=follow_symlinks) except (OSError, ValueError): mode = 0 else: @@ -182,53 +225,6 @@ def is_symlink(self): _PathStatus = _WindowsPathStatus if os.name == 'nt' else _PosixPathStatus -class _DirEntryStatus(_PathStatusBase): - """Implementation of pathlib.types.Status that provides file status - information by querying a wrapped os.DirEntry object. Don't try to - construct it yourself.""" - __slots__ = ('_entry', '_exists') - - def __init__(self, path, entry): - super().__init__(path) - self._entry = entry - - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - if not follow_symlinks: - return True - try: - return self._exists - except AttributeError: - try: - self._entry.stat() - except OSError: - self._exists = False - else: - self._exists = True - return self._exists - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - try: - return self._entry.is_dir(follow_symlinks=follow_symlinks) - except OSError: - return False - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - try: - return self._entry.is_file(follow_symlinks=follow_symlinks) - except OSError: - return False - - def is_symlink(self): - """Whether this path is a symbolic link.""" - try: - return self._entry.is_symlink() - except OSError: - return False - - class PurePath(PurePathBase): """Base class for manipulating paths without I/O. @@ -878,7 +874,7 @@ def _filter_trailing_slash(self, paths): def _from_dir_entry(self, dir_entry, path_str): path = self.with_segments(path_str) path._str = path_str - path._status = _DirEntryStatus(path, dir_entry) + path._status = _DirEntryStatus(dir_entry) return path def iterdir(self): From 5ae8b066135d3f95c95cd44409f1901112aadd83 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 21 Dec 2024 19:40:12 +0000 Subject: [PATCH 26/29] `status` --> `info` --- Doc/library/pathlib.rst | 18 +- Doc/whatsnew/3.14.rst | 4 +- Lib/glob.py | 2 +- Lib/pathlib/_abc.py | 6 +- Lib/pathlib/_local.py | 32 ++-- Lib/pathlib/types.py | 4 +- Lib/test/test_pathlib/test_pathlib.py | 64 +++---- Lib/test/test_pathlib/test_pathlib_abc.py | 168 +++++++++--------- ...-12-10-19-39-35.gh-issue-125413.wOb4yr.rst | 4 +- 9 files changed, 151 insertions(+), 151 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index fbb17721421333..8977ccfe6e4124 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1177,19 +1177,19 @@ Querying file type and status .. versionadded:: 3.5 -.. attribute:: Path.status +.. attribute:: Path.info - A :class:`~pathlib.types.Status` object that supports querying file type + A :class:`~pathlib.types.PathInfo` 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:: >>> p = Path('src') - >>> if p.status.is_symlink(): + >>> if p.info.is_symlink(): ... print('symlink') - ... elif p.status.is_dir(): + ... elif p.info.is_dir(): ... print('directory') - ... elif p.status.exists(): + ... elif p.info.exists(): ... print('something else') ... else: ... print('not found') @@ -1198,13 +1198,13 @@ Querying file type and status If the path was generated from :meth:`Path.iterdir` then this attribute is initialized with some information about the file type gleaned from scanning - the parent directory. Merely accessing :attr:`Path.status` does not perform + the parent directory. Merely accessing :attr:`Path.info` does not perform any filesystem queries. To fetch up-to-date information, it's best to call :meth:`Path.is_dir`, :meth:`~Path.is_file` and :meth:`~Path.is_symlink` rather than methods of this attribute. There is no way to reset the cache; instead you can create - a new path object with an empty status cache via ``p = Path(p)``. + a new path object with an empty info cache via ``p = Path(p)``. .. versionadded:: 3.14 @@ -1949,10 +1949,10 @@ The :mod:`pathlib.types` module provides types for static type checking. .. versionadded:: 3.14 -.. class:: Status() +.. class:: PathInfo() A :class:`typing.Protocol` describing the - :attr:`Path.status ` attribute. Implementations may + :attr:`Path.info ` attribute. Implementations may return cached results from their methods. .. method:: exists(*, follow_symlinks=True) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index d6c58a2f4d1dc2..6331b4e725c981 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -539,8 +539,8 @@ 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 +* Add :attr:`pathlib.Path.info` attribute, which stores an object + implementing the :class:`pathlib.types.PathInfo` protocol (also new). The object supports querying the file type and internally caching :func:`~os.stat` results. Path objects generated by :meth:`~pathlib.Path.iterdir` are initialized with file type information diff --git a/Lib/glob.py b/Lib/glob.py index 0abc17824808a6..a834ea7f7ce556 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -552,7 +552,7 @@ class _PathGlobber(_GlobberBase): @staticmethod def scandir(path): - return ((child.status, child.name, child) for child in path.iterdir()) + return ((child.info, child.name, child) for child in path.iterdir()) @staticmethod def concat_path(path, text): diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index bc54d069b70945..20850a492f32cb 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -339,9 +339,9 @@ class PathBase(PurePathBase): __slots__ = () @property - def status(self): + def info(self): """ - A Status object that exposes the file type and other file attributes + A PathInfo object that exposes the file type and other file attributes of this path. """ # TODO: make this abstract, delete PathBase.stat(). @@ -521,7 +521,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.info.is_dir(follow_symlinks=follow_symlinks): if not top_down: paths.append(child) dirnames.append(child.name) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 732ae75f72bcc9..a0861956eb42ba 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -66,16 +66,16 @@ def __repr__(self): return "<{}.parents>".format(type(self._path).__name__) -class _StatusBase: +class _PathInfoBase: __slots__ = () def __repr__(self): path_type = "WindowsPath" if os.name == "nt" else "PosixPath" - return f"<{path_type}.status>" + return f"<{path_type}.info>" -class _DirEntryStatus(_StatusBase): - """Implementation of pathlib.types.Status that provides file status +class _DirEntryInfo(_PathInfoBase): + """Implementation of pathlib.types.PathInfo that provides status information by querying a wrapped os.DirEntry object. Don't try to construct it yourself.""" __slots__ = ('_entry', '_exists') @@ -120,8 +120,8 @@ def is_symlink(self): return False -class _WindowsPathStatus(_StatusBase): - """Implementation of pathlib.types.Status that provides file status +class _WindowsPathInfo(_PathInfoBase): + """Implementation of pathlib.types.PathInfo that provides status information for Windows paths. Don't try to construct it yourself.""" __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') @@ -179,8 +179,8 @@ def is_symlink(self): return self._is_symlink -class _PosixPathStatus(_StatusBase): - """Implementation of pathlib.types.Status that provides file status +class _PosixPathInfo(_PathInfoBase): + """Implementation of pathlib.types.PathInfo that provides status information for POSIX paths. Don't try to construct it yourself.""" __slots__ = ('_path', '_mode') @@ -222,7 +222,7 @@ def is_symlink(self): return S_ISLNK(self._get_mode(follow_symlinks=False)) -_PathStatus = _WindowsPathStatus if os.name == 'nt' else _PosixPathStatus +_PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo class PurePath(PurePathBase): @@ -691,7 +691,7 @@ class Path(PathBase, PurePath): object. You can also instantiate a PosixPath or WindowsPath directly, but cannot instantiate a WindowsPath on a POSIX system or vice versa. """ - __slots__ = ('_status',) + __slots__ = ('_info',) def __new__(cls, *args, **kwargs): if cls is Path: @@ -699,16 +699,16 @@ def __new__(cls, *args, **kwargs): return object.__new__(cls) @property - def status(self): + def info(self): """ - A Status object that exposes the file type and other file attributes + A PathInfo object that exposes the file type and other file attributes of this path. """ try: - return self._status + return self._info except AttributeError: - self._status = _PathStatus(self) - return self._status + self._info = _PathInfo(self) + return self._info def stat(self, *, follow_symlinks=True): """ @@ -874,7 +874,7 @@ def _filter_trailing_slash(self, paths): def _from_dir_entry(self, dir_entry, path_str): path = self.with_segments(path_str) path._str = path_str - path._status = _DirEntryStatus(dir_entry) + path._info = _DirEntryInfo(dir_entry) return path def iterdir(self): diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index f710e1d8341325..e3d417d0a0507f 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -23,8 +23,8 @@ def isabs(self, path: str) -> bool: ... @runtime_checkable -class Status(Protocol): - """Protocol for path statuses, which support querying the file type. +class PathInfo(Protocol): + """Protocol for path info objects, which support querying the file type. Methods may return cached results. """ def exists(self, *, follow_symlinks: bool = True) -> bool: ... diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 61adc2db9ab389..dd6f1f6876fb96 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -1777,66 +1777,66 @@ def test_symlink_to_unsupported(self): with self.assertRaises(pathlib.UnsupportedOperation): q.symlink_to(p) - def test_status_exists_caching(self): + def test_info_exists_caching(self): p = self.cls(self.base) q = p / 'myfile' - self.assertFalse(q.status.exists()) - self.assertFalse(q.status.exists(follow_symlinks=False)) + self.assertFalse(q.info.exists()) + self.assertFalse(q.info.exists(follow_symlinks=False)) q.write_text('hullo') - self.assertFalse(q.status.exists()) - self.assertFalse(q.status.exists(follow_symlinks=False)) + self.assertFalse(q.info.exists()) + self.assertFalse(q.info.exists(follow_symlinks=False)) q = p / 'myfile' # same path, new instance. - self.assertTrue(q.status.exists()) - self.assertTrue(q.status.exists(follow_symlinks=False)) + self.assertTrue(q.info.exists()) + self.assertTrue(q.info.exists(follow_symlinks=False)) q.unlink() - self.assertTrue(q.status.exists()) - self.assertTrue(q.status.exists(follow_symlinks=False)) + self.assertTrue(q.info.exists()) + self.assertTrue(q.info.exists(follow_symlinks=False)) - def test_status_is_dir_caching(self): + def test_info_is_dir_caching(self): p = self.cls(self.base) q = p / 'mydir' - self.assertFalse(q.status.is_dir()) - self.assertFalse(q.status.is_dir(follow_symlinks=False)) + self.assertFalse(q.info.is_dir()) + self.assertFalse(q.info.is_dir(follow_symlinks=False)) q.mkdir() - self.assertFalse(q.status.is_dir()) - self.assertFalse(q.status.is_dir(follow_symlinks=False)) + self.assertFalse(q.info.is_dir()) + self.assertFalse(q.info.is_dir(follow_symlinks=False)) q = p / 'mydir' # same path, new instance. - self.assertTrue(q.status.is_dir()) - self.assertTrue(q.status.is_dir(follow_symlinks=False)) + self.assertTrue(q.info.is_dir()) + self.assertTrue(q.info.is_dir(follow_symlinks=False)) q.rmdir() - self.assertTrue(q.status.is_dir()) - self.assertTrue(q.status.is_dir(follow_symlinks=False)) + self.assertTrue(q.info.is_dir()) + self.assertTrue(q.info.is_dir(follow_symlinks=False)) - def test_status_is_file_caching(self): + def test_info_is_file_caching(self): p = self.cls(self.base) q = p / 'myfile' - self.assertFalse(q.status.is_file()) - self.assertFalse(q.status.is_file(follow_symlinks=False)) + self.assertFalse(q.info.is_file()) + self.assertFalse(q.info.is_file(follow_symlinks=False)) q.write_text('hullo') - self.assertFalse(q.status.is_file()) - self.assertFalse(q.status.is_file(follow_symlinks=False)) + self.assertFalse(q.info.is_file()) + self.assertFalse(q.info.is_file(follow_symlinks=False)) q = p / 'myfile' # same path, new instance. - self.assertTrue(q.status.is_file()) - self.assertTrue(q.status.is_file(follow_symlinks=False)) + self.assertTrue(q.info.is_file()) + self.assertTrue(q.info.is_file(follow_symlinks=False)) q.unlink() - self.assertTrue(q.status.is_file()) - self.assertTrue(q.status.is_file(follow_symlinks=False)) + self.assertTrue(q.info.is_file()) + self.assertTrue(q.info.is_file(follow_symlinks=False)) @needs_symlinks - def test_status_is_symlink_caching(self): + def test_info_is_symlink_caching(self): p = self.cls(self.base) q = p / 'mylink' - self.assertFalse(q.status.is_symlink()) + self.assertFalse(q.info.is_symlink()) q.symlink_to('blah') - self.assertFalse(q.status.is_symlink()) + self.assertFalse(q.info.is_symlink()) q = p / 'mylink' # same path, new instance. - self.assertTrue(q.status.is_symlink()) + self.assertTrue(q.info.is_symlink()) q.unlink() - self.assertTrue(q.status.is_symlink()) + self.assertTrue(q.info.is_symlink()) @needs_symlinks def test_stat_no_follow_symlinks(self): diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index abdec441f574f5..dd7007973780bc 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -6,7 +6,7 @@ import unittest from pathlib._abc import PurePathBase, PathBase -from pathlib.types import Parser, Status +from pathlib.types import Parser, PathInfo import posixpath from test.support.os_helper import TESTFN @@ -1853,19 +1853,19 @@ def test_iterdir_nodir(self): self.assertIn(cm.exception.errno, (errno.ENOTDIR, errno.ENOENT, errno.EINVAL)) - def test_iterdir_status(self): + def test_iterdir_info(self): p = self.cls(self.base) for child in p.iterdir(): - entry = child.status - self.assertIsInstance(entry, Status) - self.assertEqual(entry.exists(), child.exists()) - self.assertEqual(entry.is_dir(), child.is_dir()) - self.assertEqual(entry.is_file(), child.is_file()) - self.assertEqual(entry.is_symlink(), child.is_symlink()) - self.assertTrue(entry.exists(follow_symlinks=False)) - self.assertEqual(entry.is_dir(follow_symlinks=False), + info = child.info + self.assertIsInstance(info, PathInfo) + self.assertEqual(info.exists(), child.exists()) + self.assertEqual(info.is_dir(), child.is_dir()) + self.assertEqual(info.is_file(), child.is_file()) + self.assertEqual(info.is_symlink(), child.is_symlink()) + self.assertTrue(info.exists(follow_symlinks=False)) + self.assertEqual(info.is_dir(follow_symlinks=False), child.is_dir(follow_symlinks=False)) - self.assertEqual(entry.is_file(follow_symlinks=False), + self.assertEqual(info.is_file(follow_symlinks=False), child.is_file(follow_symlinks=False)) def test_glob_common(self): @@ -1992,87 +1992,87 @@ def test_rglob_windows(self): self.assertEqual(set(p.rglob("FILEd")), { P(self.base, "dirC/dirD/fileD") }) self.assertEqual(set(p.rglob("*\\")), { P(self.base, "dirC/dirD/") }) - def test_status_exists(self): + def test_info_exists(self): p = self.cls(self.base) - self.assertTrue(p.status.exists()) - self.assertTrue((p / 'dirA').status.exists()) - self.assertTrue((p / 'dirA').status.exists(follow_symlinks=False)) - self.assertTrue((p / 'fileA').status.exists()) - self.assertTrue((p / 'fileA').status.exists(follow_symlinks=False)) - self.assertFalse((p / 'non-existing').status.exists()) - self.assertFalse((p / 'non-existing').status.exists(follow_symlinks=False)) + self.assertTrue(p.info.exists()) + self.assertTrue((p / 'dirA').info.exists()) + self.assertTrue((p / 'dirA').info.exists(follow_symlinks=False)) + self.assertTrue((p / 'fileA').info.exists()) + self.assertTrue((p / 'fileA').info.exists(follow_symlinks=False)) + self.assertFalse((p / 'non-existing').info.exists()) + self.assertFalse((p / 'non-existing').info.exists(follow_symlinks=False)) if self.can_symlink: - self.assertTrue((p / 'linkA').status.exists()) - self.assertTrue((p / 'linkA').status.exists(follow_symlinks=False)) - self.assertTrue((p / 'linkB').status.exists()) - self.assertTrue((p / 'linkB').status.exists(follow_symlinks=True)) - self.assertFalse((p / 'brokenLink').status.exists()) - self.assertTrue((p / 'brokenLink').status.exists(follow_symlinks=False)) - self.assertFalse((p / 'brokenLinkLoop').status.exists()) - self.assertTrue((p / 'brokenLinkLoop').status.exists(follow_symlinks=False)) - self.assertFalse((p / 'fileA\udfff').status.exists()) - self.assertFalse((p / 'fileA\udfff').status.exists(follow_symlinks=False)) - self.assertFalse((p / 'fileA\x00').status.exists()) - self.assertFalse((p / 'fileA\x00').status.exists(follow_symlinks=False)) - - def test_status_is_dir(self): + self.assertTrue((p / 'linkA').info.exists()) + self.assertTrue((p / 'linkA').info.exists(follow_symlinks=False)) + self.assertTrue((p / 'linkB').info.exists()) + self.assertTrue((p / 'linkB').info.exists(follow_symlinks=True)) + self.assertFalse((p / 'brokenLink').info.exists()) + self.assertTrue((p / 'brokenLink').info.exists(follow_symlinks=False)) + self.assertFalse((p / 'brokenLinkLoop').info.exists()) + self.assertTrue((p / 'brokenLinkLoop').info.exists(follow_symlinks=False)) + self.assertFalse((p / 'fileA\udfff').info.exists()) + self.assertFalse((p / 'fileA\udfff').info.exists(follow_symlinks=False)) + self.assertFalse((p / 'fileA\x00').info.exists()) + self.assertFalse((p / 'fileA\x00').info.exists(follow_symlinks=False)) + + def test_info_is_dir(self): p = self.cls(self.base) - self.assertTrue((p / 'dirA').status.is_dir()) - self.assertTrue((p / 'dirA').status.is_dir(follow_symlinks=False)) - self.assertFalse((p / 'fileA').status.is_dir()) - self.assertFalse((p / 'fileA').status.is_dir(follow_symlinks=False)) - self.assertFalse((p / 'non-existing').status.is_dir()) - self.assertFalse((p / 'non-existing').status.is_dir(follow_symlinks=False)) + self.assertTrue((p / 'dirA').info.is_dir()) + self.assertTrue((p / 'dirA').info.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'fileA').info.is_dir()) + self.assertFalse((p / 'fileA').info.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'non-existing').info.is_dir()) + self.assertFalse((p / 'non-existing').info.is_dir(follow_symlinks=False)) if self.can_symlink: - self.assertFalse((p / 'linkA').status.is_dir()) - self.assertFalse((p / 'linkA').status.is_dir(follow_symlinks=False)) - self.assertTrue((p / 'linkB').status.is_dir()) - self.assertFalse((p / 'linkB').status.is_dir(follow_symlinks=False)) - self.assertFalse((p / 'brokenLink').status.is_dir()) - self.assertFalse((p / 'brokenLink').status.is_dir(follow_symlinks=False)) - self.assertFalse((p / 'brokenLinkLoop').status.is_dir()) - self.assertFalse((p / 'brokenLinkLoop').status.is_dir(follow_symlinks=False)) - self.assertFalse((p / 'dirA\udfff').status.is_dir()) - self.assertFalse((p / 'dirA\udfff').status.is_dir(follow_symlinks=False)) - self.assertFalse((p / 'dirA\x00').status.is_dir()) - self.assertFalse((p / 'dirA\x00').status.is_dir(follow_symlinks=False)) - - def test_status_is_file(self): + self.assertFalse((p / 'linkA').info.is_dir()) + self.assertFalse((p / 'linkA').info.is_dir(follow_symlinks=False)) + self.assertTrue((p / 'linkB').info.is_dir()) + self.assertFalse((p / 'linkB').info.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'brokenLink').info.is_dir()) + self.assertFalse((p / 'brokenLink').info.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'brokenLinkLoop').info.is_dir()) + self.assertFalse((p / 'brokenLinkLoop').info.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'dirA\udfff').info.is_dir()) + self.assertFalse((p / 'dirA\udfff').info.is_dir(follow_symlinks=False)) + self.assertFalse((p / 'dirA\x00').info.is_dir()) + self.assertFalse((p / 'dirA\x00').info.is_dir(follow_symlinks=False)) + + def test_info_is_file(self): p = self.cls(self.base) - self.assertTrue((p / 'fileA').status.is_file()) - self.assertTrue((p / 'fileA').status.is_file(follow_symlinks=False)) - self.assertFalse((p / 'dirA').status.is_file()) - self.assertFalse((p / 'dirA').status.is_file(follow_symlinks=False)) - self.assertFalse((p / 'non-existing').status.is_file()) - self.assertFalse((p / 'non-existing').status.is_file(follow_symlinks=False)) + self.assertTrue((p / 'fileA').info.is_file()) + self.assertTrue((p / 'fileA').info.is_file(follow_symlinks=False)) + self.assertFalse((p / 'dirA').info.is_file()) + self.assertFalse((p / 'dirA').info.is_file(follow_symlinks=False)) + self.assertFalse((p / 'non-existing').info.is_file()) + self.assertFalse((p / 'non-existing').info.is_file(follow_symlinks=False)) if self.can_symlink: - self.assertTrue((p / 'linkA').status.is_file()) - self.assertFalse((p / 'linkA').status.is_file(follow_symlinks=False)) - self.assertFalse((p / 'linkB').status.is_file()) - self.assertFalse((p / 'linkB').status.is_file(follow_symlinks=False)) - self.assertFalse((p / 'brokenLink').status.is_file()) - self.assertFalse((p / 'brokenLink').status.is_file(follow_symlinks=False)) - self.assertFalse((p / 'brokenLinkLoop').status.is_file()) - self.assertFalse((p / 'brokenLinkLoop').status.is_file(follow_symlinks=False)) - self.assertFalse((p / 'fileA\udfff').status.is_file()) - self.assertFalse((p / 'fileA\udfff').status.is_file(follow_symlinks=False)) - self.assertFalse((p / 'fileA\x00').status.is_file()) - self.assertFalse((p / 'fileA\x00').status.is_file(follow_symlinks=False)) - - def test_status_is_symlink(self): + self.assertTrue((p / 'linkA').info.is_file()) + self.assertFalse((p / 'linkA').info.is_file(follow_symlinks=False)) + self.assertFalse((p / 'linkB').info.is_file()) + self.assertFalse((p / 'linkB').info.is_file(follow_symlinks=False)) + self.assertFalse((p / 'brokenLink').info.is_file()) + self.assertFalse((p / 'brokenLink').info.is_file(follow_symlinks=False)) + self.assertFalse((p / 'brokenLinkLoop').info.is_file()) + self.assertFalse((p / 'brokenLinkLoop').info.is_file(follow_symlinks=False)) + self.assertFalse((p / 'fileA\udfff').info.is_file()) + self.assertFalse((p / 'fileA\udfff').info.is_file(follow_symlinks=False)) + self.assertFalse((p / 'fileA\x00').info.is_file()) + self.assertFalse((p / 'fileA\x00').info.is_file(follow_symlinks=False)) + + def test_info_is_symlink(self): p = self.cls(self.base) - self.assertFalse((p / 'fileA').status.is_symlink()) - self.assertFalse((p / 'dirA').status.is_symlink()) - self.assertFalse((p / 'non-existing').status.is_symlink()) + self.assertFalse((p / 'fileA').info.is_symlink()) + self.assertFalse((p / 'dirA').info.is_symlink()) + self.assertFalse((p / 'non-existing').info.is_symlink()) if self.can_symlink: - self.assertTrue((p / 'linkA').status.is_symlink()) - self.assertTrue((p / 'linkB').status.is_symlink()) - self.assertTrue((p / 'brokenLink').status.is_symlink()) - self.assertFalse((p / 'linkA\udfff').status.is_symlink()) - self.assertFalse((p / 'linkA\x00').status.is_symlink()) - self.assertTrue((p / 'brokenLinkLoop').status.is_symlink()) - self.assertFalse((p / 'fileA\udfff').status.is_symlink()) - self.assertFalse((p / 'fileA\x00').status.is_symlink()) + self.assertTrue((p / 'linkA').info.is_symlink()) + self.assertTrue((p / 'linkB').info.is_symlink()) + self.assertTrue((p / 'brokenLink').info.is_symlink()) + self.assertFalse((p / 'linkA\udfff').info.is_symlink()) + self.assertFalse((p / 'linkA\x00').info.is_symlink()) + self.assertTrue((p / 'brokenLinkLoop').info.is_symlink()) + self.assertFalse((p / 'fileA\udfff').info.is_symlink()) + self.assertFalse((p / 'fileA\x00').info.is_symlink()) def test_stat(self): statA = self.cls(self.base).joinpath('fileA').stat() diff --git a/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst b/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst index c54a8b3a70248b..9ac96179a88367 100644 --- a/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst +++ b/Misc/NEWS.d/next/Library/2024-12-10-19-39-35.gh-issue-125413.wOb4yr.rst @@ -1,5 +1,5 @@ -Add :attr:`pathlib.Path.status` attribute, which stores an object -implementing the :class:`pathlib.types.Status` protocol (also new). The +Add :attr:`pathlib.Path.info` attribute, which stores an object +implementing the :class:`pathlib.types.PathInfo` protocol (also new). The object supports querying the file type and internally caching :func:`~os.stat` results. Path objects generated by :meth:`~pathlib.Path.iterdir` are initialized with file type information From 6e25d2de41e41574fca257baa740afeb7a01bb25 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 21 Dec 2024 19:43:16 +0000 Subject: [PATCH 27/29] `Parser` --> `_PathParser` --- Lib/pathlib/types.py | 2 +- Lib/test/test_pathlib/test_pathlib_abc.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index e3d417d0a0507f..b2eb40f829e2d0 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -5,7 +5,7 @@ @runtime_checkable -class Parser(Protocol): +class _PathParser(Protocol): """Protocol for path parsers, which do low-level path manipulation. Path parsers provide a subset of the os.path API, specifically those diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index dd7007973780bc..18ca8d07f60393 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -6,7 +6,7 @@ import unittest from pathlib._abc import PurePathBase, PathBase -from pathlib.types import Parser, PathInfo +from pathlib.types import _PathParser, PathInfo import posixpath from test.support.os_helper import TESTFN @@ -85,7 +85,7 @@ def setUp(self): self.altsep = self.parser.altsep def test_parser(self): - self.assertIsInstance(self.cls.parser, Parser) + self.assertIsInstance(self.cls.parser, _PathParser) def test_constructor_common(self): P = self.cls From ee7da1d3cceea1a2a65a2117c477cfaf22bd3728 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Fri, 10 Jan 2025 17:16:55 +0000 Subject: [PATCH 28/29] Update Lib/pathlib/_local.py Co-authored-by: Petr Viktorin --- Lib/pathlib/_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 2c3fd00fac282c..9fc901e54bd379 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -188,7 +188,7 @@ def __init__(self, path): self._mode = [None, None] def _get_mode(self, *, follow_symlinks=True): - idx = int(follow_symlinks) + idx = bool(follow_symlinks) mode = self._mode[idx] if mode is None: try: From d2e254af97c783dfe98b1975c3b44bf3baa956a9 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 20 Jan 2025 06:51:01 +0000 Subject: [PATCH 29/29] Move path info from `pathlib._local` to `pathlib._os`. --- Lib/pathlib/_local.py | 167 +----------------------------------------- Lib/pathlib/_os.py | 160 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 163 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 7b783f80e674ed..c3e52a65d9a1fe 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -7,7 +7,7 @@ from errno import * from glob import _StringGlobber, _no_recurse_symlinks from itertools import chain -from stat import S_IMODE, S_ISDIR, S_ISREG, S_ISLNK, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO +from stat import S_IMODE, S_ISDIR, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO from _collections_abc import Sequence try: @@ -19,7 +19,7 @@ except ImportError: grp = None -from pathlib._os import copyfile +from pathlib._os import copyfile, PathInfo, DirEntryInfo from pathlib._abc import CopyWriter, JoinablePath, WritablePath @@ -65,165 +65,6 @@ def __repr__(self): return "<{}.parents>".format(type(self._path).__name__) -class _PathInfoBase: - __slots__ = () - - def __repr__(self): - path_type = "WindowsPath" if os.name == "nt" else "PosixPath" - return f"<{path_type}.info>" - - -class _DirEntryInfo(_PathInfoBase): - """Implementation of pathlib.types.PathInfo that provides status - information by querying a wrapped os.DirEntry object. Don't try to - construct it yourself.""" - __slots__ = ('_entry', '_exists') - - def __init__(self, entry): - self._entry = entry - - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - if not follow_symlinks: - return True - try: - return self._exists - except AttributeError: - try: - self._entry.stat() - except OSError: - self._exists = False - else: - self._exists = True - return self._exists - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - try: - return self._entry.is_dir(follow_symlinks=follow_symlinks) - except OSError: - return False - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - try: - return self._entry.is_file(follow_symlinks=follow_symlinks) - except OSError: - return False - - def is_symlink(self): - """Whether this path is a symbolic link.""" - try: - return self._entry.is_symlink() - except OSError: - return False - - -class _WindowsPathInfo(_PathInfoBase): - """Implementation of pathlib.types.PathInfo that provides status - information for Windows paths. Don't try to construct it yourself.""" - __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') - - def __init__(self, path): - self._path = str(path) - - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - if not follow_symlinks and self.is_symlink(): - return True - try: - return self._exists - except AttributeError: - if os.path.exists(self._path): - self._exists = True - return True - else: - self._exists = self._is_dir = self._is_file = False - return False - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - if not follow_symlinks and self.is_symlink(): - return False - try: - return self._is_dir - except AttributeError: - if os.path.isdir(self._path): - self._is_dir = self._exists = True - return True - else: - self._is_dir = False - return False - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - if not follow_symlinks and self.is_symlink(): - return False - try: - return self._is_file - except AttributeError: - if os.path.isfile(self._path): - self._is_file = self._exists = True - return True - else: - self._is_file = False - return False - - def is_symlink(self): - """Whether this path is a symbolic link.""" - try: - return self._is_symlink - except AttributeError: - self._is_symlink = os.path.islink(self._path) - return self._is_symlink - - -class _PosixPathInfo(_PathInfoBase): - """Implementation of pathlib.types.PathInfo that provides status - information for POSIX paths. Don't try to construct it yourself.""" - __slots__ = ('_path', '_mode') - - def __init__(self, path): - self._path = str(path) - self._mode = [None, None] - - def _get_mode(self, *, follow_symlinks=True): - idx = bool(follow_symlinks) - mode = self._mode[idx] - if mode is None: - try: - st = os.stat(self._path, follow_symlinks=follow_symlinks) - except (OSError, ValueError): - mode = 0 - else: - mode = st.st_mode - if follow_symlinks or S_ISLNK(mode): - self._mode[idx] = mode - else: - # Not a symlink, so stat() will give the same result - self._mode = [mode, mode] - return mode - - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - return self._get_mode(follow_symlinks=follow_symlinks) > 0 - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - return S_ISDIR(self._get_mode(follow_symlinks=follow_symlinks)) - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - return S_ISREG(self._get_mode(follow_symlinks=follow_symlinks)) - - def is_symlink(self): - """Whether this path is a symbolic link.""" - return S_ISLNK(self._get_mode(follow_symlinks=False)) - - -_PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo - - class _LocalCopyWriter(CopyWriter): """This object implements the Path.copy callable. Don't try to construct it yourself.""" @@ -867,7 +708,7 @@ def info(self): try: return self._info except AttributeError: - self._info = _PathInfo(self) + self._info = PathInfo(self) return self._info def stat(self, *, follow_symlinks=True): @@ -1026,7 +867,7 @@ def _filter_trailing_slash(self, paths): def _from_dir_entry(self, dir_entry, path_str): path = self.with_segments(path_str) path._str = path_str - path._info = _DirEntryInfo(dir_entry) + path._info = DirEntryInfo(dir_entry) return path def iterdir(self): diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 57bcaf3d680138..c2febb773cd83a 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -3,6 +3,7 @@ """ from errno import * +from stat import S_ISDIR, S_ISREG, S_ISLNK import os import sys try: @@ -162,3 +163,162 @@ def copyfileobj(source_f, target_f): write_target = target_f.write while buf := read_source(1024 * 1024): write_target(buf) + + +class _PathInfoBase: + __slots__ = () + + def __repr__(self): + path_type = "WindowsPath" if os.name == "nt" else "PosixPath" + return f"<{path_type}.info>" + + +class _WindowsPathInfo(_PathInfoBase): + """Implementation of pathlib.types.PathInfo that provides status + information for Windows paths. Don't try to construct it yourself.""" + __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') + + def __init__(self, path): + self._path = str(path) + + def exists(self, *, follow_symlinks=True): + """Whether this path exists.""" + if not follow_symlinks and self.is_symlink(): + return True + try: + return self._exists + except AttributeError: + if os.path.exists(self._path): + self._exists = True + return True + else: + self._exists = self._is_dir = self._is_file = False + return False + + def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" + if not follow_symlinks and self.is_symlink(): + return False + try: + return self._is_dir + except AttributeError: + if os.path.isdir(self._path): + self._is_dir = self._exists = True + return True + else: + self._is_dir = False + return False + + def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" + if not follow_symlinks and self.is_symlink(): + return False + try: + return self._is_file + except AttributeError: + if os.path.isfile(self._path): + self._is_file = self._exists = True + return True + else: + self._is_file = False + return False + + def is_symlink(self): + """Whether this path is a symbolic link.""" + try: + return self._is_symlink + except AttributeError: + self._is_symlink = os.path.islink(self._path) + return self._is_symlink + + +class _PosixPathInfo(_PathInfoBase): + """Implementation of pathlib.types.PathInfo that provides status + information for POSIX paths. Don't try to construct it yourself.""" + __slots__ = ('_path', '_mode') + + def __init__(self, path): + self._path = str(path) + self._mode = [None, None] + + def _get_mode(self, *, follow_symlinks=True): + idx = bool(follow_symlinks) + mode = self._mode[idx] + if mode is None: + try: + st = os.stat(self._path, follow_symlinks=follow_symlinks) + except (OSError, ValueError): + mode = 0 + else: + mode = st.st_mode + if follow_symlinks or S_ISLNK(mode): + self._mode[idx] = mode + else: + # Not a symlink, so stat() will give the same result + self._mode = [mode, mode] + return mode + + def exists(self, *, follow_symlinks=True): + """Whether this path exists.""" + return self._get_mode(follow_symlinks=follow_symlinks) > 0 + + def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" + return S_ISDIR(self._get_mode(follow_symlinks=follow_symlinks)) + + def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" + return S_ISREG(self._get_mode(follow_symlinks=follow_symlinks)) + + def is_symlink(self): + """Whether this path is a symbolic link.""" + return S_ISLNK(self._get_mode(follow_symlinks=False)) + + +PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo + + +class DirEntryInfo(_PathInfoBase): + """Implementation of pathlib.types.PathInfo that provides status + information by querying a wrapped os.DirEntry object. Don't try to + construct it yourself.""" + __slots__ = ('_entry', '_exists') + + def __init__(self, entry): + self._entry = entry + + def exists(self, *, follow_symlinks=True): + """Whether this path exists.""" + if not follow_symlinks: + return True + try: + return self._exists + except AttributeError: + try: + self._entry.stat() + except OSError: + self._exists = False + else: + self._exists = True + return self._exists + + def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" + try: + return self._entry.is_dir(follow_symlinks=follow_symlinks) + except OSError: + return False + + def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" + try: + return self._entry.is_file(follow_symlinks=follow_symlinks) + except OSError: + return False + + def is_symlink(self): + """Whether this path is a symbolic link.""" + try: + return self._entry.is_symlink() + except OSError: + return False