Skip to content

Commit

Permalink
pythonGH-64978: Add pathlib.Path.chown()
Browse files Browse the repository at this point in the history
Move implementation from `shutil.chown()`. This function now calls through
to pathlib.
  • Loading branch information
barneygale committed May 4, 2023
1 parent d47cddf commit 6f45e76
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 23 deletions.
15 changes: 15 additions & 0 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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.
32 changes: 32 additions & 0 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
owner 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.
Expand Down
32 changes: 9 additions & 23 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import fnmatch
import collections
import errno
import pathlib
import warnings

try:
Expand Down Expand Up @@ -1381,29 +1382,14 @@ def chown(path, user=None, group=None):
"""
sys.audit('shutil.chown', path, 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)
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)


def get_terminal_size(fallback=(80, 24)):
"""Get the size of the terminal window.
Expand Down
61 changes: 61 additions & 0 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :meth:`pathlib.Path.chown`, which changes the owner and/or group of a
path.

0 comments on commit 6f45e76

Please sign in to comment.