Skip to content

Commit 8d9f52a

Browse files
authored
GH-127807: pathlib ABCs: move private copying methods to dedicated class (#127810)
Move 9 private `PathBase` attributes and methods into a new `CopyWorker` class. Change `PathBase.copy` from a method to a `CopyWorker` instance. The methods remain private in the `CopyWorker` class. In future we might make some/all of them public so that user subclasses of `PathBase` can customize the copying process (in particular reading/writing of metadata,) but we'd need to make `PathBase` public first.
1 parent f5ba74b commit 8d9f52a

File tree

3 files changed

+261
-248
lines changed

3 files changed

+261
-248
lines changed

Lib/pathlib/_abc.py

Lines changed: 127 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,132 @@ def concat_path(path, text):
5757
return path.with_segments(str(path) + text)
5858

5959

60+
class CopyWorker:
61+
"""
62+
Class that implements copying between path objects. An instance of this
63+
class is available from the PathBase.copy property; it's made callable so
64+
that PathBase.copy() can be treated as a method.
65+
66+
The target path's CopyWorker drives the process from its _create() method.
67+
Files and directories are exchanged by calling methods on the source and
68+
target paths, and metadata is exchanged by calling
69+
source.copy._read_metadata() and target.copy._write_metadata().
70+
"""
71+
__slots__ = ('_path',)
72+
73+
def __init__(self, path):
74+
self._path = path
75+
76+
def __call__(self, target, follow_symlinks=True, dirs_exist_ok=False,
77+
preserve_metadata=False):
78+
"""
79+
Recursively copy this file or directory tree to the given destination.
80+
"""
81+
if not isinstance(target, PathBase):
82+
target = self._path.with_segments(target)
83+
84+
# Delegate to the target path's CopyWorker object.
85+
return target.copy._create(self._path, follow_symlinks, dirs_exist_ok, preserve_metadata)
86+
87+
_readable_metakeys = frozenset()
88+
89+
def _read_metadata(self, metakeys, *, follow_symlinks=True):
90+
"""
91+
Returns path metadata as a dict with string keys.
92+
"""
93+
raise NotImplementedError
94+
95+
_writable_metakeys = frozenset()
96+
97+
def _write_metadata(self, metadata, *, follow_symlinks=True):
98+
"""
99+
Sets path metadata from the given dict with string keys.
100+
"""
101+
raise NotImplementedError
102+
103+
def _create(self, source, follow_symlinks, dirs_exist_ok, preserve_metadata):
104+
self._ensure_distinct_path(source)
105+
if preserve_metadata:
106+
metakeys = self._writable_metakeys & source.copy._readable_metakeys
107+
else:
108+
metakeys = None
109+
if not follow_symlinks and source.is_symlink():
110+
self._create_symlink(source, metakeys)
111+
elif source.is_dir():
112+
self._create_dir(source, metakeys, follow_symlinks, dirs_exist_ok)
113+
else:
114+
self._create_file(source, metakeys)
115+
return self._path
116+
117+
def _create_dir(self, source, metakeys, follow_symlinks, dirs_exist_ok):
118+
"""Copy the given directory to our path."""
119+
children = list(source.iterdir())
120+
self._path.mkdir(exist_ok=dirs_exist_ok)
121+
for src in children:
122+
dst = self._path.joinpath(src.name)
123+
if not follow_symlinks and src.is_symlink():
124+
dst.copy._create_symlink(src, metakeys)
125+
elif src.is_dir():
126+
dst.copy._create_dir(src, metakeys, follow_symlinks, dirs_exist_ok)
127+
else:
128+
dst.copy._create_file(src, metakeys)
129+
if metakeys:
130+
metadata = source.copy._read_metadata(metakeys)
131+
if metadata:
132+
self._write_metadata(metadata)
133+
134+
def _create_file(self, source, metakeys):
135+
"""Copy the given file to our path."""
136+
self._ensure_different_file(source)
137+
with source.open('rb') as source_f:
138+
try:
139+
with self._path.open('wb') as target_f:
140+
copyfileobj(source_f, target_f)
141+
except IsADirectoryError as e:
142+
if not self._path.exists():
143+
# Raise a less confusing exception.
144+
raise FileNotFoundError(
145+
f'Directory does not exist: {self._path}') from e
146+
raise
147+
if metakeys:
148+
metadata = source.copy._read_metadata(metakeys)
149+
if metadata:
150+
self._write_metadata(metadata)
151+
152+
def _create_symlink(self, source, metakeys):
153+
"""Copy the given symbolic link to our path."""
154+
self._path.symlink_to(source.readlink())
155+
if metakeys:
156+
metadata = source.copy._read_metadata(metakeys, follow_symlinks=False)
157+
if metadata:
158+
self._write_metadata(metadata, follow_symlinks=False)
159+
160+
def _ensure_different_file(self, source):
161+
"""
162+
Raise OSError(EINVAL) if both paths refer to the same file.
163+
"""
164+
pass
165+
166+
def _ensure_distinct_path(self, source):
167+
"""
168+
Raise OSError(EINVAL) if the other path is within this path.
169+
"""
170+
# Note: there is no straightforward, foolproof algorithm to determine
171+
# if one directory is within another (a particularly perverse example
172+
# would be a single network share mounted in one location via NFS, and
173+
# in another location via CIFS), so we simply checks whether the
174+
# other path is lexically equal to, or within, this path.
175+
if source == self._path:
176+
err = OSError(EINVAL, "Source and target are the same path")
177+
elif source in self._path.parents:
178+
err = OSError(EINVAL, "Source path is a parent of target path")
179+
else:
180+
return
181+
err.filename = str(source)
182+
err.filename2 = str(self._path)
183+
raise err
184+
185+
60186
class PurePathBase:
61187
"""Base class for pure path objects.
62188
@@ -374,31 +500,6 @@ def is_symlink(self):
374500
except (OSError, ValueError):
375501
return False
376502

377-
def _ensure_different_file(self, other_path):
378-
"""
379-
Raise OSError(EINVAL) if both paths refer to the same file.
380-
"""
381-
pass
382-
383-
def _ensure_distinct_path(self, other_path):
384-
"""
385-
Raise OSError(EINVAL) if the other path is within this path.
386-
"""
387-
# Note: there is no straightforward, foolproof algorithm to determine
388-
# if one directory is within another (a particularly perverse example
389-
# would be a single network share mounted in one location via NFS, and
390-
# in another location via CIFS), so we simply checks whether the
391-
# other path is lexically equal to, or within, this path.
392-
if self == other_path:
393-
err = OSError(EINVAL, "Source and target are the same path")
394-
elif self in other_path.parents:
395-
err = OSError(EINVAL, "Source path is a parent of target path")
396-
else:
397-
return
398-
err.filename = str(self)
399-
err.filename2 = str(other_path)
400-
raise err
401-
402503
def open(self, mode='r', buffering=-1, encoding=None,
403504
errors=None, newline=None):
404505
"""
@@ -537,88 +638,13 @@ def symlink_to(self, target, target_is_directory=False):
537638
"""
538639
raise NotImplementedError
539640

540-
def _symlink_to_target_of(self, link):
541-
"""
542-
Make this path a symlink with the same target as the given link. This
543-
is used by copy().
544-
"""
545-
self.symlink_to(link.readlink())
546-
547641
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
548642
"""
549643
Create a new directory at this given path.
550644
"""
551645
raise NotImplementedError
552646

553-
# Metadata keys supported by this path type.
554-
_readable_metadata = _writable_metadata = frozenset()
555-
556-
def _read_metadata(self, keys=None, *, follow_symlinks=True):
557-
"""
558-
Returns path metadata as a dict with string keys.
559-
"""
560-
raise NotImplementedError
561-
562-
def _write_metadata(self, metadata, *, follow_symlinks=True):
563-
"""
564-
Sets path metadata from the given dict with string keys.
565-
"""
566-
raise NotImplementedError
567-
568-
def _copy_metadata(self, target, *, follow_symlinks=True):
569-
"""
570-
Copies metadata (permissions, timestamps, etc) from this path to target.
571-
"""
572-
# Metadata types supported by both source and target.
573-
keys = self._readable_metadata & target._writable_metadata
574-
if keys:
575-
metadata = self._read_metadata(keys, follow_symlinks=follow_symlinks)
576-
target._write_metadata(metadata, follow_symlinks=follow_symlinks)
577-
578-
def _copy_file(self, target):
579-
"""
580-
Copy the contents of this file to the given target.
581-
"""
582-
self._ensure_different_file(target)
583-
with self.open('rb') as source_f:
584-
try:
585-
with target.open('wb') as target_f:
586-
copyfileobj(source_f, target_f)
587-
except IsADirectoryError as e:
588-
if not target.exists():
589-
# Raise a less confusing exception.
590-
raise FileNotFoundError(
591-
f'Directory does not exist: {target}') from e
592-
else:
593-
raise
594-
595-
def copy(self, target, *, follow_symlinks=True, dirs_exist_ok=False,
596-
preserve_metadata=False):
597-
"""
598-
Recursively copy this file or directory tree to the given destination.
599-
"""
600-
if not isinstance(target, PathBase):
601-
target = self.with_segments(target)
602-
self._ensure_distinct_path(target)
603-
stack = [(self, target)]
604-
while stack:
605-
src, dst = stack.pop()
606-
if not follow_symlinks and src.is_symlink():
607-
dst._symlink_to_target_of(src)
608-
if preserve_metadata:
609-
src._copy_metadata(dst, follow_symlinks=False)
610-
elif src.is_dir():
611-
children = src.iterdir()
612-
dst.mkdir(exist_ok=dirs_exist_ok)
613-
stack.extend((child, dst.joinpath(child.name))
614-
for child in children)
615-
if preserve_metadata:
616-
src._copy_metadata(dst)
617-
else:
618-
src._copy_file(dst)
619-
if preserve_metadata:
620-
src._copy_metadata(dst)
621-
return target
647+
copy = property(CopyWorker, doc=CopyWorker.__call__.__doc__)
622648

623649
def copy_into(self, target_dir, *, follow_symlinks=True,
624650
dirs_exist_ok=False, preserve_metadata=False):

0 commit comments

Comments
 (0)