diff --git a/Lib/shutil.py b/Lib/shutil.py --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -302,17 +302,17 @@ def copytree(src, dst, symlinks=False, i os.makedirs(dst) errors = [] for name in names: if name in ignored_names: continue srcname = os.path.join(src, name) dstname = os.path.join(dst, name) try: - if os.path.islink(srcname): + if os.path.islink(srcname) and not os.path.isdir(srcname): linkto = os.readlink(srcname) if symlinks: # We can't just leave it to `copy_function` because legacy # code with a custom `copy_function` may rely on copytree # doing the right thing. os.symlink(linkto, dstname) copystat(srcname, dstname, follow_symlinks=not symlinks) else: diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -632,16 +632,31 @@ class TestShutil(unittest.TestCase): self.assertTrue(os.path.isfile(os.path.join(dst_dir, 'test_dir', 'test.txt'))) actual = read_file((dst_dir, 'test.txt')) self.assertEqual(actual, '123') actual = read_file((dst_dir, 'test_dir', 'test.txt')) self.assertEqual(actual, '456') @support.skip_unless_symlink + def test_copytree_symbolic_directory(self): + # see issue 21697 + tmp_dir = self.mkdtemp() + src_dir = os.path.join(tmp_dir, 'src') + dst_dir = os.path.join(tmp_dir, 'dst') + sub_dir = os.path.join(src_dir, 'sub') + os.mkdir(src_dir) + os.mkdir(sub_dir) + src_link = os.path.join(src_dir, 'link') + os.symlink(sub_dir, src_link) + shutil.copytree(src_dir, dst_dir) + self.assertTrue(os.path.isdir(os.path.join(dst_dir, 'sub'))) + self.assertTrue(os.path.isdir(os.path.join(dst_dir, 'link'))) + + @support.skip_unless_symlink def test_copytree_symlinks(self): tmp_dir = self.mkdtemp() src_dir = os.path.join(tmp_dir, 'src') dst_dir = os.path.join(tmp_dir, 'dst') sub_dir = os.path.join(src_dir, 'sub') os.mkdir(src_dir) os.mkdir(sub_dir) write_file((src_dir, 'file.txt'), 'foo')