]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-125413: Fix stale metadata from `pathlib.Path.copy()` and `move()` (#130424)
authorBarney Gale <barney.gale@gmail.com>
Mon, 24 Feb 2025 19:10:50 +0000 (19:10 +0000)
committerGitHub <noreply@github.com>
Mon, 24 Feb 2025 19:10:50 +0000 (19:10 +0000)
In `pathlib.Path.copy()` and `move()`, return a fresh `Path` object with an
unpopulated `info` attribute, rather than a `Path` object with information
recorded *prior* to the path's creation.

Lib/pathlib/_abc.py
Lib/pathlib/_local.py
Lib/test/test_pathlib/test_pathlib_abc.py
Misc/NEWS.d/next/Library/2025-02-21-21-50-21.gh-issue-125413.DEAD0L.rst [new file with mode: 0644]

index 4106d47882208412276d96f50c9471924df82736..97d78f557d14dc8ba3616d10c222a102728e5c56 100644 (file)
@@ -353,7 +353,8 @@ class ReadablePath(JoinablePath):
             create = target._copy_writer._create
         except AttributeError:
             raise TypeError(f"Target is not writable: {target}") from None
-        return create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
+        create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
+        return target.joinpath()  # Empty join to ensure fresh metadata.
 
     def copy_into(self, target_dir, *, follow_symlinks=True,
                   dirs_exist_ok=False, preserve_metadata=False):
index 9b2738652e9754cc8c3e29f19e1002292d61c998..0ae85a68b3b2c1626b51f7a70f52bbe60b9b9b72 100644 (file)
@@ -1098,7 +1098,8 @@ class Path(PurePath):
             create = target._copy_writer._create
         except AttributeError:
             raise TypeError(f"Target is not writable: {target}") from None
-        return create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
+        create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
+        return target.joinpath()  # Empty join to ensure fresh metadata.
 
     def copy_into(self, target_dir, *, follow_symlinks=True,
                   dirs_exist_ok=False, preserve_metadata=False):
@@ -1128,10 +1129,12 @@ class Path(PurePath):
         else:
             ensure_different_files(self, target)
             try:
-                return self.replace(target)
+                os.replace(self, target)
             except OSError as err:
                 if err.errno != EXDEV:
                     raise
+            else:
+                return target.joinpath()  # Empty join to ensure fresh metadata.
         # Fall back to copy+delete.
         target = self.copy(target, follow_symlinks=False, preserve_metadata=True)
         self._delete()
index 68fe3521410f25efad96db4637e95c0479a1274f..a6e3e0709833a39defc205b68301f68e1913f831 100644 (file)
@@ -1391,8 +1391,8 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
         target = base / 'copyA'
         result = source.copy(target)
         self.assertEqual(result, target)
-        self.assertTrue(target.exists())
-        self.assertEqual(source.read_text(), target.read_text())
+        self.assertTrue(result.info.exists())
+        self.assertEqual(source.read_text(), result.read_text())
 
     def test_copy_file_to_existing_file(self):
         base = self.cls(self.base)
@@ -1400,8 +1400,8 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
         target = base / 'dirB' / 'fileB'
         result = source.copy(target)
         self.assertEqual(result, target)
-        self.assertTrue(target.exists())
-        self.assertEqual(source.read_text(), target.read_text())
+        self.assertTrue(result.info.exists())
+        self.assertEqual(source.read_text(), result.read_text())
 
     def test_copy_file_to_existing_directory(self):
         base = self.cls(self.base)
@@ -1416,8 +1416,8 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
         source.write_bytes(b'')
         result = source.copy(target)
         self.assertEqual(result, target)
-        self.assertTrue(target.exists())
-        self.assertEqual(target.read_bytes(), b'')
+        self.assertTrue(result.info.exists())
+        self.assertEqual(result.read_bytes(), b'')
 
     def test_copy_file_to_itself(self):
         base = self.cls(self.base)
@@ -1432,13 +1432,13 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
         target = base / 'copyC'
         result = source.copy(target)
         self.assertEqual(result, target)
-        self.assertTrue(target.is_dir())
-        self.assertTrue(target.joinpath('dirD').is_dir())
-        self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
-        self.assertEqual(target.joinpath('dirD', 'fileD').read_text(),
+        self.assertTrue(result.info.is_dir())
+        self.assertTrue(result.joinpath('dirD').info.is_dir())
+        self.assertTrue(result.joinpath('dirD', 'fileD').info.is_file())
+        self.assertEqual(result.joinpath('dirD', 'fileD').read_text(),
                          "this is file D\n")
-        self.assertTrue(target.joinpath('fileC').is_file())
-        self.assertTrue(target.joinpath('fileC').read_text(),
+        self.assertTrue(result.joinpath('fileC').info.is_file())
+        self.assertTrue(result.joinpath('fileC').read_text(),
                         "this is file C\n")
 
     def test_copy_dir_complex(self, follow_symlinks=True):
@@ -1462,7 +1462,7 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
 
         # Compare the source and target trees
         source_walk = ordered_walk(source)
-        target_walk = ordered_walk(target)
+        target_walk = ordered_walk(result)
         for source_item, target_item in zip(source_walk, target_walk, strict=True):
             self.assertEqual(source_item[0].parts[len(source.parts):],
                              target_item[0].parts[len(target.parts):])  # dirpath
@@ -1472,12 +1472,12 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
             for filename in source_item[2]:
                 source_file = source_item[0].joinpath(filename)
                 target_file = target_item[0].joinpath(filename)
-                if follow_symlinks or not source_file.is_symlink():
+                if follow_symlinks or not source_file.info.is_symlink():
                     # Regular file.
                     self.assertEqual(source_file.read_bytes(), target_file.read_bytes())
-                elif source_file.is_dir():
+                elif source_file.info.is_dir():
                     # Symlink to directory.
-                    self.assertTrue(target_file.is_dir())
+                    self.assertTrue(target_file.info.is_dir())
                     self.assertEqual(source_file.readlink(), target_file.readlink())
                 else:
                     # Symlink to file.
@@ -1503,13 +1503,13 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
         target.joinpath('dirD').mkdir()
         result = source.copy(target, dirs_exist_ok=True)
         self.assertEqual(result, target)
-        self.assertTrue(target.is_dir())
-        self.assertTrue(target.joinpath('dirD').is_dir())
-        self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
-        self.assertEqual(target.joinpath('dirD', 'fileD').read_text(),
+        self.assertTrue(result.info.is_dir())
+        self.assertTrue(result.joinpath('dirD').info.is_dir())
+        self.assertTrue(result.joinpath('dirD', 'fileD').info.is_file())
+        self.assertEqual(result.joinpath('dirD', 'fileD').read_text(),
                          "this is file D\n")
-        self.assertTrue(target.joinpath('fileC').is_file())
-        self.assertTrue(target.joinpath('fileC').read_text(),
+        self.assertTrue(result.joinpath('fileC').info.is_file())
+        self.assertTrue(result.joinpath('fileC').read_text(),
                         "this is file C\n")
 
     def test_copy_dir_to_itself(self):
@@ -1524,7 +1524,7 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
         target = base / 'dirC' / 'dirD' / 'copyC'
         self.assertRaises(OSError, source.copy, target)
         self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
-        self.assertFalse(target.exists())
+        self.assertFalse(target.info.exists())
 
     def test_copy_into(self):
         base = self.cls(self.base)
@@ -1532,7 +1532,7 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
         target_dir = base / 'dirA'
         result = source.copy_into(target_dir)
         self.assertEqual(result, target_dir / 'fileA')
-        self.assertTrue(result.exists())
+        self.assertTrue(result.info.exists())
         self.assertEqual(source.read_text(), result.read_text())
 
     def test_copy_into_empty_name(self):
diff --git a/Misc/NEWS.d/next/Library/2025-02-21-21-50-21.gh-issue-125413.DEAD0L.rst b/Misc/NEWS.d/next/Library/2025-02-21-21-50-21.gh-issue-125413.DEAD0L.rst
new file mode 100644 (file)
index 0000000..87ed43c
--- /dev/null
@@ -0,0 +1,2 @@
+Ensure the path returned from :meth:`pathlib.Path.copy` or
+:meth:`~pathlib.Path.move` has fresh :attr:`~pathlib.Path.info`.