bpo-39906: Add follow_symlinks parameter to pathlib.Path.stat() and chmod() (GH-18864)

This commit is contained in:
Barney Gale 2021-04-07 16:53:39 +01:00 committed by GitHub
parent 7a7ba3d343
commit abf964942f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 17 deletions

View File

@ -713,11 +713,14 @@ call fails (for example because the path doesn't exist).
.. versionadded:: 3.5 .. 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`. 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. 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') >>> p = Path('setup.py')
@ -726,10 +729,18 @@ call fails (for example because the path doesn't exist).
>>> p.stat().st_mtime >>> p.stat().st_mtime
1327883547.852554 1327883547.852554
.. versionchanged:: 3.10
The *follow_symlinks* parameter was added.
.. method:: Path.chmod(mode) .. method:: Path.chmod(mode, *, follow_symlinks=True)
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 = Path('setup.py')
>>> p.stat().st_mode >>> p.stat().st_mode
@ -738,6 +749,8 @@ call fails (for example because the path doesn't exist).
>>> p.stat().st_mode >>> p.stat().st_mode
33060 33060
.. versionchanged:: 3.10
The *follow_symlinks* parameter was added.
.. method:: Path.exists() .. method:: Path.exists()

View File

@ -393,8 +393,6 @@ class _NormalAccessor(_Accessor):
stat = os.stat stat = os.stat
lstat = os.lstat
open = os.open open = os.open
listdir = os.listdir listdir = os.listdir
@ -403,12 +401,6 @@ class _NormalAccessor(_Accessor):
chmod = os.chmod 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 mkdir = os.mkdir
unlink = os.unlink unlink = os.unlink
@ -1191,12 +1183,12 @@ class Path(PurePath):
normed = self._flavour.pathmod.normpath(s) normed = self._flavour.pathmod.normpath(s)
return self._from_parts((normed,)) 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 Return the result of the stat() system call on this path, like
os.stat() does. os.stat() does.
""" """
return self._accessor.stat(self) return self._accessor.stat(self, follow_symlinks=follow_symlinks)
def owner(self): def owner(self):
""" """
@ -1286,18 +1278,18 @@ class Path(PurePath):
if not exist_ok or not self.is_dir(): if not exist_ok or not self.is_dir():
raise raise
def chmod(self, mode): def chmod(self, mode, *, follow_symlinks=True):
""" """
Change the permissions of the path, like os.chmod(). 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): def lchmod(self, mode):
""" """
Like chmod(), except if the path points to a symlink, the symlink's Like chmod(), except if the path points to a symlink, the symlink's
permissions are changed, rather than its target'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): def unlink(self, missing_ok=False):
""" """
@ -1321,7 +1313,7 @@ class Path(PurePath):
Like stat(), except if the path points to a symlink, the symlink's Like stat(), except if the path points to a symlink, the symlink's
status information is returned, rather than its target'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): def link_to(self, target):
""" """

View File

@ -1828,6 +1828,21 @@ class _BasePathTest(object):
p.chmod(new_mode) p.chmod(new_mode)
self.assertEqual(p.stat().st_mode, 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. # XXX also need a test for lchmod.
def test_stat(self): def test_stat(self):
@ -1839,6 +1854,17 @@ class _BasePathTest(object):
self.addCleanup(p.chmod, st.st_mode) self.addCleanup(p.chmod, st.st_mode)
self.assertNotEqual(p.stat(), st) 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 @os_helper.skip_unless_symlink
def test_lstat(self): def test_lstat(self):
p = self.cls(BASE)/ 'linkA' p = self.cls(BASE)/ 'linkA'

View File

@ -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.