]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-39906: Add follow_symlinks parameter to pathlib.Path.stat() and chmod() (GH-18864)
authorBarney Gale <barney.gale@gmail.com>
Wed, 7 Apr 2021 15:53:39 +0000 (16:53 +0100)
committerGitHub <noreply@github.com>
Wed, 7 Apr 2021 15:53:39 +0000 (16:53 +0100)
Doc/library/pathlib.rst
Lib/pathlib.py
Lib/test/test_pathlib.py
Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst [new file with mode: 0644]

index ac96de334b329062ec30b970d80ec766b1d5ec70..b1cfbeda60195489de85d1d319bb4c47582500cf 100644 (file)
@@ -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()
 
index 9db8ae2d8a389e70e63614882944017c054cd1fd..eaaf980631fc6949305a7e8a05a111c9d0946c90 100644 (file)
@@ -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 @@ class Path(PurePath):
         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 @@ class Path(PurePath):
             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 @@ class Path(PurePath):
         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):
         """
index 9be72941d33544e5f16d3dc2ff4efe20cc6b7139..26431193527909eaef54588770b218974b220e50 100644 (file)
@@ -1828,6 +1828,21 @@ class _BasePathTest(object):
         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 @@ class _BasePathTest(object):
         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 (file)
index 0000000..dacefb7
--- /dev/null
@@ -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