]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
Issue #9993: When the source and destination are on different filesystems,
authorAntoine Pitrou <solipsis@pitrou.net>
Fri, 6 Jan 2012 19:16:19 +0000 (20:16 +0100)
committerAntoine Pitrou <solipsis@pitrou.net>
Fri, 6 Jan 2012 19:16:19 +0000 (20:16 +0100)
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
Lib/shutil.py
Lib/test/test_shutil.py
Misc/ACKS
Misc/NEWS

index 45be0e570bbc4322b90ac918a43198cb5f429008..9e8784bbe23d42c528c831584448f5f050f02ecd 100644 (file)
@@ -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)
 
index 95bebb874ab30fac667b0862b25b6cfc38c900c7..5f69fb7b75d6d6d584a3218960d76f9ef8ce2d3a 100644 (file)
@@ -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)
index a750166fac296ed735e6f1883a162d4e567e0ce2..c72bac26cefbd9d7419e804ec4f6070e3c3bd592 100644 (file)
@@ -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):
 
index 12f4b49d8a3ed017a34a7634bb2707dadd8f4047..4a7dd1116051d67a8666b32e1e2e607f211f6e2e 100644 (file)
--- 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
index 47fc5e90a0f0e8b28c18e0565e0259c811ae3025..274465a146c57d4c59017fde6655943628d162fb 100644 (file)
--- 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.