Skip to content

Commit 563ab5c

Browse files
authored
GH-130614: pathlib ABCs: improve support for receiving path metadata (#131259)
In the private pathlib ABCs, replace `_WritablePath._write_info()` with `_WritablePath._copy_from()`. This provides the target path object with more control over the copying process, including support for querying and setting metadata *before* the path is created. Adjust `_ReadablePath.copy()` so that it forwards its keyword arguments to `_WritablePath._copy_from()` of the target path object. This allows us to remove the unimplemented *preserve_metadata* argument in the ABC method, making it a `Path` exclusive.
1 parent e82c2ca commit 563ab5c

File tree

3 files changed

+92
-64
lines changed

3 files changed

+92
-64
lines changed

Lib/pathlib/__init__.py

+61-13
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from pathlib._os import (
3030
PathInfo, DirEntryInfo,
3131
ensure_different_files, ensure_distinct_paths,
32-
copy_file, copy_info,
32+
copyfile2, copyfileobj, magic_open, copy_info,
3333
)
3434

3535

@@ -810,12 +810,6 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
810810
with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f:
811811
return f.write(data)
812812

813-
def _write_info(self, info, follow_symlinks=True):
814-
"""
815-
Write the given PathInfo to this path.
816-
"""
817-
copy_info(info, self, follow_symlinks=follow_symlinks)
818-
819813
_remove_leading_dot = operator.itemgetter(slice(2, None))
820814
_remove_trailing_slash = operator.itemgetter(slice(-1))
821815

@@ -1100,18 +1094,21 @@ def replace(self, target):
11001094
target = self.with_segments(target)
11011095
return target
11021096

1103-
def copy(self, target, follow_symlinks=True, preserve_metadata=False):
1097+
def copy(self, target, **kwargs):
11041098
"""
11051099
Recursively copy this file or directory tree to the given destination.
11061100
"""
11071101
if not hasattr(target, 'with_segments'):
11081102
target = self.with_segments(target)
11091103
ensure_distinct_paths(self, target)
1110-
copy_file(self, target, follow_symlinks, preserve_metadata)
1104+
try:
1105+
copy_to_target = target._copy_from
1106+
except AttributeError:
1107+
raise TypeError(f"Target path is not writable: {target!r}") from None
1108+
copy_to_target(self, **kwargs)
11111109
return target.joinpath() # Empty join to ensure fresh metadata.
11121110

1113-
def copy_into(self, target_dir, *, follow_symlinks=True,
1114-
preserve_metadata=False):
1111+
def copy_into(self, target_dir, **kwargs):
11151112
"""
11161113
Copy this file or directory tree into the given existing directory.
11171114
"""
@@ -1122,8 +1119,59 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
11221119
target = target_dir / name
11231120
else:
11241121
target = self.with_segments(target_dir, name)
1125-
return self.copy(target, follow_symlinks=follow_symlinks,
1126-
preserve_metadata=preserve_metadata)
1122+
return self.copy(target, **kwargs)
1123+
1124+
def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False):
1125+
"""
1126+
Recursively copy the given path to this path.
1127+
"""
1128+
if not follow_symlinks and source.info.is_symlink():
1129+
self._copy_from_symlink(source, preserve_metadata)
1130+
elif source.info.is_dir():
1131+
children = source.iterdir()
1132+
os.mkdir(self)
1133+
for child in children:
1134+
self.joinpath(child.name)._copy_from(
1135+
child, follow_symlinks, preserve_metadata)
1136+
if preserve_metadata:
1137+
copy_info(source.info, self)
1138+
else:
1139+
self._copy_from_file(source, preserve_metadata)
1140+
1141+
def _copy_from_file(self, source, preserve_metadata=False):
1142+
ensure_different_files(source, self)
1143+
with magic_open(source, 'rb') as source_f:
1144+
with open(self, 'wb') as target_f:
1145+
copyfileobj(source_f, target_f)
1146+
if preserve_metadata:
1147+
copy_info(source.info, self)
1148+
1149+
if copyfile2:
1150+
# Use fast OS routine for local file copying where available.
1151+
_copy_from_file_fallback = _copy_from_file
1152+
def _copy_from_file(self, source, preserve_metadata=False):
1153+
try:
1154+
source = os.fspath(source)
1155+
except TypeError:
1156+
pass
1157+
else:
1158+
copyfile2(source, str(self))
1159+
return
1160+
self._copy_from_file_fallback(source, preserve_metadata)
1161+
1162+
if os.name == 'nt':
1163+
# If a directory-symlink is copied *before* its target, then
1164+
# os.symlink() incorrectly creates a file-symlink on Windows. Avoid
1165+
# this by passing *target_is_dir* to os.symlink() on Windows.
1166+
def _copy_from_symlink(self, source, preserve_metadata=False):
1167+
os.symlink(str(source.readlink()), self, source.info.is_dir())
1168+
if preserve_metadata:
1169+
copy_info(source.info, self, follow_symlinks=False)
1170+
else:
1171+
def _copy_from_symlink(self, source, preserve_metadata=False):
1172+
os.symlink(str(source.readlink()), self)
1173+
if preserve_metadata:
1174+
copy_info(source.info, self, follow_symlinks=False)
11271175

