Skip to content

Commit a1551b4

Browse files
authored
gh-103363: Add follow_symlinks argument to pathlib.Path.owner() and group() (#107962)
1 parent 2ed20d3 commit a1551b4

File tree

5 files changed

+93
-24
lines changed

5 files changed

+93
-24
lines changed

Doc/library/pathlib.rst

+16-4
Original file line numberDiff line numberDiff line change
@@ -1017,15 +1017,21 @@ call fails (for example because the path doesn't exist).
10171017
future Python release, patterns with this ending will match both files
10181018
and directories. Add a trailing slash to match only directories.
10191019

1020-
.. method:: Path.group()
1020+
.. method:: Path.group(*, follow_symlinks=True)
10211021

1022-
Return the name of the group owning the file. :exc:`KeyError` is raised
1022+
Return the name of the group owning the file. :exc:`KeyError` is raised
10231023
if the file's gid isn't found in the system database.
10241024

1025+
This method normally follows symlinks; to get the group of the symlink, add
1026+
the argument ``follow_symlinks=False``.
1027+
10251028
.. versionchanged:: 3.13
10261029
Raises :exc:`UnsupportedOperation` if the :mod:`grp` module is not
10271030
available. In previous versions, :exc:`NotImplementedError` was raised.
10281031

1032+
.. versionchanged:: 3.13
1033+
The *follow_symlinks* parameter was added.
1034+
10291035

10301036
.. method:: Path.is_dir(*, follow_symlinks=True)
10311037

@@ -1291,15 +1297,21 @@ call fails (for example because the path doesn't exist).
12911297
'#!/usr/bin/env python3\n'
12921298

12931299

1294-
.. method:: Path.owner()
1300+
.. method:: Path.owner(*, follow_symlinks=True)
12951301

1296-
Return the name of the user owning the file. :exc:`KeyError` is raised
1302+
Return the name of the user owning the file. :exc:`KeyError` is raised
12971303
if the file's uid isn't found in the system database.
12981304

1305+
This method normally follows symlinks; to get the owner of the symlink, add
1306+
the argument ``follow_symlinks=False``.
1307+
12991308
.. versionchanged:: 3.13
13001309
Raises :exc:`UnsupportedOperation` if the :mod:`pwd` module is not
13011310
available. In previous versions, :exc:`NotImplementedError` was raised.
13021311

1312+
.. versionchanged:: 3.13
1313+
The *follow_symlinks* parameter was added.
1314+
13031315

13041316
.. method:: Path.read_bytes()
13051317

Doc/whatsnew/3.13.rst

+5-3
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,11 @@ pathlib
270270
(Contributed by Barney Gale in :gh:`73435`.)
271271

272272
* Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.glob`,
273-
:meth:`~pathlib.Path.rglob`, :meth:`~pathlib.Path.is_file`, and
274-
:meth:`~pathlib.Path.is_dir`.
275-
(Contributed by Barney Gale in :gh:`77609` and :gh:`105793`.)
273+
:meth:`~pathlib.Path.rglob`, :meth:`~pathlib.Path.is_file`,
274+
:meth:`~pathlib.Path.is_dir`, :meth:`~pathlib.Path.owner`,
275+
:meth:`~pathlib.Path.group`.
276+
(Contributed by Barney Gale in :gh:`77609` and :gh:`105793`, and
277+
Kamil Turek in :gh:`107962`).
276278

277279
pdb
278280
---

Lib/pathlib.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -1319,13 +1319,13 @@ def rmdir(self):
13191319
"""
13201320
self._unsupported("rmdir")
13211321

1322-
def owner(self):
1322+
def owner(self, *, follow_symlinks=True):
13231323
"""
13241324
Return the login name of the file owner.
13251325
"""
13261326
self._unsupported("owner")
13271327

1328-
def group(self):
1328+
def group(self, *, follow_symlinks=True):
13291329
"""
13301330
Return the group name of the file gid.
13311331
"""
@@ -1440,18 +1440,20 @@ def resolve(self, strict=False):
14401440
return self.with_segments(os.path.realpath(self, strict=strict))
14411441

14421442
if pwd:
1443-
def owner(self):
1443+
def owner(self, *, follow_symlinks=True):
14441444
"""
14451445
Return the login name of the file owner.
14461446
"""
1447-
return pwd.getpwuid(self.stat().st_uid).pw_name
1447+
uid = self.stat(follow_symlinks=follow_symlinks).st_uid
1448+
return pwd.getpwuid(uid).pw_name
14481449

14491450
if grp:
1450-
def group(self):
1451+
def group(self, *, follow_symlinks=True):
14511452
"""
14521453
Return the group name of the file gid.
14531454
"""
1454-
return grp.getgrgid(self.stat().st_gid).gr_name
1455+
gid = self.stat(follow_symlinks=follow_symlinks).st_gid
1456+
return grp.getgrgid(gid).gr_name
14551457

14561458
if hasattr(os, "readlink"):
14571459
def readlink(self):

Lib/test/test_pathlib.py

+62-11
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def test_is_notimplemented(self):
4141
only_posix = unittest.skipIf(os.name == 'nt',
4242
'test requires a POSIX-compatible system')
4343

44+
root_in_posix = False
45+
if hasattr(os, 'geteuid'):
46+
root_in_posix = (os.geteuid() == 0)
4447

4548
#
4649
# Tests for the pure classes.
@@ -2975,27 +2978,75 @@ def test_chmod_follow_symlinks_true(self):
29752978

29762979
# XXX also need a test for lchmod.
29772980

2978-
@unittest.skipUnless(pwd, "the pwd module is needed for this test")
2979-
def test_owner(self):
2980-
p = self.cls(BASE) / 'fileA'
2981-
uid = p.stat().st_uid
2981+
def _get_pw_name_or_skip_test(self, uid):
29822982
try:
2983-
name = pwd.getpwuid(uid).pw_name
2983+
return pwd.getpwuid(uid).pw_name
29842984
except KeyError:
29852985
self.skipTest(
29862986
"user %d doesn't have an entry in the system database" % uid)
2987-
self.assertEqual(name, p.owner())
29882987

2989-
@unittest.skipUnless(grp, "the grp module is needed for this test")
2990-
def test_group(self):
2988+
@unittest.skipUnless(pwd, "the pwd module is needed for this test")
2989+
def test_owner(self):
29912990
p = self.cls(BASE) / 'fileA'
2992-
gid = p.stat().st_gid
2991+
expected_uid = p.stat().st_uid
2992+
expected_name = self._get_pw_name_or_skip_test(expected_uid)
2993+
2994+
self.assertEqual(expected_name, p.owner())
2995+
2996+
@unittest.skipUnless(pwd, "the pwd module is needed for this test")
2997+
@unittest.skipUnless(root_in_posix, "test needs root privilege")
2998+
def test_owner_no_follow_symlinks(self):
2999+
all_users = [u.pw_uid for u in pwd.getpwall()]
3000+
if len(all_users) < 2:
3001+
self.skipTest("test needs more than one user")
3002+
3003+
target = self.cls(BASE) / 'fileA'
3004+
link = self.cls(BASE) / 'linkA'
3005+
3006+
uid_1, uid_2 = all_users[:2]
3007+
os.chown(target, uid_1, -1)
3008+
os.chown(link, uid_2, -1, follow_symlinks=False)
3009+
3010+
expected_uid = link.stat(follow_symlinks=False).st_uid
3011+
expected_name = self._get_pw_name_or_skip_test(expected_uid)
3012+
3013+
self.assertEqual(expected_uid, uid_2)
3014+
self.assertEqual(expected_name, link.owner(follow_symlinks=False))
3015+
3016+
def _get_gr_name_or_skip_test(self, gid):
29933017
try:
2994-
name = grp.getgrgid(gid).gr_name
3018+
return grp.getgrgid(gid).gr_name
29953019
except KeyError:
29963020
self.skipTest(
29973021
"group %d doesn't have an entry in the system database" % gid)
2998-
self.assertEqual(name, p.group())
3022+
3023+
@unittest.skipUnless(grp, "the grp module is needed for this test")
3024+
def test_group(self):
3025+
p = self.cls(BASE) / 'fileA'
3026+
expected_gid = p.stat().st_gid
3027+
expected_name = self._get_gr_name_or_skip_test(expected_gid)
3028+
3029+
self.assertEqual(expected_name, p.group())
3030+
3031+
@unittest.skipUnless(grp, "the grp module is needed for this test")
3032+
@unittest.skipUnless(root_in_posix, "test needs root privilege")
3033+
def test_group_no_follow_symlinks(self):
3034+
all_groups = [g.gr_gid for g in grp.getgrall()]
3035+
if len(all_groups) < 2:
3036+
self.skipTest("test needs more than one group")
3037+
3038+
target = self.cls(BASE) / 'fileA'
3039+
link = self.cls(BASE) / 'linkA'
3040+
3041+
gid_1, gid_2 = all_groups[:2]
3042+
os.chown(target, -1, gid_1)
3043+
os.chown(link, -1, gid_2, follow_symlinks=False)
3044+
3045+
expected_gid = link.stat(follow_symlinks=False).st_gid
3046+
expected_name = self._get_pw_name_or_skip_test(expected_gid)
3047+
3048+
self.assertEqual(expected_gid, gid_2)
3049+
self.assertEqual(expected_name, link.group(follow_symlinks=False))
29993050

30003051
def test_unlink(self):
30013052
p = self.cls(BASE) / 'fileA'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.owner`
2+
and :meth:`~pathlib.Path.group`, defaulting to ``True``.

0 commit comments

Comments
 (0)