Skip to content

Commit c78729f

Browse files
authored
GH-127381: pathlib ABCs: remove PathBase.stat() (#128334)
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 GH-125413.
1 parent 7e819ce commit c78729f

File tree

4 files changed

+62
-83
lines changed

4 files changed

+62
-83
lines changed

Lib/pathlib/_abc.py

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import posixpath
1717
from errno import EINVAL
1818
from glob import _GlobberBase, _no_recurse_symlinks
19-
from stat import S_ISDIR, S_ISLNK, S_ISREG
2019
from pathlib._os import copyfileobj
2120

2221

@@ -450,55 +449,33 @@ class PathBase(PurePathBase):
450449
"""
451450
__slots__ = ()
452451

453-
def stat(self, *, follow_symlinks=True):
454-
"""
455-
Return the result of the stat() system call on this path, like
456-
os.stat() does.
457-
"""
458-
raise NotImplementedError
459-
460-
# Convenience functions for querying the stat results
461-
462452
def exists(self, *, follow_symlinks=True):
463453
"""
464454
Whether this path exists.
465455
466456
This method normally follows symlinks; to check whether a symlink exists,
467457
add the argument follow_symlinks=False.
468458
"""
469-
try:
470-
self.stat(follow_symlinks=follow_symlinks)
471-
except (OSError, ValueError):
472-
return False
473-
return True
459+
raise NotImplementedError
474460

475461
def is_dir(self, *, follow_symlinks=True):
476462
"""
477463
Whether this path is a directory.
478464
"""
479-
try:
480-
return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
481-
except (OSError, ValueError):
482-
return False
465+
raise NotImplementedError
483466

484467
def is_file(self, *, follow_symlinks=True):
485468
"""
486469
Whether this path is a regular file (also True for symlinks pointing
487470
to regular files).
488471
"""
489-
try:
490-
return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
491-
except (OSError, ValueError):
492-
return False
472+
raise NotImplementedError
493473

494474
def is_symlink(self):
495475
"""
496476
Whether this path is a symbolic link.
497477
"""
498-
try:
499-
return S_ISLNK(self.stat(follow_symlinks=False).st_mode)
500-
except (OSError, ValueError):
501-
return False
478+
raise NotImplementedError
502479

503480
def open(self, mode='r', buffering=-1, encoding=None,
504481
errors=None, newline=None):

Lib/pathlib/_local.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from errno import *
88
from glob import _StringGlobber, _no_recurse_symlinks
99
from itertools import chain
10-
from stat import S_IMODE, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
10+
from stat import S_IMODE, S_ISDIR, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
1111
from _collections_abc import Sequence
1212

1313
try:
@@ -725,7 +725,10 @@ def is_dir(self, *, follow_symlinks=True):
725725
"""
726726
if follow_symlinks:
727727
return os.path.isdir(self)
728-
return PathBase.is_dir(self, follow_symlinks=follow_symlinks)
728+
try:
729+
return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
730+
except (OSError, ValueError):
731+
return False
729732

730733
def is_file(self, *, follow_symlinks=True):
731734
"""
@@ -734,7 +737,10 @@ def is_file(self, *, follow_symlinks=True):
734737
"""
735738
if follow_symlinks:
736739
return os.path.isfile(self)
737-
return PathBase.is_file(self, follow_symlinks=follow_symlinks)
740+
try:
741+
return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
742+
except (OSError, ValueError):
743+
return False
738744

739745
def is_mount(self):
740746
"""

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,6 +1835,31 @@ def test_symlink_to_unsupported(self):
18351835
with self.assertRaises(pathlib.UnsupportedOperation):
18361836
q.symlink_to(p)
18371837

1838+
def test_stat(self):
1839+
statA = self.cls(self.base).joinpath('fileA').stat()
1840+
statB = self.cls(self.base).joinpath('dirB', 'fileB').stat()
1841+
statC = self.cls(self.base).joinpath('dirC').stat()
1842+
# st_mode: files are the same, directory differs.
1843+
self.assertIsInstance(statA.st_mode, int)
1844+
self.assertEqual(statA.st_mode, statB.st_mode)
1845+
self.assertNotEqual(statA.st_mode, statC.st_mode)
1846+
self.assertNotEqual(statB.st_mode, statC.st_mode)
1847+
# st_ino: all different,
1848+
self.assertIsInstance(statA.st_ino, int)
1849+
self.assertNotEqual(statA.st_ino, statB.st_ino)
1850+
self.assertNotEqual(statA.st_ino, statC.st_ino)
1851+
self.assertNotEqual(statB.st_ino, statC.st_ino)
1852+
# st_dev: all the same.
1853+
self.assertIsInstance(statA.st_dev, int)
1854+
self.assertEqual(statA.st_dev, statB.st_dev)
1855+
self.assertEqual(statA.st_dev, statC.st_dev)
1856+
# other attributes not used by pathlib.
1857+
1858+
def test_stat_no_follow_symlinks_nosymlink(self):
1859+
p = self.cls(self.base) / 'fileA'
1860+
st = p.stat()
1861+
self.assertEqual(st, p.stat(follow_symlinks=False))
1862+
18381863
@needs_symlinks
18391864
def test_stat_no_follow_symlinks(self):
18401865
p = self.cls(self.base) / 'linkA'

Lib/test/test_pathlib/test_pathlib_abc.py

Lines changed: 24 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import io
33
import os
44
import errno
5-
import stat
65
import unittest
76

87
from pathlib._abc import PurePathBase, PathBase
@@ -1294,11 +1293,6 @@ def close(self):
12941293
super().close()
12951294

12961295

1297-
DummyPathStatResult = collections.namedtuple(
1298-
'DummyPathStatResult',
1299-
'st_mode st_ino st_dev st_nlink st_uid st_gid st_size st_atime st_mtime st_ctime')
1300-
1301-
13021296
class DummyPath(PathBase):
13031297
"""
13041298
Simple implementation of PathBase that keeps files and directories in
@@ -1331,15 +1325,17 @@ def __repr__(self):
13311325
def with_segments(self, *pathsegments):
13321326
return type(self)(*pathsegments)
13331327

1334-
def stat(self, *, follow_symlinks=True):
1335-
path = str(self).rstrip('/')
1336-
if path in self._files:
1337-
st_mode = stat.S_IFREG
1338-
elif path in self._directories:
1339-
st_mode = stat.S_IFDIR
1340-
else:
1341-
raise FileNotFoundError(errno.ENOENT, "Not found", str(self))
1342-
return DummyPathStatResult(st_mode, hash(str(self)), 0, 0, 0, 0, 0, 0, 0, 0)
1328+
def exists(self, *, follow_symlinks=True):
1329+
return self.is_dir() or self.is_file()
1330+
1331+
def is_dir(self, *, follow_symlinks=True):
1332+
return str(self).rstrip('/') in self._directories
1333+
1334+
def is_file(self, *, follow_symlinks=True):
1335+
return str(self) in self._files
1336+
1337+
def is_symlink(self):
1338+
return False
13431339

13441340
def open(self, mode='r', buffering=-1, encoding=None,
13451341
errors=None, newline=None):
@@ -1958,31 +1954,6 @@ def test_rglob_windows(self):
19581954
self.assertEqual(set(p.rglob("FILEd")), { P(self.base, "dirC/dirD/fileD") })
19591955
self.assertEqual(set(p.rglob("*\\")), { P(self.base, "dirC/dirD/") })
19601956

1961-
def test_stat(self):
1962-
statA = self.cls(self.base).joinpath('fileA').stat()
1963-
statB = self.cls(self.base).joinpath('dirB', 'fileB').stat()
1964-
statC = self.cls(self.base).joinpath('dirC').stat()
1965-
# st_mode: files are the same, directory differs.
1966-
self.assertIsInstance(statA.st_mode, int)
1967-
self.assertEqual(statA.st_mode, statB.st_mode)
1968-
self.assertNotEqual(statA.st_mode, statC.st_mode)
1969-
self.assertNotEqual(statB.st_mode, statC.st_mode)
1970-
# st_ino: all different,
1971-
self.assertIsInstance(statA.st_ino, int)
1972-
self.assertNotEqual(statA.st_ino, statB.st_ino)
1973-
self.assertNotEqual(statA.st_ino, statC.st_ino)
1974-
self.assertNotEqual(statB.st_ino, statC.st_ino)
1975-
# st_dev: all the same.
1976-
self.assertIsInstance(statA.st_dev, int)
1977-
self.assertEqual(statA.st_dev, statB.st_dev)
1978-
self.assertEqual(statA.st_dev, statC.st_dev)
1979-
# other attributes not used by pathlib.
1980-
1981-
def test_stat_no_follow_symlinks_nosymlink(self):
1982-
p = self.cls(self.base) / 'fileA'
1983-
st = p.stat()
1984-
self.assertEqual(st, p.stat(follow_symlinks=False))
1985-
19861957
def test_is_dir(self):
19871958
P = self.cls(self.base)
19881959
self.assertTrue((P / 'dirA').is_dir())
@@ -2054,26 +2025,26 @@ def test_is_symlink(self):
20542025
def test_delete_file(self):
20552026
p = self.cls(self.base) / 'fileA'
20562027
p._delete()
2057-
self.assertFileNotFound(p.stat)
2028+
self.assertFalse(p.exists())
20582029
self.assertFileNotFound(p._delete)
20592030

20602031
def test_delete_dir(self):
20612032
base = self.cls(self.base)
20622033
base.joinpath('dirA')._delete()
2063-
self.assertRaises(FileNotFoundError, base.joinpath('dirA').stat)
2064-
self.assertRaises(FileNotFoundError, base.joinpath('dirA', 'linkC').stat,
2065-
follow_symlinks=False)
2034+
self.assertFalse(base.joinpath('dirA').exists())
2035+
self.assertFalse(base.joinpath('dirA', 'linkC').exists(
2036+
follow_symlinks=False))
20662037
base.joinpath('dirB')._delete()
2067-
self.assertRaises(FileNotFoundError, base.joinpath('dirB').stat)
2068-
self.assertRaises(FileNotFoundError, base.joinpath('dirB', 'fileB').stat)
2069-
self.assertRaises(FileNotFoundError, base.joinpath('dirB', 'linkD').stat,
2070-
follow_symlinks=False)
2038+
self.assertFalse(base.joinpath('dirB').exists())
2039+
self.assertFalse(base.joinpath('dirB', 'fileB').exists())
2040+
self.assertFalse(base.joinpath('dirB', 'linkD').exists(
2041+
follow_symlinks=False))
20712042
base.joinpath('dirC')._delete()
2072-
self.assertRaises(FileNotFoundError, base.joinpath('dirC').stat)
2073-
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'dirD').stat)
2074-
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'dirD', 'fileD').stat)
2075-
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'fileC').stat)
2076-
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'novel.txt').stat)
2043+
self.assertFalse(base.joinpath('dirC').exists())
2044+
self.assertFalse(base.joinpath('dirC', 'dirD').exists())
2045+
self.assertFalse(base.joinpath('dirC', 'dirD', 'fileD').exists())
2046+
self.assertFalse(base.joinpath('dirC', 'fileC').exists())
2047+
self.assertFalse(base.joinpath('dirC', 'novel.txt').exists())
20772048

20782049
def test_delete_missing(self):
20792050
tmp = self.cls(self.base, 'delete')

0 commit comments

Comments
 (0)