Skip to content

Commit abf9649

Browse files
authored
bpo-39906: Add follow_symlinks parameter to pathlib.Path.stat() and chmod() (GH-18864)
1 parent 7a7ba3d commit abf9649

File tree

4 files changed

+49
-17
lines changed

4 files changed

+49
-17
lines changed

Doc/library/pathlib.rst

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -713,11 +713,14 @@ call fails (for example because the path doesn't exist).
713713
.. versionadded:: 3.5
714714

715715

716-
.. method:: Path.stat()
716+
.. method:: Path.stat(*, follow_symlinks=True)
717717

718718
Return a :class:`os.stat_result` object containing information about this path, like :func:`os.stat`.
719719
The result is looked up at each call to this method.
720720

721+
This method normally follows symlinks; to stat a symlink add the argument
722+
``follow_symlinks=False``, or use :meth:`~Path.lstat`.
723+
721724
::
722725

723726
>>> p = Path('setup.py')
@@ -726,10 +729,18 @@ call fails (for example because the path doesn't exist).
726729
>>> p.stat().st_mtime
727730
1327883547.852554
728731

732+
.. versionchanged:: 3.10
733+
The *follow_symlinks* parameter was added.
734+
735+
.. method:: Path.chmod(mode, *, follow_symlinks=True)
729736

730-
.. method:: Path.chmod(mode)
737+
Change the file mode and permissions, like :func:`os.chmod`.
731738

732-
Change the file mode and permissions, like :func:`os.chmod`::
739+
This method normally follows symlinks. Some Unix flavours support changing
740+
permissions on the symlink itself; on these platforms you may add the
741+
argument ``follow_symlinks=False``, or use :meth:`~Path.lchmod`.
742+
743+
::
733744

734745
>>> p = Path('setup.py')
735746
>>> p.stat().st_mode
@@ -738,6 +749,8 @@ call fails (for example because the path doesn't exist).
738749
>>> p.stat().st_mode
739750
33060
740751

752+
.. versionchanged:: 3.10
753+
The *follow_symlinks* parameter was added.
741754

742755
.. method:: Path.exists()
743756

Lib/pathlib.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,6 @@ class _NormalAccessor(_Accessor):
393393

394394
stat = os.stat
395395

396-
lstat = os.lstat
397-
398396
open = os.open
399397

400398
listdir = os.listdir
@@ -403,12 +401,6 @@ class _NormalAccessor(_Accessor):
403401

404402
chmod = os.chmod
405403

406-
if hasattr(os, "lchmod"):
407-
lchmod = os.lchmod
408-
else:
409-
def lchmod(self, path, mode):
410-
raise NotImplementedError("os.lchmod() not available on this system")
411-
412404
mkdir = os.mkdir
413405

414406
unlink = os.unlink
@@ -1191,12 +1183,12 @@ def resolve(self, strict=False):
11911183
normed = self._flavour.pathmod.normpath(s)
11921184
return self._from_parts((normed,))
11931185

1194-
def stat(self):
1186+
def stat(self, *, follow_symlinks=True):
11951187
"""
11961188
Return the result of the stat() system call on this path, like
11971189
os.stat() does.
11981190
"""
1199-
return self._accessor.stat(self)
1191+
return self._accessor.stat(self, follow_symlinks=follow_symlinks)
12001192

12011193
def owner(self):
12021194
"""
@@ -1286,18 +1278,18 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
12861278
if not exist_ok or not self.is_dir():
12871279
raise
12881280

1289-
def chmod(self, mode):
1281+
def chmod(self, mode, *, follow_symlinks=True):
12901282
"""
12911283
Change the permissions of the path, like os.chmod().
12921284
"""
1293-
self._accessor.chmod(self, mode)
1285+
self._accessor.chmod(self, mode, follow_symlinks=follow_symlinks)
12941286

12951287
def lchmod(self, mode):
12961288
"""
12971289
Like chmod(), except if the path points to a symlink, the symlink's
12981290
permissions are changed, rather than its target's.
12991291
"""
1300-
self._accessor.lchmod(self, mode)
1292+
self.chmod(mode, follow_symlinks=False)
13011293

13021294
def unlink(self, missing_ok=False):
13031295
"""
@@ -1321,7 +1313,7 @@ def lstat(self):
13211313
Like stat(), except if the path points to a symlink, the symlink's
13221314
status information is returned, rather than its target's.
13231315
"""
1324-
return self._accessor.lstat(self)
1316+
return self.stat(follow_symlinks=False)
13251317

13261318
def link_to(self, target):
13271319
"""

Lib/test/test_pathlib.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,6 +1828,21 @@ def test_chmod(self):
18281828
p.chmod(new_mode)
18291829
self.assertEqual(p.stat().st_mode, new_mode)
18301830

1831+
# On Windows, os.chmod does not follow symlinks (issue #15411)
1832+
@only_posix
1833+
def test_chmod_follow_symlinks_true(self):
1834+
p = self.cls(BASE) / 'linkA'
1835+
q = p.resolve()
1836+
mode = q.stat().st_mode
1837+
# Clear writable bit.
1838+
new_mode = mode & ~0o222
1839+
p.chmod(new_mode, follow_symlinks=True)
1840+
self.assertEqual(q.stat().st_mode, new_mode)
1841+
# Set writable bit
1842+
new_mode = mode | 0o222
1843+
p.chmod(new_mode, follow_symlinks=True)
1844+
self.assertEqual(q.stat().st_mode, new_mode)
1845+
18311846
# XXX also need a test for lchmod.
18321847

18331848
def test_stat(self):
@@ -1839,6 +1854,17 @@ def test_stat(self):
18391854
self.addCleanup(p.chmod, st.st_mode)
18401855
self.assertNotEqual(p.stat(), st)
18411856

1857+
@os_helper.skip_unless_symlink
1858+
def test_stat_no_follow_symlinks(self):
1859+
p = self.cls(BASE) / 'linkA'
1860+
st = p.stat()
1861+
self.assertNotEqual(st, p.stat(follow_symlinks=False))
1862+
1863+
def test_stat_no_follow_symlinks_nosymlink(self):
1864+
p = self.cls(BASE) / 'fileA'
1865+
st = p.stat()
1866+
self.assertEqual(st, p.stat(follow_symlinks=False))
1867+
18421868
@os_helper.skip_unless_symlink
18431869
def test_lstat(self):
18441870
p = self.cls(BASE)/ 'linkA'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:meth:`pathlib.Path.stat` and :meth:`~pathlib.Path.chmod` now accept a *follow_symlinks* keyword-only argument for consistency with corresponding functions in the :mod:`os` module.

0 commit comments

Comments
 (0)