From 530f085092e0586e84cddaf5283cbee6e0f9da1b Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Mon, 14 Aug 2023 21:03:54 +0000 Subject: [PATCH 1/9] Add follow_symlinks argument to pathlib.Path.owner() and pathlib.Path.group() --- Lib/pathlib.py | 10 +++-- Lib/test/test_pathlib.py | 79 ++++++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 758f70f5ba7077..aa419ec083be0d 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1252,24 +1252,26 @@ def check_eloop(e): check_eloop(e) return p - def owner(self): + def owner(self, *, follow_symlinks=True): """ Return the login name of the file owner. """ try: import pwd - return pwd.getpwuid(self.stat().st_uid).pw_name + uid = self.stat(follow_symlinks=follow_symlinks).st_uid + return pwd.getpwuid(uid).pw_name except ImportError: raise UnsupportedOperation("Path.owner() is unsupported on this system") - def group(self): + def group(self, *, follow_symlinks=True): """ Return the group name of the file gid. """ try: import grp - return grp.getgrgid(self.stat().st_gid).gr_name + gid = self.stat(follow_symlinks=follow_symlinks).st_gid + return grp.getgrgid(gid).gr_name except ImportError: raise UnsupportedOperation("Path.group() is unsupported on this system") diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 74deec84336f72..3e7c575dfa1bd6 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -30,6 +30,20 @@ def test_is_notimplemented(self): self.assertTrue(isinstance(pathlib.UnsupportedOperation(), NotImplementedError)) +try: + import pwd + all_users = [u.pw_uid for u in pwd.getpwall()] +except (ImportError, AttributeError): + all_users = [] + + +try: + import grp + all_groups = [g.gr_gid for g in grp.getgrall()] +except (ImportError, AttributeError): + all_groups = [] + + # Make sure any symbolic links in the base test path are resolved. BASE = os.path.realpath(TESTFN) join = lambda *x: os.path.join(BASE, *x) @@ -40,6 +54,9 @@ def test_is_notimplemented(self): 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. @@ -2567,27 +2584,67 @@ def test_chmod_follow_symlinks_true(self): # 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") + @unittest.skipUnless(len(all_users) > 1, "test needs more than one user") + def test_owner_no_follow_symlinks(self): + 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) + + expected_uid = link.stat(follow_symlinks=False).st_uid + expected_name = self._get_pw_name_or_skip_test(expected_uid) + + 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") + @unittest.skipUnless(len(all_groups) > 1, "test needs more than one group") + def test_group_no_follow_symlinks(self): + 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) + + expected_gid = link.stat(follow_symlinks=False).st_gid + expected_name = self._get_pw_name_or_skip_test(expected_gid) + + self.assertEqual(expected_name, link.owner(follow_symlinks=False)) def test_unlink(self): p = self.cls(BASE) / 'fileA' From 5b1578c291182c6745ada16c38d02da53c06bf6b Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Mon, 14 Aug 2023 21:19:09 +0000 Subject: [PATCH 2/9] Update pathlib docs --- Doc/library/pathlib.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 22360b22fd924b..1411ee0f092d25 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -981,15 +981,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) @@ -1255,15 +1261,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() From 86a982d43d5f9274f9134d7b62bb6604aa6e783a Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Mon, 14 Aug 2023 21:19:16 +0000 Subject: [PATCH 3/9] Add news entry --- .../next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst 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 index 00000000000000..d4a27d624eb5e6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst @@ -0,0 +1,2 @@ +Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.owner` +and :meth:`~pathlib.Path.group`, defaulting to ``True``. From 77d4d0fcfb8ae17d2dbab0f5999f84ffe2d57320 Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Tue, 15 Aug 2023 08:44:40 +0200 Subject: [PATCH 4/9] Update What's New --- Doc/whatsnew/3.13.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 63cdee6cf1a4f3..36fc4827fefd11 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -140,9 +140,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`). traceback --------- From cebcbc0f82eec4d790eca102f643e7917ec83631 Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Tue, 15 Aug 2023 06:51:32 +0000 Subject: [PATCH 5/9] Use correct method in test --- Lib/test/test_pathlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 3e7c575dfa1bd6..431385a962e67f 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -2644,7 +2644,7 @@ def test_group_no_follow_symlinks(self): expected_gid = link.stat(follow_symlinks=False).st_gid expected_name = self._get_pw_name_or_skip_test(expected_gid) - self.assertEqual(expected_name, link.owner(follow_symlinks=False)) + self.assertEqual(expected_name, link.group(follow_symlinks=False)) def test_unlink(self): p = self.cls(BASE) / 'fileA' From 7296076ae5e0ddfcf4a71cc25d14dbcb26866563 Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Tue, 15 Aug 2023 10:44:57 +0000 Subject: [PATCH 6/9] Add missing follow_symlinks in tests --- Lib/test/test_pathlib.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 431385a962e67f..75ef432516dc76 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -2608,11 +2608,12 @@ def test_owner_no_follow_symlinks(self): uid_1, uid_2 = all_users[:2] os.chown(target, uid_1, -1) - os.chown(link, uid_2, -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): @@ -2639,11 +2640,12 @@ def test_group_no_follow_symlinks(self): gid_1, gid_2 = all_groups[:2] os.chown(target, -1, gid_1) - os.chown(link, -1, gid_2) + 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): From 9077bc302164217a756afabd4d25c61d6fabd296 Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Sat, 2 Dec 2023 10:31:36 +0000 Subject: [PATCH 7/9] Move all_users and all_groups into tests --- Lib/test/test_pathlib.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 75ef432516dc76..3d5e102b703cd0 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -30,20 +30,6 @@ def test_is_notimplemented(self): self.assertTrue(isinstance(pathlib.UnsupportedOperation(), NotImplementedError)) -try: - import pwd - all_users = [u.pw_uid for u in pwd.getpwall()] -except (ImportError, AttributeError): - all_users = [] - - -try: - import grp - all_groups = [g.gr_gid for g in grp.getgrall()] -except (ImportError, AttributeError): - all_groups = [] - - # Make sure any symbolic links in the base test path are resolved. BASE = os.path.realpath(TESTFN) join = lambda *x: os.path.join(BASE, *x) @@ -2601,8 +2587,11 @@ def test_owner(self): @unittest.skipUnless(pwd, "the pwd module is needed for this test") @unittest.skipUnless(root_in_posix, "test needs root privilege") - @unittest.skipUnless(len(all_users) > 1, "test needs more than one user") 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' @@ -2633,8 +2622,11 @@ def test_group(self): @unittest.skipUnless(grp, "the grp module is needed for this test") @unittest.skipUnless(root_in_posix, "test needs root privilege") - @unittest.skipUnless(len(all_groups) > 1, "test needs more than one group") 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' From 49d716e6d05bb8aff99cce17d144b3b188e7c2bb Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Sat, 2 Dec 2023 10:47:31 +0000 Subject: [PATCH 8/9] Bring kwards back --- Lib/pathlib.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 81f75cd47ed087..5bf1695d12cc46 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1440,18 +1440,20 @@ def resolve(self, strict=False): 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): From 9279158f366ae7d7a2803a130f602ab65ec496be Mon Sep 17 00:00:00 2001 From: Kamil Turek Date: Sat, 2 Dec 2023 10:49:21 +0000 Subject: [PATCH 9/9] Add kwargs to _PathBase --- Lib/pathlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 5bf1695d12cc46..b728a0b3dfdb6c 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1319,13 +1319,13 @@ def rmdir(self): """ 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. """