https://github.com/python/cpython/commit/78e09a488d41066dea5f8dcf22405a0d5dd8be23
commit: 78e09a488d41066dea5f8dcf22405a0d5dd8be23
branch: main
author: Barney Gale <[email protected]>
committer: barneygale <[email protected]>
date: 2025-02-24T19:10:50Z
summary:
GH-125413: Fix stale metadata from `pathlib.Path.copy()` and `move()` (#130424)
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.
files:
A Misc/NEWS.d/next/Library/2025-02-21-21-50-21.gh-issue-125413.DEAD0L.rst
M Lib/pathlib/_abc.py
M Lib/pathlib/_local.py
M Lib/test/test_pathlib/test_pathlib_abc.py
diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py
index 4106d478822084..97d78f557d14dc 100644
--- a/Lib/pathlib/_abc.py
+++ b/Lib/pathlib/_abc.py
@@ -353,7 +353,8 @@ def copy(self, target, follow_symlinks=True,
dirs_exist_ok=False,
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):
diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py
index 9b2738652e9754..0ae85a68b3b2c1 100644
--- a/Lib/pathlib/_local.py
+++ b/Lib/pathlib/_local.py
@@ -1098,7 +1098,8 @@ def copy(self, target, follow_symlinks=True,
dirs_exist_ok=False,
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 @@ def move(self, target):
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()
diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py
b/Lib/test/test_pathlib/test_pathlib_abc.py
index 68fe3521410f25..a6e3e0709833a3 100644
--- a/Lib/test/test_pathlib/test_pathlib_abc.py
+++ b/Lib/test/test_pathlib/test_pathlib_abc.py
@@ -1391,8 +1391,8 @@ def test_copy_file(self):
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 @@ def test_copy_file_to_existing_file(self):
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 @@ def test_copy_file_empty(self):
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 @@ def test_copy_dir_simple(self):
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 @@ def ordered_walk(path):
# 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 @@ def ordered_walk(path):
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 @@ def
test_copy_dir_to_existing_directory_dirs_exist_ok(self):
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 @@ def test_copy_dir_into_itself(self):
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 @@ def test_copy_into(self):
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
index 00000000000000..87ed43ceb69b8c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-02-21-21-50-21.gh-issue-125413.DEAD0L.rst
@@ -0,0 +1,2 @@
+Ensure the path returned from :meth:`pathlib.Path.copy` or
+:meth:`~pathlib.Path.move` has fresh :attr:`~pathlib.Path.info`.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]