Skip to content

Commit

Permalink
pythongh-113188: Fix shutil.copymode() and shutil.copystat() on Windows
Browse files Browse the repository at this point in the history
Previously it worked differenly 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.
  • Loading branch information
serhiy-storchaka committed Dec 19, 2023
1 parent ca5a523 commit d7dd649
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 15 deletions.
16 changes: 14 additions & 2 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,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.realpath(dst, strict=True)
stat_func, chmod_func = _stat, os.chmod

st = stat_func(src)
Expand Down Expand Up @@ -382,8 +386,16 @@ def lookup(name):
# 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,
Expand Down
25 changes: 12 additions & 13 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,19 +1046,18 @@ def test_copymode_follow_symlinks(self):
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)
# 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')
@os_helper.skip_unless_symlink
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix :func:`shutil.copymode` on Windows. Previously it worked differenly 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 not modify the permission bits if
*follow_symlinks* is false and *src* is a symbolic link.

0 comments on commit d7dd649

Please sign in to comment.