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
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
30 changes: 30 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -3196,6 +3196,36 @@ def test_move_dir_permission_denied(self):
os.lchflags(TESTFN_DST, stat.UF_OPAQUE)
os_helper.rmtree(TESTFN_DST)

@os_helper.skip_unless_working_chmod
def test_no_copied_file_left_not_EXDEV(self):
mode = stat.S_IREAD|stat.S_IEXEC
os.chmod(self.src_dir, mode)
new_file = os.path.join(self.dst_dir, "bar")
self.assertRaises(PermissionError, shutil.move, self.src_file, new_file)
self.assertFalse(os.path.exists(new_file))

@os_helper.skip_unless_working_chmod
def test_no_copied_dir_left_not_EXDEV(self):
subdir = os.path.join(self.src_dir, "subdir")
os.mkdir(subdir)
create_file(os.path.join(subdir, "foo2"), b"another spam")
mode = stat.S_IREAD|stat.S_IEXEC
os.chmod(self.src_dir, mode)
new_dir = os.path.join(self.dst_dir, "new_dir")
self.assertRaises(PermissionError, shutil.move, subdir, new_dir)
self.assertFalse(os.path.exists(new_dir))

@os_helper.skip_unless_working_chmod
@os_helper.skip_unless_symlink
def test_no_copied_symlink_left_not_EXDEV(self):
old_link = os.path.join(self.src_dir, "bar")
os.symlink(self.src_file, old_link)
mode = stat.S_IREAD|stat.S_IEXEC
os.chmod(self.src_dir, mode)
new_link = os.path.join(self.dst_dir, "new_link")
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