From 163742d3a20f0ad46f99f9b114712181ec92cde3 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 30 Mar 2020 02:25:08 +0100 Subject: [PATCH] bpo-39906: add follow_symlinks parameter to pathlib.Path.stat() and chmod() --- Doc/library/pathlib.rst | 19 +++++++++++--- Lib/pathlib.py | 20 +++++--------- Lib/test/test_pathlib.py | 26 +++++++++++++++++++ .../2020-03-30-00-13-27.bpo-39906.eaR3fN.rst | 1 + 4 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index ac96de334b3290..b1cfbeda601954 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -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') @@ -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 @@ -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() diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 9db8ae2d8a389e..eaaf980631fc69 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -393,8 +393,6 @@ class _NormalAccessor(_Accessor): stat = os.stat - lstat = os.lstat - open = os.open listdir = os.listdir @@ -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 @@ -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): """ @@ -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): """ @@ -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): """ diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 9be72941d33544..26431193527909 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -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): @@ -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' diff --git a/Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst b/Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst new file mode 100644 index 00000000000000..dacefb73d93dd8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst @@ -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. \ No newline at end of file