11281176
def move(self, target):
11291177
"""

Lib/pathlib/_os.py

+3-39
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,16 @@ def _sendfile(source_fd, target_fd):
102102

103103

104104
if _winapi and hasattr(_winapi, 'CopyFile2'):
105-
def _copyfile2(source, target):
105+
def copyfile2(source, target):
106106
"""
107107
Copy from one file to another using CopyFile2 (Windows only).
108108
"""
109109
_winapi.CopyFile2(source, target, 0)
110110
else:
111-
_copyfile2 = None
111+
copyfile2 = None
112112

113113

114-
def _copyfileobj(source_f, target_f):
114+
def copyfileobj(source_f, target_f):
115115
"""
116116
Copy data from file-like object source_f to file-like object target_f.
117117
"""
@@ -242,42 +242,6 @@ def ensure_different_files(source, target):
242242
raise err
243243

244244

245-
def copy_file(source, target, follow_symlinks=True, preserve_metadata=False):
246-
"""
247-
Recursively copy the given source ReadablePath to the given target WritablePath.
248-
"""
249-
info = source.info
250-
if not follow_symlinks and info.is_symlink():
251-
target.symlink_to(str(source.readlink()), info.is_dir())
252-
if preserve_metadata:
253-
target._write_info(info, follow_symlinks=False)
254-
elif info.is_dir():
255-
children = source.iterdir()
256-
target.mkdir()
257-
for src in children:
258-
dst = target.joinpath(src.name)
259-
copy_file(src, dst, follow_symlinks, preserve_metadata)
260-
if preserve_metadata:
261-
target._write_info(info)
262-
else:
263-
if _copyfile2:
264-
# Use fast OS routine for local file copying where available.
265-
try:
266-
source_p = os.fspath(source)
267-
target_p = os.fspath(target)
268-
except TypeError:
269-
pass
270-
else:
271-
_copyfile2(source_p, target_p)
272-
return
273-
ensure_different_files(source, target)
274-
with magic_open(source, 'rb') as source_f:
275-
with magic_open(target, 'wb') as target_f:
276-
_copyfileobj(source_f, target_f)
277-
if preserve_metadata:
278-
target._write_info(info)
279-
280-
281245
def copy_info(info, target, follow_symlinks=True):
282246
"""Copy metadata from the given PathInfo to the given local path."""
283247
copy_times_ns = (

Lib/pathlib/types.py

+28-12
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
from abc import ABC, abstractmethod
1414
from glob import _PathGlobber
15+
from pathlib._os import magic_open, ensure_distinct_paths, ensure_different_files, copyfileobj
1516
from pathlib import PurePath, Path
16-
from pathlib._os import magic_open, ensure_distinct_paths, copy_file
1717
from typing import Optional, Protocol, runtime_checkable
1818

1919

@@ -332,18 +332,21 @@ def readlink(self):
332332
"""
333333
raise NotImplementedError
334334

335-
def copy(self, target, follow_symlinks=True, preserve_metadata=False):
335+
def copy(self, target, **kwargs):
336336
"""
337337
Recursively copy this file or directory tree to the given destination.
338338
"""
339339
if not hasattr(target, 'with_segments'):
340340
target = self.with_segments(target)
341341
ensure_distinct_paths(self, target)
342-
copy_file(self, target, follow_symlinks, preserve_metadata)
342+
try:
343+
copy_to_target = target._copy_from
344+
except AttributeError:
345+
raise TypeError(f"Target path is not writable: {target!r}") from None
346+
copy_to_target(self, **kwargs)
343347
return target.joinpath() # Empty join to ensure fresh metadata.
344348

345-
def copy_into(self, target_dir, *, follow_symlinks=True,
346-
preserve_metadata=False):
349+
def copy_into(self, target_dir, **kwargs):
347350
"""
348351
Copy this file or directory tree into the given existing directory.
349352
"""
@@ -354,8 +357,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
354357
target = target_dir / name
355358
else:
356359
target = self.with_segments(target_dir, name)
357-
return self.copy(target, follow_symlinks=follow_symlinks,
358-
preserve_metadata=preserve_metadata)
360+
return self.copy(target, **kwargs)
359361

360362

361363
class _WritablePath(_JoinablePath):
@@ -409,11 +411,25 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
409411
with magic_open(self, mode='w', encoding=encoding, errors=errors, newline=newline) as f:
410412
return f.write(data)
411413

412-
def _write_info(self, info, follow_symlinks=True):
413-
"""
414-
Write the given PathInfo to this path.
415-
"""
416-
pass
414+
def _copy_from(self, source, follow_symlinks=True):
415+
"""
416+
Recursively copy the given path to this path.
417+
"""
418+
stack = [(source, self)]
419+
while stack:
420+
src, dst = stack.pop()
421+
if not follow_symlinks and src.info.is_symlink():
422+
dst.symlink_to(str(src.readlink()), src.info.is_dir())
423+
elif src.info.is_dir():
424+
children = src.iterdir()
425+
dst.mkdir()
426+
for child in children:
427+
stack.append((child, dst.joinpath(child.name)))
428+
else:
429+
ensure_different_files(src, dst)
430+
with magic_open(src, 'rb') as source_f:
431+
with magic_open(dst, 'wb') as target_f:
432+
copyfileobj(source_f, target_f)
417433

418434

419435
_JoinablePath.register(PurePath)

0 commit comments

Comments
 (0)