diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 14118127835bbe2..20d3a575e9529cd 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -819,6 +819,19 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.10 The *follow_symlinks* parameter was added. +.. method:: Path.chown(owner=None, group=None) + + Change the *owner* and/or *group* of the path. + + *owner* can be a system user name or a uid; the same applies to *group*. + At least one argument is required. + + .. audit-event:: pathlib.Path.chown path,owner,group pathlib.Path.chown + + .. availability:: Unix. + + .. versionadded:: 3.12 + .. method:: Path.exists(*, follow_symlinks=True) Return ``True`` if the path points to an existing file or directory. @@ -1442,6 +1455,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` @@ -1476,4 +1490,5 @@ Below is a table mapping various :mod:`os` functions to their corresponding .. rubric:: Footnotes .. [#] :func:`os.path.abspath` normalizes the resulting path, which may change its meaning in the presence of symlinks, while :meth:`Path.absolute` does not. +.. [#] :meth:`Path.chown` supports owner and group names and IDs, whereas :func:`os.chown` only supports IDs. .. [#] :meth:`PurePath.relative_to` requires ``self`` to be the subpath of the argument, but :func:`os.path.relpath` does not. diff --git a/Lib/pathlib.py b/Lib/pathlib.py index f32e1e2d822834b..3cd626f912c22ac 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1029,6 +1029,38 @@ def lchmod(self, mode): """ self.chmod(mode, follow_symlinks=False) + if hasattr(os, 'chown'): + def chown(self, owner=None, group=None): + """Change the owner and/or group of the path. + + user and group can be the uid/gid or the user/group names, and in that case, + they are converted to their respective uid/gid. + """ + sys.audit('pathlib.Path.chown', self, owner, group) + + if owner is None: + owner = -1 + elif isinstance(owner, str): + try: + import pwd + owner = pwd.getpwnam(owner)[2] + except (ImportError, KeyError): + raise LookupError(f"no such user: {owner!r}") from None + + if group is None: + group = -1 + elif isinstance(group, str): + try: + import grp + group = grp.getgrnam(group)[2] + except (ImportError, KeyError): + raise LookupError(f"no such group: {group!r}") from None + + if owner == -1 and group == -1: + raise ValueError("user and/or group must be set") + + os.chown(self, owner, group) + def unlink(self, missing_ok=False): """ Remove this file or link. diff --git a/Lib/shutil.py b/Lib/shutil.py index 7d1a3d00011f377..022f06ea57a121e 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -10,6 +10,7 @@ import fnmatch import collections import errno +import pathlib import warnings try: @@ -1380,30 +1381,13 @@ def chown(path, user=None, group=None): they are converted to their respective uid/gid. """ sys.audit('shutil.chown', path, user, group) + if not isinstance(path, pathlib.Path): + path = os.fspath(path) + if isinstance(path, bytes): + path = os.fsdecode(path) + path = pathlib.Path(path) + path.chown(user, group) - if user is None and group is None: - raise ValueError("user and/or group must be set") - - _user = user - _group = group - - # -1 means don't change it - if user is None: - _user = -1 - # user can either be an int (the uid) or a string (the system username) - elif isinstance(user, str): - _user = _get_uid(user) - if _user is None: - raise LookupError("no such user: {!r}".format(user)) - - if group is None: - _group = -1 - elif not isinstance(group, int): - _group = _get_gid(group) - if _group is None: - raise LookupError("no such group: {!r}".format(group)) - - os.chown(path, _user, _group) def get_terminal_size(fallback=(80, 24)): """Get the size of the terminal window. diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index a932e03df4236d0..f96d726836b30ac 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -3078,6 +3078,67 @@ def test_handling_bad_descriptor(self): self.fail("Bad file descriptor not handled.") raise + @unittest.skipUnless(pwd, "the pwd module is needed for this test") + @unittest.skipUnless(grp, "the grp module is needed for this test") + @unittest.skipUnless(hasattr(os, 'chown'), 'requires os.chown') + def test_chown(self): + dirname = self.cls(BASE) + filename = dirname / "fileA" + + with self.assertRaises(ValueError): + filename.chown() + + with self.assertRaises(LookupError): + filename.chown(user='non-existing username') + + with self.assertRaises(LookupError): + filename.chown(group='non-existing groupname') + + with self.assertRaises(TypeError): + filename.chown(b'spam') + + with self.assertRaises(TypeError): + filename.chown(3.14) + + uid = os.getuid() + gid = os.getgid() + + def check_chown(path, uid=None, gid=None): + s = path.stat() + if uid is not None: + self.assertEqual(uid, s.st_uid) + if gid is not None: + self.assertEqual(gid, s.st_gid) + + filename.chown(uid, gid) + check_chown(filename, uid, gid) + filename.chown(uid) + check_chown(filename, uid) + filename.chown(user=uid) + check_chown(filename, uid) + filename.chown(group=gid) + check_chown(filename, gid=gid) + + dirname.chown(uid, gid) + check_chown(dirname, uid, gid) + dirname.chown(uid) + check_chown(dirname, uid) + dirname.chown(user=uid) + check_chown(dirname, uid) + dirname.chown(group=gid) + check_chown(dirname, gid=gid) + + try: + user = pwd.getpwuid(uid)[0] + group = grp.getgrgid(gid)[0] + except KeyError: + # On some systems uid/gid cannot be resolved. + pass + else: + filename.chown(user, group) + check_chown(filename, uid, gid) + dirname.chown(user, group) + check_chown(dirname, uid, gid) @only_nt class WindowsPathTest(_BasePathTest, unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2023-05-04-23-22-33.gh-issue-64978.lwBZ3C.rst b/Misc/NEWS.d/next/Library/2023-05-04-23-22-33.gh-issue-64978.lwBZ3C.rst new file mode 100644 index 000000000000000..9d9e04ef3d3e334 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-05-04-23-22-33.gh-issue-64978.lwBZ3C.rst @@ -0,0 +1,2 @@ +Add :meth:`pathlib.Path.chown`, which changes the owner and/or group of a +path.