Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is correct though? srcname is the the symlink file. However, we want to resolve the relative symlink with respect to src, that is, we read the symlink content and then check if the file exists. The problem is therefore not here but rather the computation of "linkto". Instead, we should readlink(srcname) by interpreting srcname's content as relative to the src rather than PWD.

continue
# otherwise let the copy occur. copy2 will raise an error
if srcentry.is_dir():
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading