Skip to content

[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) #113518

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

Merged
merged 1 commit into from
Dec 27, 2023
Merged
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 @@ -837,7 +837,7 @@ def move(src, dst, copy_function=copy2):
sys.audit("shutil.move", src, dst)
real_dst = dst
if os.path.isdir(dst):
if _samefile(src, dst):
if _samefile(src, dst) and not os.path.islink(src):
# We might be on a case insensitive filesystem,
# perform the rename anyway.
os.rename(src, dst)
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 @@ -2291,6 +2291,35 @@ def test_move_dir_caseinsensitive(self):
finally:
os.rmdir(dst_dir)

# bpo-26791: Check that a symlink to a directory can
# be moved into that directory.
@mock_rename
def _test_move_symlink_to_dir_into_dir(self, dst):
src = os.path.join(self.src_dir, 'linktodir')
dst_link = os.path.join(self.dst_dir, 'linktodir')
os.symlink(self.dst_dir, src, target_is_directory=True)
shutil.move(src, dst)
self.assertTrue(os.path.islink(dst_link))
self.assertTrue(os.path.samefile(self.dst_dir, dst_link))
self.assertFalse(os.path.exists(src))

# Repeat the move operation with the destination
# symlink already in place (should raise shutil.Error).
os.symlink(self.dst_dir, src, target_is_directory=True)
with self.assertRaises(shutil.Error):
shutil.move(src, dst)
self.assertTrue(os.path.samefile(self.dst_dir, dst_link))
self.assertTrue(os.path.exists(src))

@os_helper.skip_unless_symlink
def test_move_symlink_to_dir_into_dir(self):
self._test_move_symlink_to_dir_into_dir(self.dst_dir)

@os_helper.skip_unless_symlink
def test_move_symlink_to_dir_into_symlink_to_dir(self):
dst = os.path.join(self.src_dir, 'otherlinktodir')
os.symlink(self.dst_dir, dst, target_is_directory=True)
self._test_move_symlink_to_dir_into_dir(dst)

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