From dd962a55622eb2d311662c1c6c69b21f902d8e66 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 13 Dec 2023 21:12:21 +0200 Subject: [PATCH 1/2] gh-113188: Fix 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 nothing if follow_symlinks is false and src is a symbolic link. Also document similar changes in shutil.copystat() etc. --- Doc/library/shutil.rst | 14 +++++++++++ Doc/whatsnew/3.13.rst | 7 ++++++ Lib/shutil.py | 7 +++++- Lib/test/test_shutil.py | 25 +++++++++---------- ...-12-15-20-29-49.gh-issue-113188.AvoraB.rst | 5 ++++ 5 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index f61ef8b0ecc7ba..c230ca6a5b52c9 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -108,6 +108,13 @@ Directory and files operations .. versionchanged:: 3.3 Added *follow_symlinks* argument. + .. versionchanged:: 3.13 + In older Python versions :func:`!copymode` worked differenly on Windows + 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. + .. function:: copystat(src, dst, *, follow_symlinks=True) Copy the permission bits, last access time, last modification time, and @@ -155,6 +162,13 @@ Directory and files operations .. versionchanged:: 3.3 Added *follow_symlinks* argument and support for Linux extended attributes. + .. versionchanged:: 3.13 + In older Python versions :func:`!copymode` worked differenly on Windows + 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. + .. function:: copy(src, dst, *, follow_symlinks=True) Copies the file *src* to the file or directory *dst*. *src* and *dst* diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 4f9643967d20cf..e52d0a88427389 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -1117,6 +1117,13 @@ Changes in the Python API other "private" attributes. (See :gh:`112826`.) +* In older Python versions :mod:`shutil` functions :func:`~shutil.copymode`, + :func:`~shutil.copystat`, :func:`~shutil.copy` and :func:`~shutil.copy2` + worked differenly on Windows 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. Build Changes ============= diff --git a/Lib/shutil.py b/Lib/shutil.py index dc3aac3e07f910..c40f6ddae39a17 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -306,7 +306,12 @@ def copymode(src, dst, *, follow_symlinks=True): else: return else: - stat_func, chmod_func = _stat, os.chmod + stat_func = _stat + if os.name == 'nt' and os.path.islink(dst): + def chmod_func(*args): + os.chmod(*args, follow_symlinks=True) + else: + chmod_func = os.chmod st = stat_func(src) chmod_func(dst, stat.S_IMODE(st.st_mode)) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 5ce8e5d77fbbf3..cc5459aa08fe33 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1101,19 +1101,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 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 00000000000000..b230b3978eed30 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst @@ -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. From 1ebc29ea94648d4bf6bd023dc96190aeaebb53fb Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 23 Dec 2023 11:53:42 +0200 Subject: [PATCH 2/2] Update docs. --- Doc/library/shutil.rst | 14 -------------- Doc/whatsnew/3.13.rst | 7 ------- .../2023-12-15-20-29-49.gh-issue-113188.AvoraB.rst | 5 +++-- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index c230ca6a5b52c9..f61ef8b0ecc7ba 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -108,13 +108,6 @@ Directory and files operations .. versionchanged:: 3.3 Added *follow_symlinks* argument. - .. versionchanged:: 3.13 - In older Python versions :func:`!copymode` worked differenly on Windows - 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. - .. function:: copystat(src, dst, *, follow_symlinks=True) Copy the permission bits, last access time, last modification time, and @@ -162,13 +155,6 @@ Directory and files operations .. versionchanged:: 3.3 Added *follow_symlinks* argument and support for Linux extended attributes. - .. versionchanged:: 3.13 - In older Python versions :func:`!copymode` worked differenly on Windows - 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. - .. function:: copy(src, dst, *, follow_symlinks=True) Copies the file *src* to the file or directory *dst*. *src* and *dst* diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index c611a06de881fb..7dc02dacdc68f7 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -1159,13 +1159,6 @@ Changes in the Python API other "private" attributes. (See :gh:`112826`.) -* In older Python versions :mod:`shutil` functions :func:`~shutil.copymode`, - :func:`~shutil.copystat`, :func:`~shutil.copy` and :func:`~shutil.copy2` - worked differenly on Windows 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. Build Changes ============= 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 index b230b3978eed30..17c69572d9f2b1 100644 --- 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 @@ -1,5 +1,6 @@ -Fix :func:`shutil.copymode` on Windows. Previously it worked differenly if -*dst* is a symbolic link: it modified the permission bits of *dst* itself +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.