Skip to content

GH-130614: pathlib ABCs: improve support for receiving path metadata #131259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 61 additions & 13 deletions Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from pathlib._os import (
PathInfo, DirEntryInfo,
ensure_different_files, ensure_distinct_paths,
copy_file, copy_info,
copyfile2, copyfileobj, magic_open, copy_info,
)


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

def _write_info(self, info, follow_symlinks=True):
"""
Write the given PathInfo to this path.
"""
copy_info(info, self, follow_symlinks=follow_symlinks)

_remove_leading_dot = operator.itemgetter(slice(2, None))
_remove_trailing_slash = operator.itemgetter(slice(-1))

Expand Down Expand Up @@ -1100,18 +1094,21 @@ def replace(self, target):
target = self.with_segments(target)
return target

def copy(self, target, follow_symlinks=True, preserve_metadata=False):
def copy(self, target, **kwargs):
"""
Recursively copy this file or directory tree to the given destination.
"""
if not hasattr(target, 'with_segments'):
target = self.with_segments(target)
ensure_distinct_paths(self, target)
copy_file(self, target, follow_symlinks, preserve_metadata)
try:
copy_to_target = target._copy_from
except AttributeError:
raise TypeError(f"Target path is not writable: {target!r}") from None
copy_to_target(self, **kwargs)
return target.joinpath() # Empty join to ensure fresh metadata.

def copy_into(self, target_dir, *, follow_symlinks=True,
preserve_metadata=False):
def copy_into(self, target_dir, **kwargs):
"""
Copy this file or directory tree into the given existing directory.
"""
Expand All @@ -1122,8 +1119,59 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
target = target_dir / name
else:
target = self.with_segments(target_dir, name)
return self.copy(target, follow_symlinks=follow_symlinks,
preserve_metadata=preserve_metadata)
return self.copy(target, **kwargs)

def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False):
"""
Recursively copy the given path to this path.
"""
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(
child, follow_symlinks, preserve_metadata)
if preserve_metadata:
copy_info(source.info, self)
else:
self._copy_from_file(source, preserve_metadata)

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)
if preserve_metadata:
copy_info(source.info, self)

if copyfile2:
# Use fast OS routine for local file copying where available.
_copy_from_file_fallback = _copy_from_file
def _copy_from_file(self, source, preserve_metadata=False):
try:
source = os.fspath(source)
except TypeError:
pass
else:
copyfile2(source, str(self))
return
self._copy_from_file_fallback(source, preserve_metadata)

if os.name == 'nt':
# If a directory-symlink is copied *before* its target, then
# os.symlink() incorrectly creates a file-symlink on Windows. Avoid
# this by passing *target_is_dir* to os.symlink() on Windows.
def _copy_from_symlink(self, source, preserve_metadata=False):
os.symlink(str(source.readlink()), self, source.info.is_dir())
if preserve_metadata:
copy_info(source.info, self, follow_symlinks=False)
else:
def _copy_from_symlink(self, source, preserve_metadata=False):
os.symlink(str(source.readlink()), self)
if preserve_metadata:
copy_info(source.info, self, follow_symlinks=False)

def move(self, target):
"""
Expand Down
42 changes: 3 additions & 39 deletions Lib/pathlib/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,16 @@ def _sendfile(source_fd, target_fd):


if _winapi and hasattr(_winapi, 'CopyFile2'):
def _copyfile2(source, target):
def copyfile2(source, target):
"""
Copy from one file to another using CopyFile2 (Windows only).
"""
_winapi.CopyFile2(source, target, 0)
else:
_copyfile2 = None
copyfile2 = None


def _copyfileobj(source_f, target_f):
def copyfileobj(source_f, target_f):
"""
Copy data from file-like object source_f to file-like object target_f.
"""
Expand Down Expand Up @@ -242,42 +242,6 @@ def ensure_different_files(source, target):
raise err


def copy_file(source, target, follow_symlinks=True, preserve_metadata=False):
"""
Recursively copy the given source ReadablePath to the given target WritablePath.
"""
info = source.info
if not follow_symlinks and info.is_symlink():
target.symlink_to(str(source.readlink()), info.is_dir())
if preserve_metadata:
target._write_info(info, follow_symlinks=False)
elif info.is_dir():
children = source.iterdir()
target.mkdir()
for src in children:
dst = target.joinpath(src.name)
copy_file(src, dst, follow_symlinks, preserve_metadata)
if preserve_metadata:
target._write_info(info)
else:
if _copyfile2:
# Use fast OS routine for local file copying where available.
try:
source_p = os.fspath(source)
target_p = os.fspath(target)
except TypeError:
pass
else:
_copyfile2(source_p, target_p)
return
ensure_different_files(source, target)
with magic_open(source, 'rb') as source_f:
with magic_open(target, 'wb') as target_f:
_copyfileobj(source_f, target_f)
if preserve_metadata:
target._write_info(info)


def copy_info(info, target, follow_symlinks=True):
"""Copy metadata from the given PathInfo to the given local path."""
copy_times_ns = (
Expand Down
40 changes: 28 additions & 12 deletions Lib/pathlib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

from abc import ABC, abstractmethod
from glob import _PathGlobber
from pathlib._os import magic_open, ensure_distinct_paths, ensure_different_files, copyfileobj
from pathlib import PurePath, Path
from pathlib._os import magic_open, ensure_distinct_paths, copy_file
from typing import Optional, Protocol, runtime_checkable


Expand Down Expand Up @@ -332,18 +332,21 @@ def readlink(self):
"""
raise NotImplementedError

def copy(self, target, follow_symlinks=True, preserve_metadata=False):
def copy(self, target, **kwargs):
"""
Recursively copy this file or directory tree to the given destination.
"""
if not hasattr(target, 'with_segments'):
target = self.with_segments(target)
ensure_distinct_paths(self, target)
copy_file(self, target, follow_symlinks, preserve_metadata)
try:
copy_to_target = target._copy_from
except AttributeError:
raise TypeError(f"Target path is not writable: {target!r}") from None
copy_to_target(self, **kwargs)
return target.joinpath() # Empty join to ensure fresh metadata.

def copy_into(self, target_dir, *, follow_symlinks=True,
preserve_metadata=False):
def copy_into(self, target_dir, **kwargs):
"""
Copy this file or directory tree into the given existing directory.
"""
Expand All @@ -354,8 +357,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
target = target_dir / name
else:
target = self.with_segments(target_dir, name)
return self.copy(target, follow_symlinks=follow_symlinks,
preserve_metadata=preserve_metadata)
return self.copy(target, **kwargs)


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

def _write_info(self, info, follow_symlinks=True):
"""
Write the given PathInfo to this path.
"""
pass
def _copy_from(self, source, follow_symlinks=True):
"""
Recursively copy the given path to this path.
"""
stack = [(source, self)]
while stack:
src, dst = stack.pop()
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)))
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)


_JoinablePath.register(PurePath)
Expand Down
Loading