Skip to content

Commit 777af5f

Browse files
committed
pathlib ABCs: yield progress reports from WritablePath._copy_from()
Make `WritablePath._copy_from()` yield `(target, source, part_size)` tuples when copying files and directories. A tuple with `part_size=0` is emitted for every path encountered, and further tuples with `part_size>0` **may** be emitted when copying regular files. This should allow `anyio.Path` to wrap `_copy_from()` and make it cancelable.
1 parent f3bf304 commit 777af5f

File tree

3 files changed

+22
-14
lines changed

3 files changed

+22
-14
lines changed

Lib/pathlib/__init__.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,7 @@ def copy(self, target, **kwargs):
11091109
copy_to_target = target._copy_from
11101110
except AttributeError:
11111111
raise TypeError(f"Target path is not writable: {target!r}") from None
1112-
copy_to_target(self, **kwargs)
1112+
list(copy_to_target(self, **kwargs)) # Consume generator.
11131113
return target.joinpath() # Empty join to ensure fresh metadata.
11141114

11151115
def copy_into(self, target_dir, **kwargs):
@@ -1127,26 +1127,30 @@ def copy_into(self, target_dir, **kwargs):
11271127

11281128
def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False):
11291129
"""
1130-
Recursively copy the given path to this path.
1130+
Recursively copy the given path to this path. This a generator
1131+
function that yields (target, source, part_size) tuples as the copying
1132+
operation progresses.
11311133
"""
1134+
yield self, source, 0
11321135
if not follow_symlinks and source.info.is_symlink():
11331136
self._copy_from_symlink(source, preserve_metadata)
11341137
elif source.info.is_dir():
11351138
children = source.iterdir()
11361139
os.mkdir(self)
11371140
for child in children:
1138-
self.joinpath(child.name)._copy_from(
1141+
yield from self.joinpath(child.name)._copy_from(
11391142
child, follow_symlinks, preserve_metadata)
11401143
if preserve_metadata:
11411144
copy_info(source.info, self)
11421145
else:
1143-
self._copy_from_file(source, preserve_metadata)
1146+
for part_size in self._copy_from_file(source, preserve_metadata):
1147+
yield self, source, part_size
11441148

11451149
def _copy_from_file(self, source, preserve_metadata=False):
11461150
ensure_different_files(source, self)
11471151
with magic_open(source, 'rb') as source_f:
11481152
with open(self, 'wb') as target_f:
1149-
copyfileobj(source_f, target_f)
1153+
yield from copyfileobj(source_f, target_f)
11501154
if preserve_metadata:
11511155
copy_info(source.info, self)
11521156

@@ -1160,8 +1164,8 @@ def _copy_from_file(self, source, preserve_metadata=False):
11601164
pass
11611165
else:
11621166
copyfile2(source, str(self))
1163-
return
1164-
self._copy_from_file_fallback(source, preserve_metadata)
1167+
return iter([])
1168+
return self._copy_from_file_fallback(source, preserve_metadata)
11651169

11661170
if os.name == 'nt':
11671171
# If a directory-symlink is copied *before* its target, then

Lib/pathlib/_os.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def copyfileobj(source_f, target_f):
163163
read_source = source_f.read
164164
write_target = target_f.write
165165
while buf := read_source(1024 * 1024):
166-
write_target(buf)
166+
yield write_target(buf)
167167

168168

169169
def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None,

Lib/pathlib/types.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def copy(self, target, **kwargs):
343343
copy_to_target = target._copy_from
344344
except AttributeError:
345345
raise TypeError(f"Target path is not writable: {target!r}") from None
346-
copy_to_target(self, **kwargs)
346+
list(copy_to_target(self, **kwargs)) # Consume generator.
347347
return target.joinpath() # Empty join to ensure fresh metadata.
348348

349349
def copy_into(self, target_dir, **kwargs):
@@ -413,23 +413,27 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
413413

414414
def _copy_from(self, source, follow_symlinks=True):
415415
"""
416-
Recursively copy the given path to this path.
416+
Recursively copy the given path to this path. This a generator
417+
function that yields (target, source, part_size) tuples as the copying
418+
operation progresses.
417419
"""
418-
stack = [(source, self)]
420+
stack = [(self, source)]
419421
while stack:
420-
src, dst = stack.pop()
422+
dst, src = stack.pop()
423+
yield dst, src, 0
421424
if not follow_symlinks and src.info.is_symlink():
422425
dst.symlink_to(str(src.readlink()), src.info.is_dir())
423426
elif src.info.is_dir():
424427
children = src.iterdir()
425428
dst.mkdir()
426429
for child in children:
427-
stack.append((child, dst.joinpath(child.name)))
430+
stack.append((dst.joinpath(child.name), child))
428431
else:
429432
ensure_different_files(src, dst)
430433
with magic_open(src, 'rb') as source_f:
431434
with magic_open(dst, 'wb') as target_f:
432-
copyfileobj(source_f, target_f)
435+
for part_size in copyfileobj(source_f, target_f):
436+
yield dst, src, part_size
433437

434438

435439
_JoinablePath.register(PurePath)

0 commit comments

Comments
 (0)