Skip to content

Commit 6e02d79

Browse files
gh-113188: Fix shutil.copymode() on Windows (GH-113189)
Previously it worked differently if dst is a symbolic link: it 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. Also document similar changes in shutil.copystat().
1 parent bdc8d66 commit 6e02d79

File tree

3 files changed

+24
-14
lines changed

3 files changed

+24
-14
lines changed

Lib/shutil.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,12 @@ def copymode(src, dst, *, follow_symlinks=True):
306306
else:
307307
return
308308
else:
309-
stat_func, chmod_func = _stat, os.chmod
309+
stat_func = _stat
310+
if os.name == 'nt' and os.path.islink(dst):
311+
def chmod_func(*args):
312+
os.chmod(*args, follow_symlinks=True)
313+
else:
314+
chmod_func = os.chmod
310315

311316
st = stat_func(src)
312317
chmod_func(dst, stat.S_IMODE(st.st_mode))

Lib/test/test_shutil.py

+12-13
Original file line numberDiff line numberDiff line change
@@ -1101,19 +1101,18 @@ def test_copymode_follow_symlinks(self):
11011101
shutil.copymode(src, dst)
11021102
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
11031103
# On Windows, os.chmod does not follow symlinks (issue #15411)
1104-
if os.name != 'nt':
1105-
# follow src link
1106-
os.chmod(dst, stat.S_IRWXO)
1107-
shutil.copymode(src_link, dst)
1108-
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
1109-
# follow dst link
1110-
os.chmod(dst, stat.S_IRWXO)
1111-
shutil.copymode(src, dst_link)
1112-
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
1113-
# follow both links
1114-
os.chmod(dst, stat.S_IRWXO)
1115-
shutil.copymode(src_link, dst_link)
1116-
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
1104+
# follow src link
1105+
os.chmod(dst, stat.S_IRWXO)
1106+
shutil.copymode(src_link, dst)
1107+
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
1108+
# follow dst link
1109+
os.chmod(dst, stat.S_IRWXO)
1110+
shutil.copymode(src, dst_link)
1111+
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
1112+
# follow both links
1113+
os.chmod(dst, stat.S_IRWXO)
1114+
shutil.copymode(src_link, dst_link)
1115+
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
11171116

11181117
@unittest.skipUnless(hasattr(os, 'lchmod'), 'requires os.lchmod')
11191118
@os_helper.skip_unless_symlink
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)