Skip to content

Commit

Permalink
bpo-39906: Add follow_symlinks parameter to pathlib.Path.stat() and c…
Browse files Browse the repository at this point in the history
…hmod() (pythonGH-18864)
  • Loading branch information
barneygale authored Apr 7, 2021
1 parent 7a7ba3d commit abf9649
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 17 deletions.
19 changes: 16 additions & 3 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -713,11 +713,14 @@ call fails (for example because the path doesn't exist).
.. versionadded:: 3.5


.. method:: Path.stat()
.. method:: Path.stat(*, follow_symlinks=True)

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

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

::

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

.. versionchanged:: 3.10
The *follow_symlinks* parameter was added.

.. method:: Path.chmod(mode, *, follow_symlinks=True)

.. method:: Path.chmod(mode)
Change the file mode and permissions, like :func:`os.chmod`.

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

::

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

.. versionchanged:: 3.10
The *follow_symlinks* parameter was added.

.. method:: Path.exists()

Expand Down
20 changes: 6 additions & 14 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,6 @@ class _NormalAccessor(_Accessor):

stat = os.stat

lstat = os.lstat

open = os.open

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

chmod = os.chmod

if hasattr(os, "lchmod"):
lchmod = os.lchmod
else:
def lchmod(self, path, mode):
raise NotImplementedError("os.lchmod() not available on this system")

mkdir = os.mkdir

unlink = os.unlink
Expand Down Expand Up @@ -1191,12 +1183,12 @@ def resolve(self, strict=False):
normed = self._flavour.pathmod.normpath(s)
return self._from_parts((normed,))

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

def owner(self):
"""
Expand Down Expand Up @@ -1286,18 +1278,18 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
if not exist_ok or not self.is_dir():
raise

def chmod(self, mode):
def chmod(self, mode, *, follow_symlinks=True):
"""
Change the permissions of the path, like os.chmod().
"""
self._accessor.chmod(self, mode)
self._accessor.chmod(self, mode, follow_symlinks=follow_symlinks)

def lchmod(self, mode):
"""
Like chmod(), except if the path points to a symlink, the symlink's
permissions are changed, rather than its target's.
"""
self._accessor.lchmod(self, mode)
self.chmod(mode, follow_symlinks=False)

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

def link_to(self, target):
"""
Expand Down
26 changes: 26 additions & 0 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1828,6 +1828,21 @@ def test_chmod(self):
p.chmod(new_mode)
self.assertEqual(p.stat().st_mode, new_mode)

# On Windows, os.chmod does not follow symlinks (issue #15411)
@only_posix
def test_chmod_follow_symlinks_true(self):
p = self.cls(BASE) / 'linkA'
q = p.resolve()
mode = q.stat().st_mode
# Clear writable bit.
new_mode = mode & ~0o222
p.chmod(new_mode, follow_symlinks=True)
self.assertEqual(q.stat().st_mode, new_mode)
# Set writable bit
new_mode = mode | 0o222
p.chmod(new_mode, follow_symlinks=True)
self.assertEqual(q.stat().st_mode, new_mode)

# XXX also need a test for lchmod.

def test_stat(self):
Expand All @@ -1839,6 +1854,17 @@ def test_stat(self):
self.addCleanup(p.chmod, st.st_mode)
self.assertNotEqual(p.stat(), st)

@os_helper.skip_unless_symlink
def test_stat_no_follow_symlinks(self):
p = self.cls(BASE) / 'linkA'
st = p.stat()
self.assertNotEqual(st, p.stat(follow_symlinks=False))

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

@os_helper.skip_unless_symlink
def test_lstat(self):
p = self.cls(BASE)/ 'linkA'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +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 comments on commit abf9649

Please sign in to comment.