diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index cd28f62ce3baf5..46e7a07ae10fc8 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1105,7 +1105,7 @@ def copy(self, target, **kwargs): if not hasattr(target, 'with_segments'): target = self.with_segments(target) ensure_distinct_paths(self, target) - target._copy_from(self, **kwargs) + list(target._copy_from(self, **kwargs)) # Consume generator. return target.joinpath() # Empty join to ensure fresh metadata. def copy_into(self, target_dir, **kwargs): @@ -1123,26 +1123,30 @@ def copy_into(self, target_dir, **kwargs): def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False): """ - Recursively copy the given path to this path. + Recursively copy the given path to this path. This a generator + function that yields (target, source, sent) tuples as the copying + operation progresses. """ + yield self, source, 0 if not follow_symlinks and source.info.is_symlink(): self._copy_from_symlink(source, preserve_metadata) elif source.info.is_dir(): children = source.iterdir() os.mkdir(self) for child in children: - self.joinpath(child.name)._copy_from( + yield from self.joinpath(child.name)._copy_from( child, follow_symlinks, preserve_metadata) if preserve_metadata: copy_info(source.info, self) else: - self._copy_from_file(source, preserve_metadata) + for sent in self._copy_from_file(source, preserve_metadata): + yield self, source, sent def _copy_from_file(self, source, preserve_metadata=False): ensure_different_files(source, self) with magic_open(source, 'rb') as source_f: with open(self, 'wb') as target_f: - copyfileobj(source_f, target_f) + yield from copyfileobj(source_f, target_f) if preserve_metadata: copy_info(source.info, self) @@ -1157,7 +1161,7 @@ def _copy_from_file(self, source, preserve_metadata=False): else: copyfile2(source, str(self)) return - self._copy_from_file_fallback(source, preserve_metadata) + yield from self._copy_from_file_fallback(source, preserve_metadata) if os.name == 'nt': # If a directory-symlink is copied *before* its target, then diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index e3751bbcb62377..b5228b3db8bef9 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -72,13 +72,11 @@ def _copy_file_range(source_fd, target_fd): copy. This should work on Linux >= 4.5 only. """ + fn = os.copy_file_range blocksize = _get_copy_blocksize(source_fd) offset = 0 - while True: - sent = os.copy_file_range(source_fd, target_fd, blocksize, - offset_dst=offset) - if sent == 0: - break # EOF + while sent := fn(source_fd, target_fd, blocksize, None, offset): + yield sent offset += sent else: _copy_file_range = None @@ -90,12 +88,11 @@ def _sendfile(source_fd, target_fd): high-performance sendfile(2) syscall. This should work on Linux >= 2.6.33 only. """ + fn = os.sendfile blocksize = _get_copy_blocksize(source_fd) offset = 0 - while True: - sent = os.sendfile(target_fd, source_fd, offset, blocksize) - if sent == 0: - break # EOF + while sent := fn(target_fd, source_fd, offset, blocksize): + yield sent offset += sent else: _sendfile = None @@ -141,14 +138,14 @@ def copyfileobj(source_f, target_f): raise err if _copy_file_range: try: - _copy_file_range(source_fd, target_fd) + yield from _copy_file_range(source_fd, target_fd) return except OSError as err: if err.errno not in (ETXTBSY, EXDEV): raise err if _sendfile: try: - _sendfile(source_fd, target_fd) + yield from _sendfile(source_fd, target_fd) return except OSError as err: if err.errno != ENOTSOCK: @@ -163,7 +160,7 @@ def copyfileobj(source_f, target_f): read_source = source_f.read write_target = target_f.write while buf := read_source(1024 * 1024): - write_target(buf) + yield write_target(buf) def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None, diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index d1bb8701b887c8..4c8368a54d64ba 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -333,7 +333,7 @@ def copy(self, target, **kwargs): Recursively copy this file or directory tree to the given destination. """ ensure_distinct_paths(self, target) - target._copy_from(self, **kwargs) + list(target._copy_from(self, **kwargs)) # Consume generator. return target.joinpath() # Empty join to ensure fresh metadata. def copy_into(self, target_dir, **kwargs): @@ -399,23 +399,27 @@ def write_text(self, data, encoding=None, errors=None, newline=None): def _copy_from(self, source, follow_symlinks=True): """ - Recursively copy the given path to this path. + Recursively copy the given path to this path. This a generator + function that yields (target, source, sent) tuples as the copying + operation progresses. """ - stack = [(source, self)] + stack = [(self, source)] while stack: - src, dst = stack.pop() + dst, src = stack.pop() + yield dst, src, 0 if not follow_symlinks and src.info.is_symlink(): dst.symlink_to(str(src.readlink()), src.info.is_dir()) elif src.info.is_dir(): children = src.iterdir() dst.mkdir() for child in children: - stack.append((child, dst.joinpath(child.name))) + stack.append((dst.joinpath(child.name), child)) else: ensure_different_files(src, dst) with magic_open(src, 'rb') as source_f: with magic_open(dst, 'wb') as target_f: - copyfileobj(source_f, target_f) + for sent in copyfileobj(source_f, target_f): + yield dst, src, sent _JoinablePath.register(PurePath)