summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKamil Turek <kamil.turek@hotmail.com>2023-12-04 19:42:01 (GMT)
committerGitHub <noreply@github.com>2023-12-04 19:42:01 (GMT)
commita1551b48eebb4a68fda031b5ee9e5cbde8d924dd (patch)
tree070429ab77861787a01bd43f6de48d3b4789361d
parent2ed20d3bd84fdcf511235cc473a241c5e8278a91 (diff)
downloadcpython-a1551b48eebb4a68fda031b5ee9e5cbde8d924dd.zip
cpython-a1551b48eebb4a68fda031b5ee9e5cbde8d924dd.tar.gz
cpython-a1551b48eebb4a68fda031b5ee9e5cbde8d924dd.tar.bz2
gh-103363: Add follow_symlinks argument to `pathlib.Path.owner()` and `group()` (#107962)
-rw-r--r--Doc/library/pathlib.rst20
-rw-r--r--Doc/whatsnew/3.13.rst8
-rw-r--r--Lib/pathlib.py14
-rw-r--r--Lib/test/test_pathlib.py73
-rw-r--r--Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst2
5 files changed, 93 insertions, 24 deletions
diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst
index 7ecfd12..62d4ed5 100644
--- a/Doc/library/pathlib.rst
+++ b/Doc/library/pathlib.rst
@@ -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)
@@ -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()
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index be890ff..534813f 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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
---
diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index 81f75cd..b728a0b 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -1319,13 +1319,13 @@ class _PathBase(PurePath):
"""
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.
"""
@@ -1440,18 +1440,20 @@ class Path(_PathBase):
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):
diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py
index ccaef07..1b10d6c 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -41,6 +41,9 @@ only_nt = unittest.skipIf(os.name != 'nt',
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.
@@ -2975,27 +2978,75 @@ class PathTest(DummyPathTest, PurePathTest):
# 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'
diff --git a/Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst b/Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst
new file mode 100644
index 0000000..d4a27d6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-08-14-21-10-52.gh-issue-103363.u64_QI.rst
@@ -0,0 +1,2 @@
+Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.owner`
+and :meth:`~pathlib.Path.group`, defaulting to ``True``.