Skip to content

gh-64978: Add chown() to pathlib.Path #31212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This method normally follows symlinks. Some Unix flavours support changing
This method follows symlinks by default. 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::
Expand Down Expand Up @@ -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.


Expand Down Expand Up @@ -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`
Expand Down
9 changes: 9 additions & 0 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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().
"""
Comment on lines +1131 to +1133
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow PEP 8.

Suggested change
"""
Change the owner and group id of path to the numeric uid and gid, like os.chown().
"""
"""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.
Expand Down Expand Up @@ -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")
53 changes: 53 additions & 0 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Comment on lines +24 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks to be a copy-and-paste from test_os.py, correct? If so, can you move this to https://github.com/python/cpython/tree/main/Lib/test/support somewhere?


root_in_posix = False
if hasattr(os, 'geteuid'):
root_in_posix = (os.geteuid() == 0)
Comment on lines +30 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
root_in_posix = False
if hasattr(os, 'geteuid'):
root_in_posix = (os.geteuid() == 0)
try:
root_in_posix = not os.geteuid()
except AttributeError:
root_in_posix = False

This looks to be a copy-and-paste from test_os.py, correct? If so, can you move this to https://github.com/python/cpython/tree/main/Lib/test/support somewhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can move it and use it from there as a helper module ...


class _BaseFlavourTest(object):

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow PEP 8.

Suggested change
# original uid and gid
# The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# get users and groups for testing
# 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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :meth:`pathlib.Path.chown` method that changes file ownership.
Patch by Jaspar Stach.