diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 7ab603fd133b86..eb526d3437c8da 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -755,6 +755,32 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.10 The *follow_symlinks* parameter was added. +.. method:: Path.chown(uid, gid, *, follow_symlinks=True) + + Change the file ownership, like :func:`os.chown`. + + 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``. + + :: + + >>> p = Path('setup.py') + >>> p.stat().st_uid + 1000 + >>> p.stat().st_gid + 1000 + >>> p.chown(1, 20) + >>> p.stat().st_uid + 1 + >>> p.stat().st_gid + 20 + + .. availability:: Unix. + + .. versionadded:: 3.12 + + .. method:: Path.exists() Whether the path points to an existing file or directory:: @@ -925,7 +951,7 @@ call fails (for example because the path doesn't exist). .. method:: Path.lstat() - Like :meth:`Path.stat` but, if the path points to a symbolic link, return + Like :meth:`Path.stat`, but if the path points to a symbolic link, return the symbolic link's information rather than its target's. @@ -1260,6 +1286,7 @@ Below is a table mapping various :mod:`os` functions to their corresponding :func:`os.path.abspath` :meth:`Path.absolute` [#]_ :func:`os.path.realpath` :meth:`Path.resolve` :func:`os.chmod` :meth:`Path.chmod` +:func:`os.chown` :meth:`Path.chown` :func:`os.mkdir` :meth:`Path.mkdir` :func:`os.makedirs` :meth:`Path.mkdir` :func:`os.rename` :meth:`Path.rename` diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 7f4210e2b80c9b..18acf587e8bfda 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1127,6 +1127,12 @@ def lchmod(self, mode): """ self.chmod(mode, follow_symlinks=False) + def chown(self, uid, gid, *, follow_symlinks=True): + """ + Change the owner and group id of path to the numeric uid and gid, like os.chown(). + """ + os.chown(self, uid, gid, follow_symlinks=follow_symlinks) + def unlink(self, missing_ok=False): """ Remove this file or link. @@ -1393,3 +1399,6 @@ class WindowsPath(Path, PureWindowsPath): def is_mount(self): raise NotImplementedError("Path.is_mount() is unsupported on this system") + + def chown(self, uid, gid, *, follow_symlinks=True): + raise NotImplementedError("Path.chown() is unsupported on this system") diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index ec2baca18fd817..56362e6182dd7f 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -21,6 +21,15 @@ except ImportError: grp = pwd = None +try: + import pwd + all_users = [u.pw_uid for u in pwd.getpwall()] +except (ImportError, AttributeError): + all_users = [] + +root_in_posix = False +if hasattr(os, 'geteuid'): + root_in_posix = (os.geteuid() == 0) class _BaseFlavourTest(object): @@ -1874,6 +1883,50 @@ def test_chmod(self): p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) + @unittest.skipUnless(root_in_posix and len(all_users) > 1, + "test needs root privilege and more than one user") + def test_chown_with_root(self): + # original uid and gid + p = self.cls(BASE) / 'fileA' + uid = p.stat().st_uid + gid = p.stat().st_gid + + # get users and groups for testing + uid_1, uid_2 = all_users[:2] + groups = os.getgroups() + if len(groups) < 2: + self.skipTest("test needs at least 2 groups") + gid_1, gid_2 = groups[:2] + + p.chown(uid=uid_1, gid=gid_1) + self.assertEqual(p.stat().st_uid, uid_1) + self.assertEqual(p.stat().st_gid, gid_1) + p.chown(uid=uid_2, gid=gid_2) + self.assertEqual(p.stat().st_uid, uid_2) + self.assertEqual(p.stat().st_gid, gid_2) + + # Set back to original + p.chown(uid=uid, gid=gid) + + @unittest.skipUnless(not root_in_posix and len(all_users) > 1, + "test needs non-root account and more than one user") + def test_chown_without_permission(self): + p = self.cls(BASE) / 'fileA' + + new_uid = 503 + new_gid = 503 + with self.assertRaises(PermissionError): + p.chown(uid=new_uid, gid=new_gid) + + @only_nt + def test_chown_windows(self): + p = self.cls(BASE) / 'fileA' + + new_uid = 503 + new_gid = 503 + with self.assertRaises(NotImplementedError): + p.chown(uid=new_uid, gid=new_gid) + # On Windows, os.chmod does not follow symlinks (issue #15411) @only_posix def test_chmod_follow_symlinks_true(self): diff --git a/Misc/NEWS.d/next/Library/2022-02-08-12-26-20.bpo-20779.Gha5Fa.rst b/Misc/NEWS.d/next/Library/2022-02-08-12-26-20.bpo-20779.Gha5Fa.rst new file mode 100644 index 00000000000000..d5e52de5ae15e7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-02-08-12-26-20.bpo-20779.Gha5Fa.rst @@ -0,0 +1,2 @@ +Add :meth:`pathlib.Path.chown` method that changes file ownership. +Patch by Jaspar Stach.