Skip to content

Commit 54927cd

Browse files
[3.11] bpo-26791: Update shutil.move() to provide the same symlink move behavior as the mv shell when moving a symlink into a directory that is the target of the symlink (GH-21759) (GH-113518)
(cherry picked from commit c66b577) Co-authored-by: Jeffrey Kintscher <[email protected]>
1 parent a1f0118 commit 54927cd

File tree

3 files changed

+34
-1
lines changed

3 files changed

+34
-1
lines changed

Lib/shutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ def move(src, dst, copy_function=copy2):
837837
sys.audit("shutil.move", src, dst)
838838
real_dst = dst
839839
if os.path.isdir(dst):
840-
if _samefile(src, dst):
840+
if _samefile(src, dst) and not os.path.islink(src):
841841
# We might be on a case insensitive filesystem,
842842
# perform the rename anyway.
843843
os.rename(src, dst)

Lib/test/test_shutil.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2291,6 +2291,35 @@ def test_move_dir_caseinsensitive(self):
22912291
finally:
22922292
os.rmdir(dst_dir)
22932293

2294+
# bpo-26791: Check that a symlink to a directory can
2295+
# be moved into that directory.
2296+
@mock_rename
2297+
def _test_move_symlink_to_dir_into_dir(self, dst):
2298+
src = os.path.join(self.src_dir, 'linktodir')
2299+
dst_link = os.path.join(self.dst_dir, 'linktodir')
2300+
os.symlink(self.dst_dir, src, target_is_directory=True)
2301+
shutil.move(src, dst)
2302+
self.assertTrue(os.path.islink(dst_link))
2303+
self.assertTrue(os.path.samefile(self.dst_dir, dst_link))
2304+
self.assertFalse(os.path.exists(src))
2305+
2306+
# Repeat the move operation with the destination
2307+
# symlink already in place (should raise shutil.Error).
2308+
os.symlink(self.dst_dir, src, target_is_directory=True)
2309+
with self.assertRaises(shutil.Error):
2310+
shutil.move(src, dst)
2311+
self.assertTrue(os.path.samefile(self.dst_dir, dst_link))
2312+
self.assertTrue(os.path.exists(src))
2313+
2314+
@os_helper.skip_unless_symlink
2315+
def test_move_symlink_to_dir_into_dir(self):
2316+
self._test_move_symlink_to_dir_into_dir(self.dst_dir)
2317+
2318+
@os_helper.skip_unless_symlink
2319+
def test_move_symlink_to_dir_into_symlink_to_dir(self):
2320+
dst = os.path.join(self.src_dir, 'otherlinktodir')
2321+
os.symlink(self.dst_dir, dst, target_is_directory=True)
2322+
self._test_move_symlink_to_dir_into_dir(dst)
22942323

22952324
@os_helper.skip_unless_dac_override
22962325
@unittest.skipUnless(hasattr(os, 'lchflags')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`shutil.move` now moves a symlink into a directory when that
2+
directory is the target of the symlink. This provides the same behavior as
3+
the mv shell command. The previous behavior raised an exception. Patch by
4+
Jeffrey Kintscher.

0 commit comments

Comments
 (0)