10000 [3.11] gh-113188: Fix shutil.copymode() and shutil.copystat() on Wind… · python/cpython@80b2bad · GitHub
[go: up one dir, main page]

Skip to content

Commit 80b2bad

Browse files
[3.11] gh-113188: Fix shutil.copymode() and shutil.copystat() on Windows (GH-113285) (GH-113426)
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 nothing if follow_symlinks is false and src is a symbolic link. (cherry picked from commit c7874bb) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 0bd134d commit 80b2bad

File tree

3 files changed

+43
-23
lines changed

3 files changed

+43
-23
lines changed

Lib/shutil.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,15 @@ def copymode(src, dst, *, follow_symlinks=True):
298298
sys.audit("shutil.copymode", src, dst)
299299

300300
if not follow_symlinks and _islink(src) and os.path.islink(dst):
301-
if hasattr(os, 'lchmod'):
301+
if os.name == 'nt':
302+
stat_func, chmod_func = os.lstat, os.chmod
303+
elif hasattr(os, 'lchmod'):
302304
stat_func, chmod_func = os.lstat, os.lchmod
303305
else:
304306
return
305307
else:
308+
if os.name == 'nt' and os.path.islink(dst):
309+
dst = os.path.realpath(dst, strict=True)
306310
stat_func, chmod_func = _stat, os.chmod
307311

308312
st = stat_func(src)
@@ -378,8 +382,16 @@ def lookup(name):
378382
# We must copy extended attributes before the file is (potentially)
379383
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
380384
_copyxattr(src, dst, follow_symlinks=follow)
385+
_chmod = lookup("chmod")
386+
if os.name == 'nt':
387+
if follow:
388+
if os.path.islink(dst):
389+
dst = os.path.realpath(dst, strict=True)
390+
else:
391+
def _chmod(*args, **kwargs):
392+
os.chmod(*args)
381393
try:
382-
lookup("chmod")(dst, mode, follow_symlinks=follow)
394+
_chmod(dst, mode, follow_symlinks=follow)
383395
except NotImplementedError:
384396
# if we got a NotImplementedError, it's because
385397
# * follow_symlinks=False,

Lib/test/test_shutil.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -877,23 +877,23 @@ def test_copymode_follow_symlinks(self):
877877
shutil.copymode(src, dst)
878878
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
879879
# On Windows, os.chmod does not follow symlinks (issue #15411)
880-
if os.name != 'nt':
881-
# follow src link
882-
os.chmod(dst, stat.S_IRWXO)
883-
shutil.copymode(src_link, dst)
884-
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
885-
# follow dst link
886-
os.chmod(dst, stat.S_IRWXO)
887-
shutil.copymode(src, dst_link)
888-
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
889-
# follow both links
890-
os.chmod(dst, stat.S_IRWXO)
891-
shutil.copymode(src_link, dst_link)
892-
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
893-
894-
@unittest.skipUnless(hasattr(os, 'lchmod'), 'requires os.lchmod')
880+
# follow src link
881+
os.chmod(dst, stat.S_IRWXO)
882+
shutil.copymode(src_link, dst)
883+
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
884+
# follow dst link
885+
os.chmod(dst, stat.S_IRWXO)
886+
shutil.copymode(src, dst_link)
887+
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
888+
# follow both links
889+
os.chmod(dst, stat.S_IRWXO)
890+
shutil.copymode(src_link, dst_link)
891+
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
892+
893+
@unittest.skipUnless(hasattr(os, 'lchmod') or os.name == 'nt', 'requires os.lchmod')
895894
@os_helper.skip_unless_symlink
896895
def test_copymode_symlink_to_symlink(self):
896+
_lchmod = os.chmod if os.name == 'nt' else os.lchmod
897897
tmp_dir = self.mkdtemp()
898898
src = os.path.join(tmp_dir, 'foo')
899899
dst = os.path.join(tmp_dir, 'bar')
@@ -905,20 +905,20 @@ def test_copymode_symlink_to_symlink(self):
905905
os.symlink(dst, dst_link)
906906
os.chmod(src, stat.S_IRWXU|stat.S_IRWXG)
907907
os.chmod(dst, stat.S_IRWXU)
908-
os.lchmod(src_link, stat.S_IRWXO|stat.S_IRWXG)
908+
_lchmod(src_link, stat.S_IRWXO|stat.S_IRWXG)
909909
# link to link
910-
os.lchmod(dst_link, stat.S_IRWXO)
910+
_lchmod(dst_link, stat.S_IRWXO)
911911
old_mode = os.stat(dst).st_mode
912912
shutil.copymode(src_link, dst_link, follow_symlinks=False)
913913
self.assertEqual(os.lstat(src_link).st_mode,
914914
os.lstat(dst_link).st_mode)
915915
self.assertEqual(os.stat(dst).st_mode, old_mode)
916916
# src link - use chmod
917-
os.lchmod(dst_link, stat.S_IRWXO)
917+
_lchmod(dst_link, stat.S_IRWXO)
918918
shutil.copymode(src_link, dst, follow_symlinks=False)
919919
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
920920
# dst link - use chmod
921-
os.lchmod(dst_link, stat.S_IRWXO)
921+
_lchmod(dst_link, stat.S_IRWXO)
922922
shutil.copymode(src, dst_link, follow_symlinks=False)
923923
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
924924

@@ -955,11 +955,13 @@ def test_copystat_symlinks(self):
955955
os.symlink(dst, dst_link)
956956
if hasattr(os, 'lchmod'):
957957
os.lchmod(src_link, stat.S_IRWXO)
958+
elif os.name == 'nt':
959+
os.chmod(src_link, stat.S_IRWXO)
958960
if hasattr(os, 'lchflags') and hasattr(stat, 'UF_NODUMP'):
959961
os.lchflags(src_link, stat.UF_NODUMP)
960962
src_link_stat = os.lstat(src_link)
961963
# follow
962-
if hasattr(os, 'lchmod'):
964+
if hasattr(os, 'lchmod') or os.name == 'nt':
963965
shutil.copystat(src_link, dst_link, follow_symlinks=True)
964966
self.assertNotEqual(src_link_stat.st_mode, os.stat(dst).st_mode)
965967
# don't follow
@@ -970,7 +972,7 @@ def test_copystat_symlinks(self):
970972
# The modification times may be truncated in the new file.
971973
self.assertLessEqual(getattr(src_link_stat, attr),
972974
getattr(dst_link_stat, attr) + 1)
973-
if hasattr(os, 'lchmod'):
975+
if hasattr(os, 'lchmod') or os.name == 'nt':
974976
self.assertEqual(src_link_stat.st_mode, dst_link_stat.st_mode)
975977
if hasattr(os, 'lchflags') and hasattr(src_link_stat, 'st_flags'):
976978
self.assertEqual(src_link_stat.st_flags, dst_link_stat.st_flags)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix :func:`shutil.copymode` and :func:`shutil.copystat` on Windows.
2+
Previously they worked differenly if *dst* is a symbolic link:
3+
they modified the permission bits of *dst* itself
4+
rather than the file it points to if *follow_symlinks* is true or *src* is
5+
not a symbolic link, and did not modify the permission bits if
6+
*follow_symlinks* is false and *src* is a symbolic link.

0 commit comments

Comments
 (0)
0