Skip to content

Commit

Permalink
pythonGH-127381: pathlib ABCs: remove PathBase.stat() (python#128334)
Browse files Browse the repository at this point in the history
Remove the `PathBase.stat()` method. Its use of the `os.stat_result` API,
with its 10 mandatory fields and low-level types, makes it an awkward fit
for virtual filesystems.

We'll look to add a `PathBase.info` attribute later - see pythonGH-125413.
  • Loading branch information
barneygale authored and srinivasreddy committed Jan 8, 2025
1 parent 7c0567d commit 72c1848
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 83 deletions.
31 changes: 4 additions & 27 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import posixpath
from errno import EINVAL
from glob import _GlobberBase, _no_recurse_symlinks
from stat import S_ISDIR, S_ISLNK, S_ISREG
from pathlib._os import copyfileobj


Expand Down Expand Up @@ -450,55 +449,33 @@ class PathBase(PurePathBase):
"""
__slots__ = ()

def stat(self, *, follow_symlinks=True):
"""
Return the result of the stat() system call on this path, like
os.stat() does.
"""
raise NotImplementedError

# Convenience functions for querying the stat results

def exists(self, *, follow_symlinks=True):
"""
Whether this path exists.
This method normally follows symlinks; to check whether a symlink exists,
add the argument follow_symlinks=False.
"""
try:
self.stat(follow_symlinks=follow_symlinks)
except (OSError, ValueError):
return False
return True
raise NotImplementedError

def is_dir(self, *, follow_symlinks=True):
"""
Whether this path is a directory.
"""
try:
return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
except (OSError, ValueError):
return False
raise NotImplementedError

def is_file(self, *, follow_symlinks=True):
"""
Whether this path is a regular file (also True for symlinks pointing
to regular files).
"""
try:
return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
except (OSError, ValueError):
return False
raise NotImplementedError

def is_symlink(self):
"""
Whether this path is a symbolic link.
"""
try:
return S_ISLNK(self.stat(follow_symlinks=False).st_mode)
except (OSError, ValueError):
return False
raise NotImplementedError

def open(self, mode='r', buffering=-1, encoding=None,
errors=None, newline=None):
Expand Down
12 changes: 9 additions & 3 deletions Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from errno import *
from glob import _StringGlobber, _no_recurse_symlinks
from itertools import chain
from stat import S_IMODE, 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:
Expand Down Expand Up @@ -725,7 +725,10 @@ def is_dir(self, *, follow_symlinks=True):
"""
if follow_symlinks:
return os.path.isdir(self)
return PathBase.is_dir(self, follow_symlinks=follow_symlinks)
try:
return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
except (OSError, ValueError):
return False

def is_file(self, *, follow_symlinks=True):
"""
Expand All @@ -734,7 +737,10 @@ def is_file(self, *, follow_symlinks=True):
"""
if follow_symlinks:
return os.path.isfile(self)
return PathBase.is_file(self, follow_symlinks=follow_symlinks)
try:
return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
except (OSError, ValueError):
return False

def is_mount(self):
"""
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,31 @@ def test_symlink_to_unsupported(self):
with self.assertRaises(pathlib.UnsupportedOperation):
q.symlink_to(p)

def test_stat(self):
statA = self.cls(self.base).joinpath('fileA').stat()
statB = self.cls(self.base).joinpath('dirB', 'fileB').stat()
statC = self.cls(self.base).joinpath('dirC').stat()
# st_mode: files are the same, directory differs.
self.assertIsInstance(statA.st_mode, int)
self.assertEqual(statA.st_mode, statB.st_mode)
self.assertNotEqual(statA.st_mode, statC.st_mode)
self.assertNotEqual(statB.st_mode, statC.st_mode)
# st_ino: all different,
self.assertIsInstance(statA.st_ino, int)
self.assertNotEqual(statA.st_ino, statB.st_ino)
self.assertNotEqual(statA.st_ino, statC.st_ino)
self.assertNotEqual(statB.st_ino, statC.st_ino)
# st_dev: all the same.
self.assertIsInstance(statA.st_dev, int)
self.assertEqual(statA.st_dev, statB.st_dev)
self.assertEqual(statA.st_dev, statC.st_dev)
# other attributes not used by pathlib.

def test_stat_no_follow_symlinks_nosymlink(self):
p = self.cls(self.base) / 'fileA'
st = p.stat()
self.assertEqual(st, p.stat(follow_symlinks=False))

@needs_symlinks
def test_stat_no_follow_symlinks(self):
p = self.cls(self.base) / 'linkA'
Expand Down
77 changes: 24 additions & 53 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import io
import os
import errno
import stat
import unittest

from pathlib._abc import PurePathBase, PathBase
Expand Down Expand Up @@ -1294,11 +1293,6 @@ def close(self):
super().close()


DummyPathStatResult = collections.namedtuple(
'DummyPathStatResult',
'st_mode st_ino st_dev st_nlink st_uid st_gid st_size st_atime st_mtime st_ctime')


class DummyPath(PathBase):
"""
Simple implementation of PathBase that keeps files and directories in
Expand Down Expand Up @@ -1331,15 +1325,17 @@ def __repr__(self):
def with_segments(self, *pathsegments):
return type(self)(*pathsegments)

def stat(self, *, follow_symlinks=True):
path = str(self).rstrip('/')
if path in self._files:
st_mode = stat.S_IFREG
elif path in self._directories:
st_mode = stat.S_IFDIR
else:
raise FileNotFoundError(errno.ENOENT, "Not found", str(self))
return DummyPathStatResult(st_mode, hash(str(self)), 0, 0, 0, 0, 0, 0, 0, 0)
def exists(self, *, follow_symlinks=True):
return self.is_dir() or self.is_file()

def is_dir(self, *, follow_symlinks=True):
return str(self).rstrip('/') in self._directories

def is_file(self, *, follow_symlinks=True):
return str(self) in self._files

def is_symlink(self):
return False

def open(self, mode='r', buffering=-1, encoding=None,
errors=None, newline=None):
Expand Down Expand Up @@ -1958,31 +1954,6 @@ 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_stat(self):
statA = self.cls(self.base).joinpath('fileA').stat()
statB = self.cls(self.base).joinpath('dirB', 'fileB').stat()
statC = self.cls(self.base).joinpath('dirC').stat()
# st_mode: files are the same, directory differs.
self.assertIsInstance(statA.st_mode, int)
self.assertEqual(statA.st_mode, statB.st_mode)
self.assertNotEqual(statA.st_mode, statC.st_mode)
self.assertNotEqual(statB.st_mode, statC.st_mode)
# st_ino: all different,
self.assertIsInstance(statA.st_ino, int)
self.assertNotEqual(statA.st_ino, statB.st_ino)
self.assertNotEqual(statA.st_ino, statC.st_ino)
self.assertNotEqual(statB.st_ino, statC.st_ino)
# st_dev: all the same.
self.assertIsInstance(statA.st_dev, int)
self.assertEqual(statA.st_dev, statB.st_dev)
self.assertEqual(statA.st_dev, statC.st_dev)
# other attributes not used by pathlib.

def test_stat_no_follow_symlinks_nosymlink(self):
p = self.cls(self.base) / 'fileA'
st = p.stat()
self.assertEqual(st, p.stat(follow_symlinks=False))

def test_is_dir(self):
P = self.cls(self.base)
self.assertTrue((P / 'dirA').is_dir())
Expand Down Expand Up @@ -2054,26 +2025,26 @@ def test_is_symlink(self):
def test_delete_file(self):
p = self.cls(self.base) / 'fileA'
p._delete()
self.assertFileNotFound(p.stat)
self.assertFalse(p.exists())
self.assertFileNotFound(p._delete)

def test_delete_dir(self):
base = self.cls(self.base)
base.joinpath('dirA')._delete()
self.assertRaises(FileNotFoundError, base.joinpath('dirA').stat)
self.assertRaises(FileNotFoundError, base.joinpath('dirA', 'linkC').stat,
follow_symlinks=False)
self.assertFalse(base.joinpath('dirA').exists())
self.assertFalse(base.joinpath('dirA', 'linkC').exists(
follow_symlinks=False))
base.joinpath('dirB')._delete()
self.assertRaises(FileNotFoundError, base.joinpath('dirB').stat)
self.assertRaises(FileNotFoundError, base.joinpath('dirB', 'fileB').stat)
self.assertRaises(FileNotFoundError, base.joinpath('dirB', 'linkD').stat,
follow_symlinks=False)
self.assertFalse(base.joinpath('dirB').exists())
self.assertFalse(base.joinpath('dirB', 'fileB').exists())
self.assertFalse(base.joinpath('dirB', 'linkD').exists(
follow_symlinks=False))
base.joinpath('dirC')._delete()
self.assertRaises(FileNotFoundError, base.joinpath('dirC').stat)
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'dirD').stat)
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'dirD', 'fileD').stat)
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'fileC').stat)
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'novel.txt').stat)
self.assertFalse(base.joinpath('dirC').exists())
self.assertFalse(base.joinpath('dirC', 'dirD').exists())
self.assertFalse(base.joinpath('dirC', 'dirD', 'fileD').exists())
self.assertFalse(base.joinpath('dirC', 'fileC').exists())
self.assertFalse(base.joinpath('dirC', 'novel.txt').exists())

def test_delete_missing(self):
tmp = self.cls(self.base, 'delete')
Expand Down

0 comments on commit 72c1848

Please sign in to comment.