8000 gh-103363: Add follow_symlinks argument to `pathlib.Path.owner()` and `group()` by kamilturek · Pull Request #107962 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-103363: Add follow_symlinks argument to pathlib.Path.owner() and group() #107962

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

Merged
merged 10 commits into from
Dec 4, 2023
Merged
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
20 changes: 16 additions & 4 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1017,15 +1017,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)

Expand Down Expand Up @@ -1291,15 +1297,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()

Expand Down
8 changes: 5 additions & 3 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,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`).

pdb
---
Expand Down
14 changes: 8 additions & 6 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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):
Expand Down
73 changes: 62 additions & 11 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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.
Expand Down Expand Up @@ -2975,27 +2978,75 @@ 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")
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'

uid_1, uid_2 = all_users[:2]
os.chown(target, uid_1, -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):
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")
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'

gid_1, gid_2 = all_groups[:2]
os.chown(target, -1, gid_1)
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):
p = self.cls(BASE) / 'fileA'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.owner`
and :meth:`~pathlib.Path.group`, defaulting to ``True``.
0