summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBarney Gale <barney.gale@gmail.com>2021-04-07 15:53:39 (GMT)
committerGitHub <noreply@github.com>2021-04-07 15:53:39 (GMT)
commitabf964942f97f6489360a75fd57b5e4f41c75f57 (patch)
tree109be2453227ad4cae6204d1f364704527dc3af6
parent7a7ba3d343d360a03a34bc3901628f9f40a58307 (diff)
downloadcpython-abf964942f97f6489360a75fd57b5e4f41c75f57.zip
cpython-abf964942f97f6489360a75fd57b5e4f41c75f57.tar.gz
cpython-abf964942f97f6489360a75fd57b5e4f41c75f57.tar.bz2
bpo-39906: Add follow_symlinks parameter to pathlib.Path.stat() and chmod() (GH-18864)
-rw-r--r--Doc/library/pathlib.rst19
-rw-r--r--Lib/pathlib.py20
-rw-r--r--Lib/test/test_pathlib.py26
-rw-r--r--Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst1
4 files changed, 49 insertions, 17 deletions
diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst
index ac96de3..b1cfbed 100644
--- a/Doc/library/pathlib.rst
+++ b/Doc/library/pathlib.rst
@@ -713,11 +713,14 @@ call fails (for example because the path doesn't exist).
.. versionadded:: 3.5
-.. method:: Path.stat()
+.. method:: Path.stat(*, follow_symlinks=True)
Return a :class:`os.stat_result` object containing information about this path, like :func:`os.stat`.
The result is looked up at each call to this method.
+ This method normally follows symlinks; to stat a symlink add the argument
+ ``follow_symlinks=False``, or use :meth:`~Path.lstat`.
+
::
>>> p = Path('setup.py')
@@ -726,10 +729,18 @@ call fails (for example because the path doesn't exist).
>>> p.stat().st_mtime
1327883547.852554
+ .. versionchanged:: 3.10
+ The *follow_symlinks* parameter was added.
+
+.. method:: Path.chmod(mode, *, follow_symlinks=True)
-.. method:: Path.chmod(mode)
+ Change the file mode and permissions, like :func:`os.chmod`.
- Change the file mode and permissions, like :func:`os.chmod`::
+ This method normally follows symlinks. Some Unix flavours support changing
+ permissions on the symlink itself; on these platforms you may add the
+ argument ``follow_symlinks=False``, or use :meth:`~Path.lchmod`.
+
+ ::
>>> p = Path('setup.py')
>>> p.stat().st_mode
@@ -738,6 +749,8 @@ call fails (for example because the path doesn't exist).
>>> p.stat().st_mode
33060
+ .. versionchanged:: 3.10
+ The *follow_symlinks* parameter was added.
.. method:: Path.exists()
diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index 9db8ae2..eaaf980 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -393,8 +393,6 @@ class _NormalAccessor(_Accessor):
stat = os.stat
- lstat = os.lstat
-
open = os.open
listdir = os.listdir
@@ -403,12 +401,6 @@ class _NormalAccessor(_Accessor):
chmod = os.chmod
- if hasattr(os, "lchmod"):
- lchmod = os.lchmod
- else:
- def lchmod(self, path, mode):
- raise NotImplementedError("os.lchmod() not available on this system")
-
mkdir = os.mkdir
unlink = os.unlink
@@ -1191,12 +1183,12 @@ class Path(PurePath):
normed = self._flavour.pathmod.normpath(s)
return self._from_parts((normed,))
- def stat(self):
+ def stat(self, *, follow_symlinks=True):
"""
Return the result of the stat() system call on this path, like
os.stat() does.
"""
- return self._accessor.stat(self)
+ return self._accessor.stat(self, follow_symlinks=follow_symlinks)
def owner(self):
"""
@@ -1286,18 +1278,18 @@ class Path(PurePath):
if not exist_ok or not self.is_dir():
raise
- def chmod(self, mode):
+ def chmod(self, mode, *, follow_symlinks=True):
"""
Change the permissions of the path, like os.chmod().
"""
- self._accessor.chmod(self, mode)
+ self._accessor.chmod(self, mode, follow_symlinks=follow_symlinks)
def lchmod(self, mode):
"""
Like chmod(), except if the path points to a symlink, the symlink's
permissions are changed, rather than its target's.
"""
- self._accessor.lchmod(self, mode)
+ self.chmod(mode, follow_symlinks=False)
def unlink(self, missing_ok=False):
"""
@@ -1321,7 +1313,7 @@ class Path(PurePath):
Like stat(), except if the path points to a symlink, the symlink's
status information is returned, rather than its target's.
"""
- return self._accessor.lstat(self)
+ return self.stat(follow_symlinks=False)
def link_to(self, target):
"""
diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py
index 9be7294..2643119 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -1828,6 +1828,21 @@ class _BasePathTest(object):
p.chmod(new_mode)
self.assertEqual(p.stat().st_mode, new_mode)
+ # On Windows, os.chmod does not follow symlinks (issue #15411)
+ @only_posix
+ def test_chmod_follow_symlinks_true(self):
+ p = self.cls(BASE) / 'linkA'
+ q = p.resolve()
+ mode = q.stat().st_mode
+ # Clear writable bit.
+ new_mode = mode & ~0o222
+ p.chmod(new_mode, follow_symlinks=True)
+ self.assertEqual(q.stat().st_mode, new_mode)
+ # Set writable bit
+ new_mode = mode | 0o222
+ p.chmod(new_mode, follow_symlinks=True)
+ self.assertEqual(q.stat().st_mode, new_mode)
+
# XXX also need a test for lchmod.
def test_stat(self):
@@ -1840,6 +1855,17 @@ class _BasePathTest(object):
self.assertNotEqual(p.stat(), st)
@os_helper.skip_unless_symlink
+ def test_stat_no_follow_symlinks(self):
+ p = self.cls(BASE) / 'linkA'
+ st = p.stat()
+ self.assertNotEqual(st, p.stat(follow_symlinks=False))
+
+ def test_stat_no_follow_symlinks_nosymlink(self):
+ p = self.cls(BASE) / 'fileA'
+ st = p.stat()
+ self.assertEqual(st, p.stat(follow_symlinks=False))
+
+ @os_helper.skip_unless_symlink
def test_lstat(self):
p = self.cls(BASE)/ 'linkA'
st = p.stat()
diff --git a/Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst b/Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst
new file mode 100644
index 0000000..dacefb7
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-03-30-00-13-27.bpo-39906.eaR3fN.rst
@@ -0,0 +1 @@
+:meth:`pathlib.Path.stat` and :meth:`~pathlib.Path.chmod` now accept a *follow_symlinks* keyword-only argument for consistency with corresponding functions in the :mod:`os` module. \ No newline at end of file