Skip to content

Commit 78e09a4

Browse files
authored
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.
1 parent 48c84a4 commit 78e09a4

File tree

4 files changed

+33
-27
lines changed

4 files changed

+33
-27
lines changed

Lib/pathlib/_abc.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,8 @@ def copy(self, target, follow_symlinks=True, dirs_exist_ok=False,
353353
create = target._copy_writer._create
354354
except AttributeError:
355355
raise TypeError(f"Target is not writable: {target}") from None
356-
return create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
356+
create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
357+
return target.joinpath() # Empty join to ensure fresh metadata.
357358

358359
def copy_into(self, target_dir, *, follow_symlinks=True,
359360
dirs_exist_ok=False, preserve_metadata=False):

Lib/pathlib/_local.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1098,7 +1098,8 @@ def copy(self, target, follow_symlinks=True, dirs_exist_ok=False,
10981098
create = target._copy_writer._create
10991099
except AttributeError:
11001100
raise TypeError(f"Target is not writable: {target}") from None
1101-
return create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
1101+
create(self, follow_symlinks, dirs_exist_ok, preserve_metadata)
1102+
return target.joinpath() # Empty join to ensure fresh metadata.
11021103

11031104
def copy_into(self, target_dir, *, follow_symlinks=True,
11041105
dirs_exist_ok=False, preserve_metadata=False):
@@ -1128,10 +1129,12 @@ def move(self, target):
11281129
else:
11291130
ensure_different_files(self, target)
11301131
try:
1131-
return self.replace(target)
1132+
os.replace(self, target)
11321133
except OSError as err:
11331134
if err.errno != EXDEV:
11341135
raise
1136+
else:
1137+
return target.joinpath() # Empty join to ensure fresh metadata.
11351138
# Fall back to copy+delete.
11361139
target = self.copy(target, follow_symlinks=False, preserve_metadata=True)
11371140
self._delete()

Lib/test/test_pathlib/test_pathlib_abc.py

+24-24
Original file line numberDiff line numberDiff line change
@@ -1391,17 +1391,17 @@ def test_copy_file(self):
13911391
target = base / 'copyA'
13921392
result = source.copy(target)
13931393
self.assertEqual(result, target)
1394-
self.assertTrue(target.exists())
1395-
self.assertEqual(source.read_text(), target.read_text())
1394+
self.assertTrue(result.info.exists())
1395+
self.assertEqual(source.read_text(), result.read_text())
13961396

13971397
def test_copy_file_to_existing_file(self):
13981398
base = self.cls(self.base)
13991399
source = base / 'fileA'
14001400
target = base / 'dirB' / 'fileB'
14011401
result = source.copy(target)
14021402
self.assertEqual(result, target)
1403-
self.assertTrue(target.exists())
1404-
self.assertEqual(source.read_text(), target.read_text())
1403+
self.assertTrue(result.info.exists())
1404+
self.assertEqual(source.read_text(), result.read_text())
14051405

14061406
def test_copy_file_to_existing_directory(self):
14071407
base = self.cls(self.base)
@@ -1416,8 +1416,8 @@ def test_copy_file_empty(self):
14161416
source.write_bytes(b'')
14171417
result = source.copy(target)
14181418
self.assertEqual(result, target)
1419-
self.assertTrue(target.exists())
1420-
self.assertEqual(target.read_bytes(), b'')
1419+
self.assertTrue(result.info.exists())
1420+
self.assertEqual(result.read_bytes(), b'')
14211421

14221422
def test_copy_file_to_itself(self):
14231423
base = self.cls(self.base)
@@ -1432,13 +1432,13 @@ def test_copy_dir_simple(self):
14321432
target = base / 'copyC'
14331433
result = source.copy(target)
14341434
self.assertEqual(result, target)
1435-
self.assertTrue(target.is_dir())
1436-
self.assertTrue(target.joinpath('dirD').is_dir())
1437-
self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
1438-
self.assertEqual(target.joinpath('dirD', 'fileD').read_text(),
1435+
self.assertTrue(result.info.is_dir())
1436+
self.assertTrue(result.joinpath('dirD').info.is_dir())
1437+
self.assertTrue(result.joinpath('dirD', 'fileD').info.is_file())
1438+
self.assertEqual(result.joinpath('dirD', 'fileD').read_text(),
14391439
"this is file D\n")
1440-
self.assertTrue(target.joinpath('fileC').is_file())
1441-
self.assertTrue(target.joinpath('fileC').read_text(),
1440+
self.assertTrue(result.joinpath('fileC').info.is_file())
1441+
self.assertTrue(result.joinpath('fileC').read_text(),
14421442
"this is file C\n")
14431443

14441444
def test_copy_dir_complex(self, follow_symlinks=True):
@@ -1462,7 +1462,7 @@ def ordered_walk(path):
14621462

14631463
# Compare the source and target trees
14641464
source_walk = ordered_walk(source)
1465-
target_walk = ordered_walk(target)
1465+
target_walk = ordered_walk(result)
14661466
for source_item, target_item in zip(source_walk, target_walk, strict=True):
14671467
self.assertEqual(source_item[0].parts[len(source.parts):],
14681468
target_item[0].parts[len(target.parts):]) # dirpath
@@ -1472,12 +1472,12 @@ def ordered_walk(path):
14721472
for filename in source_item[2]:
14731473
source_file = source_item[0].joinpath(filename)
14741474
target_file = target_item[0].joinpath(filename)
1475-
if follow_symlinks or not source_file.is_symlink():
1475+
if follow_symlinks or not source_file.info.is_symlink():
14761476
# Regular file.
14771477
self.assertEqual(source_file.read_bytes(), target_file.read_bytes())
1478-
elif source_file.is_dir():
1478+
elif source_file.info.is_dir():
14791479
# Symlink to directory.
1480-
self.assertTrue(target_file.is_dir())
1480+
self.assertTrue(target_file.info.is_dir())
14811481
self.assertEqual(source_file.readlink(), target_file.readlink())
14821482
else:
14831483
# Symlink to file.
@@ -1503,13 +1503,13 @@ def test_copy_dir_to_existing_directory_dirs_exist_ok(self):
15031503
target.joinpath('dirD').mkdir()
15041504
result = source.copy(target, dirs_exist_ok=True)
15051505
self.assertEqual(result, target)
1506-
self.assertTrue(target.is_dir())
1507-
self.assertTrue(target.joinpath('dirD').is_dir())
1508-
self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
1509-
self.assertEqual(target.joinpath('dirD', 'fileD').read_text(),
1506+
self.assertTrue(result.info.is_dir())
1507+
self.assertTrue(result.joinpath('dirD').info.is_dir())
1508+
self.assertTrue(result.joinpath('dirD', 'fileD').info.is_file())
1509+
self.assertEqual(result.joinpath('dirD', 'fileD').read_text(),
15101510
"this is file D\n")
1511-
self.assertTrue(target.joinpath('fileC').is_file())
1512-
self.assertTrue(target.joinpath('fileC').read_text(),
1511+
self.assertTrue(result.joinpath('fileC').info.is_file())
1512+
self.assertTrue(result.joinpath('fileC').read_text(),
15131513
"this is file C\n")
15141514

15151515
def test_copy_dir_to_itself(self):
@@ -1524,15 +1524,15 @@ def test_copy_dir_into_itself(self):
15241524
target = base / 'dirC' / 'dirD' / 'copyC'
15251525
self.assertRaises(OSError, source.copy, target)
15261526
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
1527-
self.assertFalse(target.exists())
1527+
self.assertFalse(target.info.exists())
15281528

15291529
def test_copy_into(self):
15301530
base = self.cls(self.base)
15311531
source = base / 'fileA'
15321532
target_dir = base / 'dirA'
15331533
result = source.copy_into(target_dir)
15341534
self.assertEqual(result, target_dir / 'fileA')
1535-
self.assertTrue(result.exists())
1535+
self.assertTrue(result.info.exists())
15361536
self.assertEqual(source.read_text(), result.read_text())
15371537

15381538
def test_copy_into_empty_name(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Ensure the path returned from :meth:`pathlib.Path.copy` or
2+
:meth:`~pathlib.Path.move` has fresh :attr:`~pathlib.Path.info`.

0 commit comments

Comments
 (0)