Skip to content

bpo-47049: Fix incorrect shutil.copytree() behaviour with symlinks #31967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
12 changes: 10 additions & 2 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,18 +484,26 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
os.symlink(linkto, dstname)
copystat(srcobj, dstname, follow_symlinks=not symlinks)
else:
# If the symlink is relative, linkto has to be normalized,
# otherwise os.path.exists() will incorrectly report that
# the link is dangling when the current directory is not
# the same as src.
if not os.path.isabs(linkto):
linkto = os.path.normpath(os.path.join(src, linkto))
# ignore dangling symlink if the flag is on
if not os.path.exists(linkto) and ignore_dangling_symlinks:
continue
# otherwise let the copy occur. copy2 will raise an error
if srcentry.is_dir():
copytree(srcobj, dstname, symlinks, ignore,
copy_function, dirs_exist_ok=dirs_exist_ok)
copy_function, dirs_exist_ok=dirs_exist_ok,
ignore_dangling_symlinks=ignore_dangling_symlinks)
else:
copy_function(srcobj, dstname)
elif srcentry.is_dir():
copytree(srcobj, dstname, symlinks, ignore, copy_function,
dirs_exist_ok=dirs_exist_ok)
dirs_exist_ok=dirs_exist_ok,
ignore_dangling_symlinks=ignore_dangling_symlinks)
else:
# Will raise a SpecialFileError for unsupported file types
copy_function(srcobj, dstname)
Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,25 @@ def test_copytree_dangling_symlinks(self):
shutil.copytree(src_dir, dst_dir, symlinks=True)
self.assertIn('test.txt', os.listdir(dst_dir))

# Issue 47049: See https://bugs.python.org/issue47049
@os_helper.skip_unless_symlink
def test_copytree_no_symlinks_ignore_dangling_symlinks(self):
src_dir = self.mkdtemp()
dst_dir = os.path.join(self.mkdtemp(), 'destination')
src_subdir = os.path.join(src_dir, 'subdir')
src_file = os.path.join(src_dir, 'test.txt')
dst_file = os.path.join(dst_dir, 'test.txt')
dst_nonexisting_file = os.path.join(dst_dir, 'doesnotexist.txt')
write_file(src_file, 'dummy content')
os.mkdir(src_subdir)
with os_helper.change_cwd(src_subdir):
os.symlink(os.path.join('..', 'test.txt'), 'test.txt')
os.symlink('IDONOTEXIST', 'doesnotexist.txt')
shutil.copytree(src_subdir, dst_dir, symlinks=False, ignore_dangling_symlinks=True)
self.assertFalse(os.path.islink(dst_file))
self.assertFalse(os.path.exists(dst_nonexisting_file))
self.assertEqual(read_file(dst_file), 'dummy content')

@os_helper.skip_unless_symlink
def test_copytree_symlink_dir(self):
src_dir = self.mkdtemp()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug, which causes shutil.copytree() incorrectly not copy symlink contents if called with symlink=False and ignore_dangling_symlinks=True.