From 5b5536dd5fa435967e174f83902a85b643e3a502 Mon Sep 17 00:00:00 2001 From: lipengyu Date: Sat, 22 Aug 2026 11:11:39 +0800 Subject: [PATCH] gh-156210: Fix shutil.copytree() detection of dangling relative symlinks --- Lib/shutil.py | 2 +- Lib/test/test_shutil.py | 29 +++++++++++++++++++ ...-08-22-11-00-09.gh-issue-156210.IBtBw-.rst | 3 ++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-22-11-00-09.gh-issue-156210.IBtBw-.rst diff --git a/Lib/shutil.py b/Lib/shutil.py index ab75ba9da8894b6..e17242b33a6eaff 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -593,7 +593,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function, copystat(srcobj, dstname, follow_symlinks=not symlinks) else: # ignore dangling symlink if the flag is on - if not os.path.exists(linkto) and ignore_dangling_symlinks: + if not os.path.exists(srcname) and ignore_dangling_symlinks: continue # otherwise let the copy occur. copy2 will raise an error if srcentry.is_dir(): diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 06ebdf9b68f20fc..941179d9471b261 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1060,6 +1060,35 @@ def test_copytree_dangling_symlinks(self): shutil.copytree(src_dir, dst_dir, symlinks=True) self.assertIn('test.txt', os.listdir(dst_dir)) + @os_helper.skip_unless_symlink + def test_copytree_valid_relative_symlink(self): + root_dir = self.mkdtemp() + src_dir = os.path.join(root_dir, 'source') + os.mkdir(src_dir) + create_file(os.path.join(src_dir, 'target'), 'abc') + os.symlink('target', os.path.join(src_dir, 'link')) + + dst_dir = os.path.join(root_dir, 'destination') + with os_helper.change_cwd(root_dir): + shutil.copytree(src_dir, dst_dir, ignore_dangling_symlinks=True) + self.assertEqual(['link', 'target'], sorted(os.listdir(dst_dir))) + self.assertFalse(os.path.islink(os.path.join(dst_dir, 'link'))) + self.assertEqual('abc', read_file(os.path.join(dst_dir, 'link'))) + + @os_helper.skip_unless_symlink + def test_copytree_dangling_relative_symlink(self): + root_dir = self.mkdtemp() + src_dir = os.path.join(root_dir, 'source') + os.mkdir(src_dir) + os.symlink('target', os.path.join(src_dir, 'link')) + create_file(os.path.join(root_dir, 'target'), 'abc') + + dst_dir = os.path.join(root_dir, 'destination') + with os_helper.change_cwd(root_dir): + shutil.copytree(src_dir, dst_dir, + ignore_dangling_symlinks=True) + self.assertEqual([], os.listdir(dst_dir)) + @os_helper.skip_unless_symlink def test_copytree_symlink_dir(self): src_dir = self.mkdtemp() diff --git a/Misc/NEWS.d/next/Library/2026-08-22-11-00-09.gh-issue-156210.IBtBw-.rst b/Misc/NEWS.d/next/Library/2026-08-22-11-00-09.gh-issue-156210.IBtBw-.rst new file mode 100644 index 000000000000000..0f36109e59ab04f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-22-11-00-09.gh-issue-156210.IBtBw-.rst @@ -0,0 +1,3 @@ +Fix :func:`shutil.copytree` to determine whether relative symbolic links are +dangling based on their parent directory rather than the current working +directory when ``ignore_dangling_symlinks`` is true.