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

This commit is contained in:
Kamil Turek 2023-12-04 20:42:01 +01:00 committed by GitHub
parent 2ed20d3bd8
commit a1551b48ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 95 additions and 26 deletions

View file

@ -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 future Python release, patterns with this ending will match both files
and directories. Add a trailing slash to match only directories. 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. 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 .. versionchanged:: 3.13
Raises :exc:`UnsupportedOperation` if the :mod:`grp` module is not Raises :exc:`UnsupportedOperation` if the :mod:`grp` module is not
available. In previous versions, :exc:`NotImplementedError` was raised. available. In previous versions, :exc:`NotImplementedError` was raised.
.. versionchanged:: 3.13
The *follow_symlinks* parameter was added.
.. method:: Path.is_dir(*, follow_symlinks=True) .. method:: Path.is_dir(*, follow_symlinks=True)
@ -1291,15 +1297,21 @@ call fails (for example because the path doesn't exist).
'#!/usr/bin/env python3\n' '#!/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. 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 .. versionchanged:: 3.13
Raises :exc:`UnsupportedOperation` if the :mod:`pwd` module is not Raises :exc:`UnsupportedOperation` if the :mod:`pwd` module is not
available. In previous versions, :exc:`NotImplementedError` was raised. available. In previous versions, :exc:`NotImplementedError` was raised.
.. versionchanged:: 3.13
The *follow_symlinks* parameter was added.
.. method:: Path.read_bytes() .. method:: Path.read_bytes()

View file

@ -270,9 +270,11 @@ pathlib
(Contributed by Barney Gale in :gh:`73435`.) (Contributed by Barney Gale in :gh:`73435`.)
* Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.glob`, * Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.glob`,
:meth:`~pathlib.Path.rglob`, :meth:`~pathlib.Path.is_file`, and :meth:`~pathlib.Path.rglob`, :meth:`~pathlib.Path.is_file`,
:meth:`~pathlib.Path.is_dir`. :meth:`~pathlib.Path.is_dir`, :meth:`~pathlib.Path.owner`,
(Contributed by Barney Gale in :gh:`77609` and :gh:`105793`.) :meth:`~pathlib.Path.group`.
(Contributed by Barney Gale in :gh:`77609` and :gh:`105793`, and
Kamil Turek in :gh:`107962`).
pdb pdb
--- ---

View file

@ -1319,13 +1319,13 @@ class _PathBase(PurePath):
""" """
self._unsupported("rmdir") self._unsupported("rmdir")
def owner(self): def owner(self, *, follow_symlinks=True):
""" """
Return the login name of the file owner. Return the login name of the file owner.
""" """
self._unsupported("owner") self._unsupported("owner")
def group(self): def group(self, *, follow_symlinks=True):
""" """
Return the group name of the file gid. Return the group name of the file gid.
""" """
@ -1440,18 +1440,20 @@ class Path(_PathBase):
return self.with_segments(os.path.realpath(self, strict=strict)) return self.with_segments(os.path.realpath(self, strict=strict))
if pwd: if pwd:
def owner(self): def owner(self, *, follow_symlinks=True):
""" """
Return the login name of the file owner. 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: if grp:
def group(self): def group(self, *, follow_symlinks=True):
""" """
Return the group name of the file gid. 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"): if hasattr(os, "readlink"):
def readlink(self): def readlink(self):

View file

@ -41,6 +41,9 @@ only_nt = unittest.skipIf(os.name != 'nt',
only_posix = unittest.skipIf(os.name == 'nt', only_posix = unittest.skipIf(os.name == 'nt',
'test requires a POSIX-compatible system') '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. # Tests for the pure classes.
@ -2975,27 +2978,75 @@ class PathTest(DummyPathTest, PurePathTest):
# XXX also need a test for lchmod. # XXX also need a test for lchmod.
@unittest.skipUnless(pwd, "the pwd module is needed for this test") def _get_pw_name_or_skip_test(self, uid):
def test_owner(self):
p = self.cls(BASE) / 'fileA'
uid = p.stat().st_uid
try: try:
name = pwd.getpwuid(uid).pw_name return pwd.getpwuid(uid).pw_name
except KeyError: except KeyError:
self.skipTest( self.skipTest(
"user %d doesn't have an entry in the system database" % uid) "user %d doesn't have an entry in the system database" % uid)
self.assertEqual(name, p.owner())
@unittest.skipUnless(pwd, "the pwd module is needed for this test")
def test_owner(self):
p = self.cls(BASE) / 'fileA'
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:
return grp.getgrgid(gid).gr_name
except KeyError:
self.skipTest(
"group %d doesn't have an entry in the system database" % gid)
@unittest.skipUnless(grp, "the grp module is needed for this test") @unittest.skipUnless(grp, "the grp module is needed for this test")
def test_group(self): def test_group(self):
p = self.cls(BASE) / 'fileA' p = self.cls(BASE) / 'fileA'
gid = p.stat().st_gid expected_gid = p.stat().st_gid
try: expected_name = self._get_gr_name_or_skip_test(expected_gid)
name = grp.getgrgid(gid).gr_name
except KeyError: self.assertEqual(expected_name, p.group())
self.skipTest(
"group %d doesn't have an entry in the system database" % gid) @unittest.skipUnless(grp, "the grp module is needed for this test")
self.assertEqual(name, p.group()) @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): def test_unlink(self):
p = self.cls(BASE) / 'fileA' p = self.cls(BASE) / 'fileA'

View file

@ -0,0 +1,2 @@
Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.owner`
and :meth:`~pathlib.Path.group`, defaulting to ``True``.