]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-103363: Add follow_symlinks argument to `pathlib.Path.owner()` and `group()` ...
authorKamil Turek <kamil.turek@hotmail.com>
Mon, 4 Dec 2023 19:42:01 +0000 (20:42 +0100)
committerGitHub <noreply@github.com>
Mon, 4 Dec 2023 19:42:01 +0000 (19:42 +0000)
Doc/library/pathlib.rst
Doc/whatsnew/3.13.rst
Lib/pathlib.py
Lib/test/test_pathlib.py
Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst [new file with mode: 0644]

index 7ecfd120db8d154837c805540fcdbff672ad98f3..62d4ed5e3f46b97c1b191d01ffa65f009b310397 100644 (file)
@@ -1017,15 +1017,21 @@ call fails (for example because the path doesn't exist).
       future Python release, patterns with this ending will match both files
       and directories. Add a trailing slash to match only directories.
 
-.. method:: Path.group()
+.. method:: Path.group(*, follow_symlinks=True)
 
-   Return the name of the group owning the file.  :exc:`KeyError` is raised
+   Return the name of the group owning the file. :exc:`KeyError` is raised
    if the file's gid isn't found in the system database.
 
+   This method normally follows symlinks; to get the group of the symlink, add
+   the argument ``follow_symlinks=False``.
+
    .. versionchanged:: 3.13
       Raises :exc:`UnsupportedOperation` if the :mod:`grp` module is not
       available. In previous versions, :exc:`NotImplementedError` was raised.
 
+   .. versionchanged:: 3.13
+      The *follow_symlinks* parameter was added.
+
 
 .. method:: Path.is_dir(*, follow_symlinks=True)
 
@@ -1291,15 +1297,21 @@ call fails (for example because the path doesn't exist).
       '#!/usr/bin/env python3\n'
 
 
-.. method:: Path.owner()
+.. method:: Path.owner(*, follow_symlinks=True)
 
-   Return the name of the user owning the file.  :exc:`KeyError` is raised
+   Return the name of the user owning the file. :exc:`KeyError` is raised
    if the file's uid isn't found in the system database.
 
+   This method normally follows symlinks; to get the owner of the symlink, add
+   the argument ``follow_symlinks=False``.
+
    .. versionchanged:: 3.13
       Raises :exc:`UnsupportedOperation` if the :mod:`pwd` module is not
       available. In previous versions, :exc:`NotImplementedError` was raised.
 
+   .. versionchanged:: 3.13
+      The *follow_symlinks* parameter was added.
+
 
 .. method:: Path.read_bytes()
 
index be890ff314dfa44965f6d4f05b1bbbf9c11217a5..534813f3659c9d945bab89e117d36f91bee9e150 100644 (file)
@@ -270,9 +270,11 @@ pathlib
   (Contributed by Barney Gale in :gh:`73435`.)
 
 * Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.glob`,
-  :meth:`~pathlib.Path.rglob`, :meth:`~pathlib.Path.is_file`, and
-  :meth:`~pathlib.Path.is_dir`.
-  (Contributed by Barney Gale in :gh:`77609` and :gh:`105793`.)
+  :meth:`~pathlib.Path.rglob`, :meth:`~pathlib.Path.is_file`,
+  :meth:`~pathlib.Path.is_dir`, :meth:`~pathlib.Path.owner`,
+  :meth:`~pathlib.Path.group`.
+  (Contributed by Barney Gale in :gh:`77609` and :gh:`105793`, and
+  Kamil Turek in :gh:`107962`).
 
 pdb
 ---
index 81f75cd47ed08708ab29282863178eb3a076d6e0..b728a0b3dfdb6c422c152925494b4359a412c0a7 100644 (file)
@@ -1319,13 +1319,13 @@ class _PathBase(PurePath):
         """
         self._unsupported("rmdir")
 
-    def owner(self):
+    def owner(self, *, follow_symlinks=True):
         """
         Return the login name of the file owner.
         """
         self._unsupported("owner")
 
-    def group(self):
+    def group(self, *, follow_symlinks=True):
         """
         Return the group name of the file gid.
         """
@@ -1440,18 +1440,20 @@ class Path(_PathBase):
         return self.with_segments(os.path.realpath(self, strict=strict))
 
     if pwd:
-        def owner(self):
+        def owner(self, *, follow_symlinks=True):
             """
             Return the login name of the file owner.
             """
-            return pwd.getpwuid(self.stat().st_uid).pw_name
+            uid = self.stat(follow_symlinks=follow_symlinks).st_uid
+            return pwd.getpwuid(uid).pw_name
 
     if grp:
-        def group(self):
+        def group(self, *, follow_symlinks=True):
             """
             Return the group name of the file gid.
             """
-            return grp.getgrgid(self.stat().st_gid).gr_name
+            gid = self.stat(follow_symlinks=follow_symlinks).st_gid
+            return grp.getgrgid(gid).gr_name
 
     if hasattr(os, "readlink"):
         def readlink(self):
index ccaef070974ffdc5efd90e6a7ca01666922cc196..1b10d6c2f0cb19034e62fbc4b0fe32d551df9df3 100644 (file)
@@ -41,6 +41,9 @@ only_nt = unittest.skipIf(os.name != 'nt',
 only_posix = unittest.skipIf(os.name == 'nt',
                              'test requires a POSIX-compatible system')
 
+root_in_posix = False
+if hasattr(os, 'geteuid'):
+    root_in_posix = (os.geteuid() == 0)
 
 #
 # Tests for the pure classes.
@@ -2975,27 +2978,75 @@ class PathTest(DummyPathTest, PurePathTest):
 
     # XXX also need a test for lchmod.
 
-    @unittest.skipUnless(pwd, "the pwd module is needed for this test")
-    def test_owner(self):
-        p = self.cls(BASE) / 'fileA'
-        uid = p.stat().st_uid
+    def _get_pw_name_or_skip_test(self, uid):
         try:
-            name = pwd.getpwuid(uid).pw_name
+            return pwd.getpwuid(uid).pw_name
         except KeyError:
             self.skipTest(
                 "user %d doesn't have an entry in the system database" % uid)
-        self.assertEqual(name, p.owner())
 
-    @unittest.skipUnless(grp, "the grp module is needed for this test")
-    def test_group(self):
+    @unittest.skipUnless(pwd, "the pwd module is needed for this test")
+    def test_owner(self):
         p = self.cls(BASE) / 'fileA'
-        gid = p.stat().st_gid
+        expected_uid = p.stat().st_uid
+        expected_name = self._get_pw_name_or_skip_test(expected_uid)
+
+        self.assertEqual(expected_name, p.owner())
+
+    @unittest.skipUnless(pwd, "the pwd module is needed for this test")
+    @unittest.skipUnless(root_in_posix, "test needs root privilege")
+    def test_owner_no_follow_symlinks(self):
+        all_users = [u.pw_uid for u in pwd.getpwall()]
+        if len(all_users) < 2:
+            self.skipTest("test needs more than one user")
+
+        target = self.cls(BASE) / 'fileA'
+        link = self.cls(BASE) / 'linkA'
+
+        uid_1, uid_2 = all_users[:2]
+        os.chown(target, uid_1, -1)
+        os.chown(link, uid_2, -1, follow_symlinks=False)
+
+        expected_uid = link.stat(follow_symlinks=False).st_uid
+        expected_name = self._get_pw_name_or_skip_test(expected_uid)
+
+        self.assertEqual(expected_uid, uid_2)
+        self.assertEqual(expected_name, link.owner(follow_symlinks=False))
+
+    def _get_gr_name_or_skip_test(self, gid):
         try:
-            name = grp.getgrgid(gid).gr_name
+            return grp.getgrgid(gid).gr_name
         except KeyError:
             self.skipTest(
                 "group %d doesn't have an entry in the system database" % gid)
-        self.assertEqual(name, p.group())
+
+    @unittest.skipUnless(grp, "the grp module is needed for this test")
+    def test_group(self):
+        p = self.cls(BASE) / 'fileA'
+        expected_gid = p.stat().st_gid
+        expected_name = self._get_gr_name_or_skip_test(expected_gid)
+
+        self.assertEqual(expected_name, p.group())
+
+    @unittest.skipUnless(grp, "the grp module is needed for this test")
+    @unittest.skipUnless(root_in_posix, "test needs root privilege")
+    def test_group_no_follow_symlinks(self):
+        all_groups = [g.gr_gid for g in grp.getgrall()]
+        if len(all_groups) < 2:
+            self.skipTest("test needs more than one group")
+
+        target = self.cls(BASE) / 'fileA'
+        link = self.cls(BASE) / 'linkA'
+
+        gid_1, gid_2 = all_groups[:2]
+        os.chown(target, -1, gid_1)
+        os.chown(link, -1, gid_2, follow_symlinks=False)
+
+        expected_gid = link.stat(follow_symlinks=False).st_gid
+        expected_name = self._get_pw_name_or_skip_test(expected_gid)
+
+        self.assertEqual(expected_gid, gid_2)
+        self.assertEqual(expected_name, link.group(follow_symlinks=False))
 
     def test_unlink(self):
         p = self.cls(BASE) / 'fileA'
diff --git a/Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst b/Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst
new file mode 100644 (file)
index 0000000..d4a27d6
--- /dev/null
@@ -0,0 +1,2 @@
+Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.owner`
+and :meth:`~pathlib.Path.group`, defaulting to ``True``.