summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/shutil.py16
-rw-r--r--Lib/test/test_shutil.py44
-rw-r--r--Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst6
3 files changed, 43 insertions, 23 deletions
diff --git a/Lib/shutil.py b/Lib/shutil.py
index cae65a3..eecc4be 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -298,11 +298,15 @@ def copymode(src, dst, *, follow_symlinks=True):
sys.audit("shutil.copymode", src, dst)
if not follow_symlinks and _islink(src) and os.path.islink(dst):
- if hasattr(os, 'lchmod'):
+ if os.name == 'nt':
+ stat_func, chmod_func = os.lstat, os.chmod
+ elif hasattr(os, 'lchmod'):
stat_func, chmod_func = os.lstat, os.lchmod
else:
return
else:
+ if os.name == 'nt' and os.path.islink(dst):
+ dst = os.path.realpath(dst, strict=True)
stat_func, chmod_func = _stat, os.chmod
st = stat_func(src)
@@ -378,8 +382,16 @@ def copystat(src, dst, *, follow_symlinks=True):
# We must copy extended attributes before the file is (potentially)
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
_copyxattr(src, dst, follow_symlinks=follow)
+ _chmod = lookup("chmod")
+ if os.name == 'nt':
+ if follow:
+ if os.path.islink(dst):
+ dst = os.path.realpath(dst, strict=True)
+ else:
+ def _chmod(*args, **kwargs):
+ os.chmod(*args)
try:
- lookup("chmod")(dst, mode, follow_symlinks=follow)
+ _chmod(dst, mode, follow_symlinks=follow)
except NotImplementedError:
# if we got a NotImplementedError, it's because
# * follow_symlinks=False,
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index 0844105..f4fdeef 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -877,23 +877,23 @@ class TestCopy(BaseTest, unittest.TestCase):
shutil.copymode(src, dst)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
# On Windows, os.chmod does not follow symlinks (issue #15411)
- if os.name != 'nt':
- # follow src link
- os.chmod(dst, stat.S_IRWXO)
- shutil.copymode(src_link, dst)
- self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
- # follow dst link
- os.chmod(dst, stat.S_IRWXO)
- shutil.copymode(src, dst_link)
- self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
- # follow both links
- os.chmod(dst, stat.S_IRWXO)
- shutil.copymode(src_link, dst_link)
- self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
-
- @unittest.skipUnless(hasattr(os, 'lchmod'), 'requires os.lchmod')
+ # follow src link
+ os.chmod(dst, stat.S_IRWXO)
+ shutil.copymode(src_link, dst)
+ self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
+ # follow dst link
+ os.chmod(dst, stat.S_IRWXO)
+ shutil.copymode(src, dst_link)
+ self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
+ # follow both links
+ os.chmod(dst, stat.S_IRWXO)
+ shutil.copymode(src_link, dst_link)
+ self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
+
+ @unittest.skipUnless(hasattr(os, 'lchmod') or os.name == 'nt', 'requires os.lchmod')
@os_helper.skip_unless_symlink
def test_copymode_symlink_to_symlink(self):
+ _lchmod = os.chmod if os.name == 'nt' else os.lchmod
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'foo')
dst = os.path.join(tmp_dir, 'bar')
@@ -905,20 +905,20 @@ class TestCopy(BaseTest, unittest.TestCase):
os.symlink(dst, dst_link)
os.chmod(src, stat.S_IRWXU|stat.S_IRWXG)
os.chmod(dst, stat.S_IRWXU)
- os.lchmod(src_link, stat.S_IRWXO|stat.S_IRWXG)
+ _lchmod(src_link, stat.S_IRWXO|stat.S_IRWXG)
# link to link
- os.lchmod(dst_link, stat.S_IRWXO)
+ _lchmod(dst_link, stat.S_IRWXO)
old_mode = os.stat(dst).st_mode
shutil.copymode(src_link, dst_link, follow_symlinks=False)
self.assertEqual(os.lstat(src_link).st_mode,
os.lstat(dst_link).st_mode)
self.assertEqual(os.stat(dst).st_mode, old_mode)
# src link - use chmod
- os.lchmod(dst_link, stat.S_IRWXO)
+ _lchmod(dst_link, stat.S_IRWXO)
shutil.copymode(src_link, dst, follow_symlinks=False)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
# dst link - use chmod
- os.lchmod(dst_link, stat.S_IRWXO)
+ _lchmod(dst_link, stat.S_IRWXO)
shutil.copymode(src, dst_link, follow_symlinks=False)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
@@ -955,11 +955,13 @@ class TestCopy(BaseTest, unittest.TestCase):
os.symlink(dst, dst_link)
if hasattr(os, 'lchmod'):
os.lchmod(src_link, stat.S_IRWXO)
+ elif os.name == 'nt':
+ os.chmod(src_link, stat.S_IRWXO)
if hasattr(os, 'lchflags') and hasattr(stat, 'UF_NODUMP'):
os.lchflags(src_link, stat.UF_NODUMP)
src_link_stat = os.lstat(src_link)
# follow
- if hasattr(os, 'lchmod'):
+ if hasattr(os, 'lchmod') or os.name == 'nt':
shutil.copystat(src_link, dst_link, follow_symlinks=True)
self.assertNotEqual(src_link_stat.st_mode, os.stat(dst).st_mode)
# don't follow
@@ -970,7 +972,7 @@ class TestCopy(BaseTest, unittest.TestCase):
# The modification times may be truncated in the new file.
self.assertLessEqual(getattr(src_link_stat, attr),
getattr(dst_link_stat, attr) + 1)
- if hasattr(os, 'lchmod'):
+ if hasattr(os, 'lchmod') or os.name == 'nt':
self.assertEqual(src_link_stat.st_mode, dst_link_stat.st_mode)
if hasattr(os, 'lchflags') and hasattr(src_link_stat, 'st_flags'):
self.assertEqual(src_link_stat.st_flags, dst_link_stat.st_flags)
diff --git a/Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst b/Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst
new file mode 100644
index 0000000..17c6957
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst
@@ -0,0 +1,6 @@
+Fix :func:`shutil.copymode` and :func:`shutil.copystat` on Windows.
+Previously they worked differenly if *dst* is a symbolic link:
+they modified the permission bits of *dst* itself
+rather than the file it points to if *follow_symlinks* is true or *src* is
+not a symbolic link, and did not modify the permission bits if
+*follow_symlinks* is false and *src* is a symbolic link.