Skip to content
9 changes: 8 additions & 1 deletion Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,9 +943,10 @@ def move(src, dst, copy_function=copy2):
raise Error("Destination path '%s' already exists" % real_dst)
try:
os.rename(src, real_dst)
except OSError:
except OSError as e:
if os.path.islink(src):
linkto = os.readlink(src)
_raise_not_exdev(e)
os.symlink(linkto, real_dst)
os.unlink(src)
elif os.path.isdir(src):
Expand All @@ -958,10 +959,12 @@ def move(src, dst, copy_function=copy2):
raise PermissionError("Cannot move the non-empty directory "
"'%s': Lacking write permission to '%s'."
% (src, src))
_raise_not_exdev(e)
copytree(src, real_dst, copy_function=copy_function,
symlinks=True)
rmtree(src)
else:
_raise_not_exdev(e)
copy_function(src, real_dst)
os.unlink(src)
return real_dst
Expand All @@ -980,6 +983,10 @@ def _is_immutable(src):
immutable_states = [stat.UF_IMMUTABLE, stat.SF_IMMUTABLE]
return hasattr(st, 'st_flags') and st.st_flags in immutable_states

def _raise_not_exdev(err):
if err.errno != errno.EXDEV:
raise err from None

def _get_gid(name):
"""Returns a gid, given a group name."""
if name is None:
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 @@ -3196,6 +3196,35 @@ def test_move_dir_permission_denied(self):
os.lchflags(TESTFN_DST, stat.UF_OPAQUE)
os_helper.rmtree(TESTFN_DST)

@unittest.mock.patch('os.rename')
def test_no_copied_file_left_not_EXDEV(self, mock_rename):
new_file = os.path.join(self.dst_dir, "bar")
mock_rename.side_effect = PermissionError("not EXDEV")
with self.assertRaises(PermissionError):
shutil.move(self.src_file, new_file)
self.assertFalse(os.path.exists(new_file))

@unittest.mock.patch('os.rename')
def test_no_copied_dir_left_not_EXDEV(self, mock_rename):
subdir = os.path.join(self.src_dir, "subdir")
os.mkdir(subdir)
new_dir = os.path.join(self.dst_dir, "new_dir")
mock_rename.side_effect = PermissionError("not EXDEV")
with self.assertRaises(PermissionError):
shutil.move(subdir, new_dir)
self.assertFalse(os.path.exists(new_dir))

@unittest.mock.patch('os.rename')
@os_helper.skip_unless_symlink
def test_no_copied_symlink_left_not_EXDEV(self, mock_rename):
old_link = os.path.join(self.src_dir, "bar")
os.symlink(os.path.basename(self.src_file), old_link)
new_link = os.path.join(self.dst_dir, "new_link")
mock_rename.side_effect = PermissionError("not EXDEV")
with self.assertRaises(PermissionError):
shutil.move(old_link, new_link)
self.assertFalse(os.path.exists(new_link))


class TestCopyFile(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:func:`shutil.move` won't copy the source to its destination if it fails
with errors except EXDEV. Patch by Timothy Poon.
Loading