Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

GH-127381: pathlib ABCs: remove PathBase.stat() #128334

Merged
merged 2 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
GH-127381: pathlib ABCs: remove PathBase.stat()
Remove the `PathBase.stat()` method. Its use of the `os.stat_result` API,
with its 10 mandatory fields and low-level types, makes it a poor fit for
virtual filesystems.

We'll look to add a `PathBase.info` attribute later - see GH-125413.
  • Loading branch information
barneygale committed Dec 29, 2024
commit 5effdbbebf838fc5990eb8bffe7a24abbdcb3caf
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
72 changes: 24 additions & 48 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 @@ -1331,15 +1330,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 +1959,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 +2030,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
Loading