From 0a08d7a09537b2ccfc93c5a81bd36cf5f1b1e92d Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Fri, 6 Jan 2012 20:16:19 +0100 Subject: Issue #9993: When the source and destination are on different filesystems, and the source is a symlink, shutil.move() now recreates a symlink on the destination instead of copying the file contents. Patch by Jonathan Niehof and Hynek Schlawack. --- Doc/library/shutil.rst | 7 ++++++- Lib/shutil.py | 11 +++++++++-- Lib/test/test_shutil.py | 43 +++++++++++++++++++++++++++++++++++++++++++ Misc/ACKS | 1 + Misc/NEWS | 5 +++++ 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 45be0e5..9e8784b 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -196,7 +196,12 @@ Directory and files operations If the destination is on the current filesystem, then :func:`os.rename` is used. Otherwise, *src* is copied (using :func:`copy2`) to *dst* and then - removed. + removed. In case of symlinks, a new symlink pointing to the target of *src* + will be created in or as *dst* and *src* will be removed. + + .. versionchanged:: 3.3 + Added explicit symlink handling for foreign filesystems, thus adapting + it to the behavior of GNU's :program:`mv`. .. function:: disk_usage(path) diff --git a/Lib/shutil.py b/Lib/shutil.py index 95bebb8..5f69fb7 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -356,7 +356,10 @@ def move(src, dst): overwritten depending on os.rename() semantics. If the destination is on our current filesystem, then rename() is used. - Otherwise, src is copied to the destination and then removed. + Otherwise, src is copied to the destination and then removed. Symlinks are + recreated under the new name if os.rename() fails because of cross + filesystem renames. + A lot more could be done here... A look at a mv.c shows a lot of the issues this implementation glosses over. @@ -375,7 +378,11 @@ def move(src, dst): try: os.rename(src, real_dst) except OSError: - if os.path.isdir(src): + if os.path.islink(src): + linkto = os.readlink(src) + os.symlink(linkto, real_dst) + os.unlink(src) + elif os.path.isdir(src): if _destinsrc(src, dst): raise Error("Cannot move a directory '%s' into itself '%s'." % (src, dst)) copytree(src, real_dst, symlinks=True) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index a750166..c72bac2 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1104,6 +1104,49 @@ class TestMove(unittest.TestCase): finally: shutil.rmtree(TESTFN, ignore_errors=True) + @support.skip_unless_symlink + @mock_rename + def test_move_file_symlink(self): + dst = os.path.join(self.src_dir, 'bar') + os.symlink(self.src_file, dst) + shutil.move(dst, self.dst_file) + self.assertTrue(os.path.islink(self.dst_file)) + self.assertTrue(os.path.samefile(self.src_file, self.dst_file)) + + @support.skip_unless_symlink + @mock_rename + def test_move_file_symlink_to_dir(self): + filename = "bar" + dst = os.path.join(self.src_dir, filename) + os.symlink(self.src_file, dst) + shutil.move(dst, self.dst_dir) + final_link = os.path.join(self.dst_dir, filename) + self.assertTrue(os.path.islink(final_link)) + self.assertTrue(os.path.samefile(self.src_file, final_link)) + + @support.skip_unless_symlink + @mock_rename + def test_move_dangling_symlink(self): + src = os.path.join(self.src_dir, 'baz') + dst = os.path.join(self.src_dir, 'bar') + os.symlink(src, dst) + dst_link = os.path.join(self.dst_dir, 'quux') + shutil.move(dst, dst_link) + self.assertTrue(os.path.islink(dst_link)) + self.assertEqual(os.path.realpath(src), os.path.realpath(dst_link)) + + @support.skip_unless_symlink + @mock_rename + def test_move_dir_symlink(self): + src = os.path.join(self.src_dir, 'baz') + dst = os.path.join(self.src_dir, 'bar') + os.mkdir(src) + os.symlink(src, dst) + dst_link = os.path.join(self.dst_dir, 'quux') + shutil.move(dst, dst_link) + self.assertTrue(os.path.islink(dst_link)) + self.assertTrue(os.path.samefile(src, dst_link)) + class TestCopyFile(unittest.TestCase): diff --git a/Misc/ACKS b/Misc/ACKS index 12f4b49..4a7dd11 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -707,6 +707,7 @@ Max Neunhöffer George Neville-Neil Johannes Nicolai Samuel Nicolary +Jonathan Niehof Gustavo Niemeyer Oscar Nierstrasz Hrvoje Niksic diff --git a/Misc/NEWS b/Misc/NEWS index 47fc5e9..274465a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -422,6 +422,11 @@ Core and Builtins Library ------- +- Issue #9993: When the source and destination are on different filesystems, + and the source is a symlink, shutil.move() now recreates a symlink on the + destination instead of copying the file contents. Patch by Jonathan Niehof + and Hynek Schlawack. + - Issue #12926: Fix a bug in tarfile's link extraction. - Issue #13696: Fix the 302 Relative URL Redirection problem. -- cgit v0.12