Skip to content

Commit c88dacb

Browse files
authored
GH-125413: Move pathlib.Path.copy() implementation alongside Path.info (#129856)
Move pathlib's private `CopyReader`, `LocalCopyReader`, `CopyWriter` and `LocalCopyWriter` classes into `pathlib._os`, where they can live alongside the low-level copying functions (`copyfileobj()` etc) and high-level path querying interface (`PathInfo`). This sets the stage for merging `LocalCopyReader` into `PathInfo`. No change of behaviour; just moving some code around.
1 parent d5796e6 commit c88dacb

File tree

3 files changed

+297
-300
lines changed

3 files changed

+297
-300
lines changed

Lib/pathlib/_abc.py

Lines changed: 1 addition & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@
1212
"""
1313

1414
import functools
15-
import io
1615
import posixpath
17-
from errno import EINVAL
1816
from glob import _PathGlobber, _no_recurse_symlinks
19-
from pathlib._os import copyfileobj
17+
from pathlib._os import magic_open, CopyReader, CopyWriter
2018

2119

2220
@functools.cache
@@ -41,162 +39,6 @@ def _explode_path(path):
4139
return path, names
4240

4341

44-
def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None,
45-
newline=None):
46-
"""
47-
Open the file pointed to by this path and return a file object, as
48-
the built-in open() function does.
49-
"""
50-
try:
51-
return io.open(path, mode, buffering, encoding, errors, newline)
52-
except TypeError:
53-
pass
54-
cls = type(path)
55-
text = 'b' not in mode
56-
mode = ''.join(sorted(c for c in mode if c not in 'bt'))
57-
if text:
58-
try:
59-
attr = getattr(cls, f'__open_{mode}__')
60-
except AttributeError:
61-
pass
62-
else:
63-
return attr(path, buffering, encoding, errors, newline)
64-
65-
try:
66-
attr = getattr(cls, f'__open_{mode}b__')
67-
except AttributeError:
68-
pass
69-
else:
70-
stream = attr(path, buffering)
71-
if text:
72-
stream = io.TextIOWrapper(stream, encoding, errors, newline)
73-
return stream
74-
75-
raise TypeError(f"{cls.__name__} can't be opened with mode {mode!r}")
76-
77-
78-
class CopyReader:
79-
"""
80-
Class that implements the "read" part of copying between path objects.
81-
An instance of this class is available from the ReadablePath._copy_reader
82-
property.
83-
"""
84-
__slots__ = ('_path',)
85-
86-
def __init__(self, path):
87-
self._path = path
88-
89-
_readable_metakeys = frozenset()
90-
91-
def _read_metadata(self, metakeys, *, follow_symlinks=True):
92-
"""
93-
Returns path metadata as a dict with string keys.
94-
"""
95-
raise NotImplementedError
96-
97-
98-
class CopyWriter:
99-
"""
100-
Class that implements the "write" part of copying between path objects. An
101-
instance of this class is available from the WritablePath._copy_writer
102-
property.
103-
"""
104-
__slots__ = ('_path',)
105-
106-
def __init__(self, path):
107-
self._path = path
108-
109-
_writable_metakeys = frozenset()
110-
111-
def _write_metadata(self, metadata, *, follow_symlinks=True):
112-
"""
113-
Sets path metadata from the given dict with string keys.
114-
"""
115-
raise NotImplementedError
116-
117-
def _create(self, source, follow_symlinks, dirs_exist_ok, preserve_metadata):
118-
self._ensure_distinct_path(source)
119-
if preserve_metadata:
120-
metakeys = self._writable_metakeys & source._copy_reader._readable_metakeys
121-
else:
122-
metakeys = None
123-
if not follow_symlinks and source.is_symlink():
124-
self._create_symlink(source, metakeys)
125-
elif source.is_dir():
126-
self._create_dir(source, metakeys, follow_symlinks, dirs_exist_ok)
127-
else:
128-
self._create_file(source, metakeys)
129-
return self._path
130-
131-
def _create_dir(self, source, metakeys, follow_symlinks, dirs_exist_ok):
132-
"""Copy the given directory to our path."""
133-
children = list(source.iterdir())
134-
self._path.mkdir(exist_ok=dirs_exist_ok)
135-
for src in children:
136-
dst = self._path.joinpath(src.name)
137-
if not follow_symlinks and src.is_symlink():
138-
dst._copy_writer._create_symlink(src, metakeys)
139-
elif src.is_dir():
140-
dst._copy_writer._create_dir(src, metakeys, follow_symlinks, dirs_exist_ok)
141-
else:
142-
dst._copy_writer._create_file(src, metakeys)
143-
if metakeys:
144-
metadata = source._copy_reader._read_metadata(metakeys)
145-
if metadata:
146-
self._write_metadata(metadata)
147-
148-
def _create_file(self, source, metakeys):
149-
"""Copy the given file to our path."""
150-
self._ensure_different_file(source)
151-
with magic_open(source, 'rb') as source_f:
152-
try:
153-
with magic_open(self._path, 'wb') as target_f:
154-
copyfileobj(source_f, target_f)
155-
except IsADirectoryError as e:
156-
if not self._path.exists():
157-
# Raise a less confusing exception.
158-
raise FileNotFoundError(
159-
f'Directory does not exist: {self._path}') from e
160-
raise
161-
if metakeys:
162-
metadata = source._copy_reader._read_metadata(metakeys)
163-
if metadata:
164-
self._write_metadata(metadata)
165-
166-
def _create_symlink(self, source, metakeys):
167-
"""Copy the given symbolic link to our path."""
168-
self._path.symlink_to(source.readlink())
169-
if metakeys:
170-
metadata = source._copy_reader._read_metadata(metakeys, follow_symlinks=False)
171-
if metadata:
172-
self._write_metadata(metadata, follow_symlinks=False)
173-
174-
def _ensure_different_file(self, source):
175-
"""
176-
Raise OSError(EINVAL) if both paths refer to the same file.
177-
"""
178-
pass
179-
180-
def _ensure_distinct_path(self, source):
181-
"""
182-
Raise OSError(EINVAL) if the other path is within this path.
183-
"""
184-
# Note: there is no straightforward, foolproof algorithm to determine
185-
# if one directory is within another (a particularly perverse example
186-
# would be a single network share mounted in one location via NFS, and
187-
# in another location via CIFS), so we simply checks whether the
188-
# other path is lexically equal to, or within, this path.
189-
if source == self._path:
190-
err = OSError(EINVAL, "Source and target are the same path")
191-
elif source in self._path.parents:
192-
err = OSError(EINVAL, "Source path is a parent of target path")
193-
else:
194-
return
195-
err.filename = str(source)
196-
err.filename2 = str(self._path)
197-
raise err
198-
199-
20042
class JoinablePath:
20143
"""Base class for pure path objects.
20244

Lib/pathlib/_local.py

Lines changed: 5 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from errno import *
88
from glob import _StringGlobber, _no_recurse_symlinks
99
from itertools import chain
10-
from stat import S_IMODE, S_ISDIR, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
10+
from stat import S_ISDIR, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
1111
from _collections_abc import Sequence
1212

1313
try:
@@ -19,8 +19,8 @@
1919
except ImportError:
2020
grp = None
2121

22-
from pathlib._os import copyfile, PathInfo, DirEntryInfo
23-
from pathlib._abc import CopyReader, CopyWriter, JoinablePath, ReadablePath, WritablePath
22+
from pathlib._os import LocalCopyReader, LocalCopyWriter, PathInfo, DirEntryInfo
23+
from pathlib._abc import JoinablePath, ReadablePath, WritablePath
2424

2525

2626
__all__ = [
@@ -65,141 +65,6 @@ def __repr__(self):
6565
return "<{}.parents>".format(type(self._path).__name__)
6666

6767

68-
class _LocalCopyReader(CopyReader):
69-
"""This object implements the "read" part of copying local paths. Don't
70-
try to construct it yourself.
71-
"""
72-
__slots__ = ()
73-
74-
_readable_metakeys = {'mode', 'times_ns'}
75-
if hasattr(os.stat_result, 'st_flags'):
76-
_readable_metakeys.add('flags')
77-
if hasattr(os, 'listxattr'):
78-
_readable_metakeys.add('xattrs')
79-
_readable_metakeys = frozenset(_readable_metakeys)
80-
81-
def _read_metadata(self, metakeys, *, follow_symlinks=True):
82-
metadata = {}
83-
if 'mode' in metakeys or 'times_ns' in metakeys or 'flags' in metakeys:
84-
st = self._path.stat(follow_symlinks=follow_symlinks)
85-
if 'mode' in metakeys:
86-
metadata['mode'] = S_IMODE(st.st_mode)
87-
if 'times_ns' in metakeys:
88-
metadata['times_ns'] = st.st_atime_ns, st.st_mtime_ns
89-
if 'flags' in metakeys:
90-
metadata['flags'] = st.st_flags
91-
if 'xattrs' in metakeys:
92-
try:
93-
metadata['xattrs'] = [
94-
(attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks))
95-
for attr in os.listxattr(self._path, follow_symlinks=follow_symlinks)]
96-
except OSError as err:
97-
if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
98-
raise
99-
return metadata
100-
101-
102-
class _LocalCopyWriter(CopyWriter):
103-
"""This object implements the "write" part of copying local paths. Don't
104-
try to construct it yourself.
105-
"""
106-
__slots__ = ()
107-
108-
_writable_metakeys = _LocalCopyReader._readable_metakeys
109-
110-
def _write_metadata(self, metadata, *, follow_symlinks=True):
111-
def _nop(*args, ns=None, follow_symlinks=None):
112-
pass
113-
114-
if follow_symlinks:
115-
# use the real function if it exists
116-
def lookup(name):
117-
return getattr(os, name, _nop)
118-
else:
119-
# use the real function only if it exists
120-
# *and* it supports follow_symlinks
121-
def lookup(name):
122-
fn = getattr(os, name, _nop)
123-
if fn in os.supports_follow_symlinks:
124-
return fn
125-
return _nop
126-
127-
times_ns = metadata.get('times_ns')
128-
if times_ns is not None:
129-
lookup("utime")(self._path, ns=times_ns, follow_symlinks=follow_symlinks)
130-
# We must copy extended attributes before the file is (potentially)
131-
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
132-
xattrs = metadata.get('xattrs')
133-
if xattrs is not None:
134-
for attr, value in xattrs:
135-
try:
136-
os.setxattr(self._path, attr, value, follow_symlinks=follow_symlinks)
137-
except OSError as e:
138-
if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
139-
raise
140-
mode = metadata.get('mode')
141-
if mode is not None:
142-
try:
143-
lookup("chmod")(self._path, mode, follow_symlinks=follow_symlinks)
144-
except NotImplementedError:
145-
# if we got a NotImplementedError, it's because
146-
# * follow_symlinks=False,
147-
# * lchown() is unavailable, and
148-
# * either
149-
# * fchownat() is unavailable or
150-
# * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW.
151-
# (it returned ENOSUP.)
152-
# therefore we're out of options--we simply cannot chown the
153-
# symlink. give up, suppress the error.
154-
# (which is what shutil always did in this circumstance.)
155-
pass
156-
flags = metadata.get('flags')
157-
if flags is not None:
158-
try:
159-
lookup("chflags")(self._path, flags, follow_symlinks=follow_symlinks)
160-
except OSError as why:
161-
if why.errno not in (EOPNOTSUPP, ENOTSUP):
162-
raise
163-
164-
if copyfile:
165-
# Use fast OS routine for local file copying where available.
166-
def _create_file(self, source, metakeys):
167-
"""Copy the given file to the given target."""
168-
try:
169-
source = os.fspath(source)
170-
except TypeError:
171-
if not isinstance(source, WritablePath):
172-
raise
173-
super()._create_file(source, metakeys)
174-
else:
175-
copyfile(source, os.fspath(self._path))
176-
177-
if os.name == 'nt':
178-
# Windows: symlink target might not exist yet if we're copying several
179-
# files, so ensure we pass is_dir to os.symlink().
180-
def _create_symlink(self, source, metakeys):
181-
"""Copy the given symlink to the given target."""
182-
self._path.symlink_to(source.readlink(), source.is_dir())
183-
if metakeys:
184-
metadata = source._copy_reader._read_metadata(metakeys, follow_symlinks=False)
185-
if metadata:
186-
self._write_metadata(metadata, follow_symlinks=False)
187-
188-
def _ensure_different_file(self, source):
189-
"""
190-
Raise OSError(EINVAL) if both paths refer to the same file.
191-
"""
192-
try:
193-
if not self._path.samefile(source):
194-
return
195-
except (OSError, ValueError):
196-
return
197-
err = OSError(EINVAL, "Source and target are the same file")
198-
err.filename = str(source)
199-
err.filename2 = str(self._path)
200-
raise err
201-
202-
20368
class PurePath(JoinablePath):
20469
"""Base class for manipulating paths without I/O.
20570
@@ -1190,8 +1055,8 @@ def replace(self, target):
11901055
os.replace(self, target)
11911056
return self.with_segments(target)
11921057

1193-
_copy_reader = property(_LocalCopyReader)
1194-
_copy_writer = property(_LocalCopyWriter)
1058+
_copy_reader = property(LocalCopyReader)
1059+
_copy_writer = property(LocalCopyWriter)
11951060

11961061
def move(self, target):
11971062
"""

0 commit comments

Comments
 (0)