mirror of
https://github.com/python/cpython.git
synced 2025-07-23 03:05:38 +00:00
GH-73991: Rework pathlib.Path.copytree()
into copy()
(#122369)
Rename `pathlib.Path.copy()` to `_copy_file()` (i.e. make it private.) Rename `pathlib.Path.copytree()` to `copy()`, and add support for copying non-directories. This simplifies the interface for users, and nicely complements the upcoming `move()` and `delete()` methods (which will also accept any type of file.) Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
This commit is contained in:
parent
ea70439bd2
commit
a6644d4464
10 changed files with 141 additions and 197 deletions
|
@ -1539,50 +1539,33 @@ Creating files and directories
|
||||||
Copying, renaming and deleting
|
Copying, renaming and deleting
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. method:: Path.copy(target, *, follow_symlinks=True, preserve_metadata=False)
|
.. method:: Path.copy(target, *, follow_symlinks=True, dirs_exist_ok=False, \
|
||||||
|
preserve_metadata=False, ignore=None, on_error=None)
|
||||||
|
|
||||||
Copy the contents of this file to the *target* file. If *target* specifies
|
Copy this file or directory tree to the given *target*, and return a new
|
||||||
a file that already exists, it will be replaced.
|
:class:`!Path` instance pointing to *target*.
|
||||||
|
|
||||||
If *follow_symlinks* is false, and this file is a symbolic link, *target*
|
If the source is a file, the target will be replaced if it is an existing
|
||||||
will be created as a symbolic link. If *follow_symlinks* is true and this
|
file. If the source is a symlink and *follow_symlinks* is true (the
|
||||||
file is a symbolic link, *target* will be a copy of the symlink target.
|
default), the symlink's target is copied. Otherwise, the symlink is
|
||||||
|
recreated at the destination.
|
||||||
|
|
||||||
If *preserve_metadata* is false (the default), only the file data is
|
If the source is a directory and *dirs_exist_ok* is false (the default), a
|
||||||
guaranteed to be copied. Set *preserve_metadata* to true to ensure that the
|
:exc:`FileExistsError` is raised if the target is an existing directory.
|
||||||
file mode (permissions), flags, last access and modification times, and
|
If *dirs_exists_ok* is true, the copying operation will overwrite
|
||||||
extended attributes are copied where supported. This argument has no effect
|
existing files within the destination tree with corresponding files
|
||||||
on Windows, where metadata is always preserved when copying.
|
from the source tree.
|
||||||
|
|
||||||
.. versionadded:: 3.14
|
If *preserve_metadata* is false (the default), only directory structures
|
||||||
|
|
||||||
|
|
||||||
.. method:: Path.copytree(target, *, follow_symlinks=True, \
|
|
||||||
preserve_metadata=False, dirs_exist_ok=False, \
|
|
||||||
ignore=None, on_error=None)
|
|
||||||
|
|
||||||
Recursively copy this directory tree to the given destination.
|
|
||||||
|
|
||||||
If a symlink is encountered in the source tree, and *follow_symlinks* is
|
|
||||||
true (the default), the symlink's target is copied. Otherwise, the symlink
|
|
||||||
is recreated in the destination tree.
|
|
||||||
|
|
||||||
If *preserve_metadata* is false (the default), only the directory structure
|
|
||||||
and file data are guaranteed to be copied. Set *preserve_metadata* to true
|
and file data are guaranteed to be copied. Set *preserve_metadata* to true
|
||||||
to ensure that file and directory permissions, flags, last access and
|
to ensure that file and directory permissions, flags, last access and
|
||||||
modification times, and extended attributes are copied where supported.
|
modification times, and extended attributes are copied where supported.
|
||||||
This argument has no effect on Windows, where metadata is always preserved
|
This argument has no effect when copying files on Windows (where
|
||||||
when copying.
|
metadata is always preserved).
|
||||||
|
|
||||||
If the destination is an existing directory and *dirs_exist_ok* is false
|
|
||||||
(the default), a :exc:`FileExistsError` is raised. Otherwise, the copying
|
|
||||||
operation will continue if it encounters existing directories, and files
|
|
||||||
within the destination tree will be overwritten by corresponding files from
|
|
||||||
the source tree.
|
|
||||||
|
|
||||||
If *ignore* is given, it should be a callable accepting one argument: a
|
If *ignore* is given, it should be a callable accepting one argument: a
|
||||||
file or directory path within the source tree. The callable may return true
|
source file or directory path. The callable may return true to suppress
|
||||||
to suppress copying of the path.
|
copying of the path.
|
||||||
|
|
||||||
If *on_error* is given, it should be a callable accepting one argument: an
|
If *on_error* is given, it should be a callable accepting one argument: an
|
||||||
instance of :exc:`OSError`. The callable may re-raise the exception or do
|
instance of :exc:`OSError`. The callable may re-raise the exception or do
|
||||||
|
|
|
@ -146,10 +146,8 @@ pathlib
|
||||||
|
|
||||||
* Add methods to :class:`pathlib.Path` to recursively copy or remove files:
|
* Add methods to :class:`pathlib.Path` to recursively copy or remove files:
|
||||||
|
|
||||||
* :meth:`~pathlib.Path.copy` copies the content of one file to another, like
|
* :meth:`~pathlib.Path.copy` copies a file or directory tree to a given
|
||||||
:func:`shutil.copyfile`.
|
destination.
|
||||||
* :meth:`~pathlib.Path.copytree` copies one directory tree to another, like
|
|
||||||
:func:`shutil.copytree`.
|
|
||||||
* :meth:`~pathlib.Path.delete` removes a file or directory tree.
|
* :meth:`~pathlib.Path.delete` removes a file or directory tree.
|
||||||
|
|
||||||
(Contributed by Barney Gale in :gh:`73991`.)
|
(Contributed by Barney Gale in :gh:`73991`.)
|
||||||
|
|
|
@ -5,8 +5,8 @@ paths with operations that have semantics appropriate for different
|
||||||
operating systems.
|
operating systems.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ._os import *
|
from pathlib._abc import *
|
||||||
from ._local import *
|
from pathlib._local import *
|
||||||
|
|
||||||
__all__ = (_os.__all__ +
|
__all__ = (_abc.__all__ +
|
||||||
_local.__all__)
|
_local.__all__)
|
||||||
|
|
|
@ -16,7 +16,16 @@ import operator
|
||||||
import posixpath
|
import posixpath
|
||||||
from glob import _GlobberBase, _no_recurse_symlinks
|
from glob import _GlobberBase, _no_recurse_symlinks
|
||||||
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
|
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
|
||||||
from ._os import UnsupportedOperation, copyfileobj
|
from pathlib._os import copyfileobj
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["UnsupportedOperation"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedOperation(NotImplementedError):
|
||||||
|
"""An exception that is raised when an unsupported operation is attempted.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
@functools.cache
|
||||||
|
@ -761,6 +770,13 @@ class PathBase(PurePathBase):
|
||||||
"""
|
"""
|
||||||
raise UnsupportedOperation(self._unsupported_msg('symlink_to()'))
|
raise UnsupportedOperation(self._unsupported_msg('symlink_to()'))
|
||||||
|
|
||||||
|
def _symlink_to_target_of(self, link):
|
||||||
|
"""
|
||||||
|
Make this path a symlink with the same target as the given link. This
|
||||||
|
is used by copy().
|
||||||
|
"""
|
||||||
|
self.symlink_to(link.readlink())
|
||||||
|
|
||||||
def hardlink_to(self, target):
|
def hardlink_to(self, target):
|
||||||
"""
|
"""
|
||||||
Make this path a hard link pointing to the same file as *target*.
|
Make this path a hard link pointing to the same file as *target*.
|
||||||
|
@ -806,21 +822,12 @@ class PathBase(PurePathBase):
|
||||||
metadata = self._read_metadata(keys, follow_symlinks=follow_symlinks)
|
metadata = self._read_metadata(keys, follow_symlinks=follow_symlinks)
|
||||||
target._write_metadata(metadata, follow_symlinks=follow_symlinks)
|
target._write_metadata(metadata, follow_symlinks=follow_symlinks)
|
||||||
|
|
||||||
def copy(self, target, *, follow_symlinks=True, preserve_metadata=False):
|
def _copy_file(self, target):
|
||||||
"""
|
"""
|
||||||
Copy the contents of this file to the given target. If this file is a
|
Copy the contents of this file to the given target.
|
||||||
symlink and follow_symlinks is false, a symlink will be created at the
|
|
||||||
target.
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(target, PathBase):
|
|
||||||
target = self.with_segments(target)
|
|
||||||
if self._samefile_safe(target):
|
if self._samefile_safe(target):
|
||||||
raise OSError(f"{self!r} and {target!r} are the same file")
|
raise OSError(f"{self!r} and {target!r} are the same file")
|
||||||
if not follow_symlinks and self.is_symlink():
|
|
||||||
target.symlink_to(self.readlink())
|
|
||||||
if preserve_metadata:
|
|
||||||
self._copy_metadata(target, follow_symlinks=False)
|
|
||||||
return
|
|
||||||
with self.open('rb') as source_f:
|
with self.open('rb') as source_f:
|
||||||
try:
|
try:
|
||||||
with target.open('wb') as target_f:
|
with target.open('wb') as target_f:
|
||||||
|
@ -832,42 +839,39 @@ class PathBase(PurePathBase):
|
||||||
f'Directory does not exist: {target}') from e
|
f'Directory does not exist: {target}') from e
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
if preserve_metadata:
|
|
||||||
self._copy_metadata(target)
|
|
||||||
|
|
||||||
def copytree(self, target, *, follow_symlinks=True,
|
def copy(self, target, *, follow_symlinks=True, dirs_exist_ok=False,
|
||||||
preserve_metadata=False, dirs_exist_ok=False,
|
preserve_metadata=False, ignore=None, on_error=None):
|
||||||
ignore=None, on_error=None):
|
|
||||||
"""
|
"""
|
||||||
Recursively copy this directory tree to the given destination.
|
Recursively copy this file or directory tree to the given destination.
|
||||||
"""
|
"""
|
||||||
if not isinstance(target, PathBase):
|
if not isinstance(target, PathBase):
|
||||||
target = self.with_segments(target)
|
target = self.with_segments(target)
|
||||||
if on_error is None:
|
|
||||||
def on_error(err):
|
|
||||||
raise err
|
|
||||||
stack = [(self, target)]
|
stack = [(self, target)]
|
||||||
while stack:
|
while stack:
|
||||||
source_dir, target_dir = stack.pop()
|
src, dst = stack.pop()
|
||||||
try:
|
try:
|
||||||
sources = source_dir.iterdir()
|
if not follow_symlinks and src.is_symlink():
|
||||||
target_dir.mkdir(exist_ok=dirs_exist_ok)
|
dst._symlink_to_target_of(src)
|
||||||
if preserve_metadata:
|
if preserve_metadata:
|
||||||
source_dir._copy_metadata(target_dir)
|
src._copy_metadata(dst, follow_symlinks=False)
|
||||||
for source in sources:
|
elif src.is_dir():
|
||||||
if ignore and ignore(source):
|
children = src.iterdir()
|
||||||
continue
|
dst.mkdir(exist_ok=dirs_exist_ok)
|
||||||
try:
|
for child in children:
|
||||||
if source.is_dir(follow_symlinks=follow_symlinks):
|
if not (ignore and ignore(child)):
|
||||||
stack.append((source, target_dir.joinpath(source.name)))
|
stack.append((child, dst.joinpath(child.name)))
|
||||||
else:
|
if preserve_metadata:
|
||||||
source.copy(target_dir.joinpath(source.name),
|
src._copy_metadata(dst)
|
||||||
follow_symlinks=follow_symlinks,
|
else:
|
||||||
preserve_metadata=preserve_metadata)
|
src._copy_file(dst)
|
||||||
except OSError as err:
|
if preserve_metadata:
|
||||||
on_error(err)
|
src._copy_metadata(dst)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
if on_error is None:
|
||||||
|
raise
|
||||||
on_error(err)
|
on_error(err)
|
||||||
|
return target
|
||||||
|
|
||||||
def rename(self, target):
|
def rename(self, target):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -18,9 +18,9 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
grp = None
|
grp = None
|
||||||
|
|
||||||
from ._os import (UnsupportedOperation, copyfile, file_metadata_keys,
|
from pathlib._os import (copyfile, file_metadata_keys, read_file_metadata,
|
||||||
read_file_metadata, write_file_metadata)
|
write_file_metadata)
|
||||||
from ._abc import PurePathBase, PathBase
|
from pathlib._abc import UnsupportedOperation, PurePathBase, PathBase
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -788,25 +788,18 @@ class Path(PathBase, PurePath):
|
||||||
_write_metadata = write_file_metadata
|
_write_metadata = write_file_metadata
|
||||||
|
|
||||||
if copyfile:
|
if copyfile:
|
||||||
def copy(self, target, *, follow_symlinks=True, preserve_metadata=False):
|
def _copy_file(self, target):
|
||||||
"""
|
"""
|
||||||
Copy the contents of this file to the given target. If this file is a
|
Copy the contents of this file to the given target.
|
||||||
symlink and follow_symlinks is false, a symlink will be created at the
|
|
||||||
target.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
target = os.fspath(target)
|
target = os.fspath(target)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
if not isinstance(target, PathBase):
|
if not isinstance(target, PathBase):
|
||||||
raise
|
raise
|
||||||
|
PathBase._copy_file(self, target)
|
||||||
else:
|
else:
|
||||||
try:
|
copyfile(os.fspath(self), target)
|
||||||
copyfile(os.fspath(self), target, follow_symlinks)
|
|
||||||
return
|
|
||||||
except UnsupportedOperation:
|
|
||||||
pass # Fall through to generic code.
|
|
||||||
PathBase.copy(self, target, follow_symlinks=follow_symlinks,
|
|
||||||
preserve_metadata=preserve_metadata)
|
|
||||||
|
|
||||||
def chmod(self, mode, *, follow_symlinks=True):
|
def chmod(self, mode, *, follow_symlinks=True):
|
||||||
"""
|
"""
|
||||||
|
@ -894,6 +887,14 @@ class Path(PathBase, PurePath):
|
||||||
"""
|
"""
|
||||||
os.symlink(target, self, target_is_directory)
|
os.symlink(target, self, target_is_directory)
|
||||||
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
def _symlink_to_target_of(self, link):
|
||||||
|
"""
|
||||||
|
Make this path a symlink with the same target as the given link.
|
||||||
|
This is used by copy().
|
||||||
|
"""
|
||||||
|
self.symlink_to(link.readlink(), link.is_dir())
|
||||||
|
|
||||||
if hasattr(os, "link"):
|
if hasattr(os, "link"):
|
||||||
def hardlink_to(self, target):
|
def hardlink_to(self, target):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -20,15 +20,6 @@ except ImportError:
|
||||||
_winapi = None
|
_winapi = None
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["UnsupportedOperation"]
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedOperation(NotImplementedError):
|
|
||||||
"""An exception that is raised when an unsupported operation is attempted.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_copy_blocksize(infd):
|
def get_copy_blocksize(infd):
|
||||||
"""Determine blocksize for fastcopying on Linux.
|
"""Determine blocksize for fastcopying on Linux.
|
||||||
Hopefully the whole file will be copied in a single call.
|
Hopefully the whole file will be copied in a single call.
|
||||||
|
@ -101,44 +92,12 @@ else:
|
||||||
copyfd = None
|
copyfd = None
|
||||||
|
|
||||||
|
|
||||||
if _winapi and hasattr(_winapi, 'CopyFile2') and hasattr(os.stat_result, 'st_file_attributes'):
|
if _winapi and hasattr(_winapi, 'CopyFile2'):
|
||||||
def _is_dirlink(path):
|
def copyfile(source, target):
|
||||||
try:
|
|
||||||
st = os.lstat(path)
|
|
||||||
except (OSError, ValueError):
|
|
||||||
return False
|
|
||||||
return (st.st_file_attributes & stat.FILE_ATTRIBUTE_DIRECTORY and
|
|
||||||
st.st_reparse_tag == stat.IO_REPARSE_TAG_SYMLINK)
|
|
||||||
|
|
||||||
def copyfile(source, target, follow_symlinks):
|
|
||||||
"""
|
"""
|
||||||
Copy from one file to another using CopyFile2 (Windows only).
|
Copy from one file to another using CopyFile2 (Windows only).
|
||||||
"""
|
"""
|
||||||
if follow_symlinks:
|
_winapi.CopyFile2(source, target, 0)
|
||||||
_winapi.CopyFile2(source, target, 0)
|
|
||||||
else:
|
|
||||||
# Use COPY_FILE_COPY_SYMLINK to copy a file symlink.
|
|
||||||
flags = _winapi.COPY_FILE_COPY_SYMLINK
|
|
||||||
try:
|
|
||||||
_winapi.CopyFile2(source, target, flags)
|
|
||||||
return
|
|
||||||
except OSError as err:
|
|
||||||
# Check for ERROR_ACCESS_DENIED
|
|
||||||
if err.winerror == 5 and _is_dirlink(source):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Add COPY_FILE_DIRECTORY to copy a directory symlink.
|
|
||||||
flags |= _winapi.COPY_FILE_DIRECTORY
|
|
||||||
try:
|
|
||||||
_winapi.CopyFile2(source, target, flags)
|
|
||||||
except OSError as err:
|
|
||||||
# Check for ERROR_INVALID_PARAMETER
|
|
||||||
if err.winerror == 87:
|
|
||||||
raise UnsupportedOperation(err) from None
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
copyfile = None
|
copyfile = None
|
||||||
|
|
||||||
|
|
|
@ -709,19 +709,19 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
|
|
||||||
@unittest.skipIf(sys.platform == "win32" or sys.platform == "wasi", "directories are always readable on Windows and WASI")
|
@unittest.skipIf(sys.platform == "win32" or sys.platform == "wasi", "directories are always readable on Windows and WASI")
|
||||||
@unittest.skipIf(root_in_posix, "test fails with root privilege")
|
@unittest.skipIf(root_in_posix, "test fails with root privilege")
|
||||||
def test_copytree_no_read_permission(self):
|
def test_copy_dir_no_read_permission(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirE'
|
source = base / 'dirE'
|
||||||
target = base / 'copyE'
|
target = base / 'copyE'
|
||||||
self.assertRaises(PermissionError, source.copytree, target)
|
self.assertRaises(PermissionError, source.copy, target)
|
||||||
self.assertFalse(target.exists())
|
self.assertFalse(target.exists())
|
||||||
errors = []
|
errors = []
|
||||||
source.copytree(target, on_error=errors.append)
|
source.copy(target, on_error=errors.append)
|
||||||
self.assertEqual(len(errors), 1)
|
self.assertEqual(len(errors), 1)
|
||||||
self.assertIsInstance(errors[0], PermissionError)
|
self.assertIsInstance(errors[0], PermissionError)
|
||||||
self.assertFalse(target.exists())
|
self.assertFalse(target.exists())
|
||||||
|
|
||||||
def test_copytree_preserve_metadata(self):
|
def test_copy_dir_preserve_metadata(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirC'
|
source = base / 'dirC'
|
||||||
if hasattr(os, 'chmod'):
|
if hasattr(os, 'chmod'):
|
||||||
|
@ -729,7 +729,7 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
if hasattr(os, 'chflags') and hasattr(stat, 'UF_NODUMP'):
|
if hasattr(os, 'chflags') and hasattr(stat, 'UF_NODUMP'):
|
||||||
os.chflags(source / 'fileC', stat.UF_NODUMP)
|
os.chflags(source / 'fileC', stat.UF_NODUMP)
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
source.copytree(target, preserve_metadata=True)
|
source.copy(target, preserve_metadata=True)
|
||||||
|
|
||||||
for subpath in ['.', 'fileC', 'dirD', 'dirD/fileD']:
|
for subpath in ['.', 'fileC', 'dirD', 'dirD/fileD']:
|
||||||
source_st = source.joinpath(subpath).stat()
|
source_st = source.joinpath(subpath).stat()
|
||||||
|
@ -741,13 +741,13 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
self.assertEqual(source_st.st_flags, target_st.st_flags)
|
self.assertEqual(source_st.st_flags, target_st.st_flags)
|
||||||
|
|
||||||
@os_helper.skip_unless_xattr
|
@os_helper.skip_unless_xattr
|
||||||
def test_copytree_preserve_metadata_xattrs(self):
|
def test_copy_dir_preserve_metadata_xattrs(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirC'
|
source = base / 'dirC'
|
||||||
source_file = source.joinpath('dirD', 'fileD')
|
source_file = source.joinpath('dirD', 'fileD')
|
||||||
os.setxattr(source_file, b'user.foo', b'42')
|
os.setxattr(source_file, b'user.foo', b'42')
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
source.copytree(target, preserve_metadata=True)
|
source.copy(target, preserve_metadata=True)
|
||||||
target_file = target.joinpath('dirD', 'fileD')
|
target_file = target.joinpath('dirD', 'fileD')
|
||||||
self.assertEqual(os.getxattr(target_file, b'user.foo'), b'42')
|
self.assertEqual(os.getxattr(target_file, b'user.foo'), b'42')
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,7 @@ import errno
|
||||||
import stat
|
import stat
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from pathlib._os import UnsupportedOperation
|
from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase
|
||||||
from pathlib._abc import ParserBase, PurePathBase, PathBase
|
|
||||||
import posixpath
|
import posixpath
|
||||||
|
|
||||||
from test.support import is_wasi
|
from test.support import is_wasi
|
||||||
|
@ -1732,23 +1731,18 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'fileA'
|
source = base / 'fileA'
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
source.copy(target)
|
result = source.copy(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertEqual(source.read_text(), target.read_text())
|
self.assertEqual(source.read_text(), target.read_text())
|
||||||
|
|
||||||
def test_copy_directory(self):
|
|
||||||
base = self.cls(self.base)
|
|
||||||
source = base / 'dirA'
|
|
||||||
target = base / 'copyA'
|
|
||||||
with self.assertRaises(OSError):
|
|
||||||
source.copy(target)
|
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_copy_symlink_follow_symlinks_true(self):
|
def test_copy_symlink_follow_symlinks_true(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'linkA'
|
source = base / 'linkA'
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
source.copy(target)
|
result = source.copy(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertFalse(target.is_symlink())
|
self.assertFalse(target.is_symlink())
|
||||||
self.assertEqual(source.read_text(), target.read_text())
|
self.assertEqual(source.read_text(), target.read_text())
|
||||||
|
@ -1758,7 +1752,8 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'linkA'
|
source = base / 'linkA'
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
source.copy(target, follow_symlinks=False)
|
result = source.copy(target, follow_symlinks=False)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertTrue(target.is_symlink())
|
self.assertTrue(target.is_symlink())
|
||||||
self.assertEqual(source.readlink(), target.readlink())
|
self.assertEqual(source.readlink(), target.readlink())
|
||||||
|
@ -1768,20 +1763,22 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'linkB'
|
source = base / 'linkB'
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
source.copy(target, follow_symlinks=False)
|
result = source.copy(target, follow_symlinks=False)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertTrue(target.is_symlink())
|
self.assertTrue(target.is_symlink())
|
||||||
self.assertEqual(source.readlink(), target.readlink())
|
self.assertEqual(source.readlink(), target.readlink())
|
||||||
|
|
||||||
def test_copy_to_existing_file(self):
|
def test_copy_file_to_existing_file(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'fileA'
|
source = base / 'fileA'
|
||||||
target = base / 'dirB' / 'fileB'
|
target = base / 'dirB' / 'fileB'
|
||||||
source.copy(target)
|
result = source.copy(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertEqual(source.read_text(), target.read_text())
|
self.assertEqual(source.read_text(), target.read_text())
|
||||||
|
|
||||||
def test_copy_to_existing_directory(self):
|
def test_copy_file_to_existing_directory(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'fileA'
|
source = base / 'fileA'
|
||||||
target = base / 'dirA'
|
target = base / 'dirA'
|
||||||
|
@ -1789,12 +1786,13 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
source.copy(target)
|
source.copy(target)
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_copy_to_existing_symlink(self):
|
def test_copy_file_to_existing_symlink(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirB' / 'fileB'
|
source = base / 'dirB' / 'fileB'
|
||||||
target = base / 'linkA'
|
target = base / 'linkA'
|
||||||
real_target = base / 'fileA'
|
real_target = base / 'fileA'
|
||||||
source.copy(target)
|
result = source.copy(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertTrue(target.is_symlink())
|
self.assertTrue(target.is_symlink())
|
||||||
self.assertTrue(real_target.exists())
|
self.assertTrue(real_target.exists())
|
||||||
|
@ -1802,32 +1800,35 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertEqual(source.read_text(), real_target.read_text())
|
self.assertEqual(source.read_text(), real_target.read_text())
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_copy_to_existing_symlink_follow_symlinks_false(self):
|
def test_copy_file_to_existing_symlink_follow_symlinks_false(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirB' / 'fileB'
|
source = base / 'dirB' / 'fileB'
|
||||||
target = base / 'linkA'
|
target = base / 'linkA'
|
||||||
real_target = base / 'fileA'
|
real_target = base / 'fileA'
|
||||||
source.copy(target, follow_symlinks=False)
|
result = source.copy(target, follow_symlinks=False)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertTrue(target.is_symlink())
|
self.assertTrue(target.is_symlink())
|
||||||
self.assertTrue(real_target.exists())
|
self.assertTrue(real_target.exists())
|
||||||
self.assertFalse(real_target.is_symlink())
|
self.assertFalse(real_target.is_symlink())
|
||||||
self.assertEqual(source.read_text(), real_target.read_text())
|
self.assertEqual(source.read_text(), real_target.read_text())
|
||||||
|
|
||||||
def test_copy_empty(self):
|
def test_copy_file_empty(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'empty'
|
source = base / 'empty'
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
source.write_bytes(b'')
|
source.write_bytes(b'')
|
||||||
source.copy(target)
|
result = source.copy(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.exists())
|
self.assertTrue(target.exists())
|
||||||
self.assertEqual(target.read_bytes(), b'')
|
self.assertEqual(target.read_bytes(), b'')
|
||||||
|
|
||||||
def test_copytree_simple(self):
|
def test_copy_dir_simple(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirC'
|
source = base / 'dirC'
|
||||||
target = base / 'copyC'
|
target = base / 'copyC'
|
||||||
source.copytree(target)
|
result = source.copy(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.is_dir())
|
self.assertTrue(target.is_dir())
|
||||||
self.assertTrue(target.joinpath('dirD').is_dir())
|
self.assertTrue(target.joinpath('dirD').is_dir())
|
||||||
self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
|
self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
|
||||||
|
@ -1837,7 +1838,7 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertTrue(target.joinpath('fileC').read_text(),
|
self.assertTrue(target.joinpath('fileC').read_text(),
|
||||||
"this is file C\n")
|
"this is file C\n")
|
||||||
|
|
||||||
def test_copytree_complex(self, follow_symlinks=True):
|
def test_copy_dir_complex(self, follow_symlinks=True):
|
||||||
def ordered_walk(path):
|
def ordered_walk(path):
|
||||||
for dirpath, dirnames, filenames in path.walk(follow_symlinks=follow_symlinks):
|
for dirpath, dirnames, filenames in path.walk(follow_symlinks=follow_symlinks):
|
||||||
dirnames.sort()
|
dirnames.sort()
|
||||||
|
@ -1853,7 +1854,8 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
|
|
||||||
# Perform the copy
|
# Perform the copy
|
||||||
target = base / 'copyC'
|
target = base / 'copyC'
|
||||||
source.copytree(target, follow_symlinks=follow_symlinks)
|
result = source.copy(target, follow_symlinks=follow_symlinks)
|
||||||
|
self.assertEqual(result, target)
|
||||||
|
|
||||||
# Compare the source and target trees
|
# Compare the source and target trees
|
||||||
source_walk = ordered_walk(source)
|
source_walk = ordered_walk(source)
|
||||||
|
@ -1879,24 +1881,25 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertEqual(source_file.read_bytes(), target_file.read_bytes())
|
self.assertEqual(source_file.read_bytes(), target_file.read_bytes())
|
||||||
self.assertEqual(source_file.readlink(), target_file.readlink())
|
self.assertEqual(source_file.readlink(), target_file.readlink())
|
||||||
|
|
||||||
def test_copytree_complex_follow_symlinks_false(self):
|
def test_copy_dir_complex_follow_symlinks_false(self):
|
||||||
self.test_copytree_complex(follow_symlinks=False)
|
self.test_copy_dir_complex(follow_symlinks=False)
|
||||||
|
|
||||||
def test_copytree_to_existing_directory(self):
|
def test_copy_dir_to_existing_directory(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirC'
|
source = base / 'dirC'
|
||||||
target = base / 'copyC'
|
target = base / 'copyC'
|
||||||
target.mkdir()
|
target.mkdir()
|
||||||
target.joinpath('dirD').mkdir()
|
target.joinpath('dirD').mkdir()
|
||||||
self.assertRaises(FileExistsError, source.copytree, target)
|
self.assertRaises(FileExistsError, source.copy, target)
|
||||||
|
|
||||||
def test_copytree_to_existing_directory_dirs_exist_ok(self):
|
def test_copy_dir_to_existing_directory_dirs_exist_ok(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirC'
|
source = base / 'dirC'
|
||||||
target = base / 'copyC'
|
target = base / 'copyC'
|
||||||
target.mkdir()
|
target.mkdir()
|
||||||
target.joinpath('dirD').mkdir()
|
target.joinpath('dirD').mkdir()
|
||||||
source.copytree(target, dirs_exist_ok=True)
|
result = source.copy(target, dirs_exist_ok=True)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertTrue(target.is_dir())
|
self.assertTrue(target.is_dir())
|
||||||
self.assertTrue(target.joinpath('dirD').is_dir())
|
self.assertTrue(target.joinpath('dirD').is_dir())
|
||||||
self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
|
self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
|
||||||
|
@ -1906,22 +1909,17 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertTrue(target.joinpath('fileC').read_text(),
|
self.assertTrue(target.joinpath('fileC').read_text(),
|
||||||
"this is file C\n")
|
"this is file C\n")
|
||||||
|
|
||||||
def test_copytree_file(self):
|
def test_copy_missing_on_error(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'fileA'
|
source = base / 'foo'
|
||||||
target = base / 'copyA'
|
|
||||||
self.assertRaises(NotADirectoryError, source.copytree, target)
|
|
||||||
|
|
||||||
def test_copytree_file_on_error(self):
|
|
||||||
base = self.cls(self.base)
|
|
||||||
source = base / 'fileA'
|
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
errors = []
|
errors = []
|
||||||
source.copytree(target, on_error=errors.append)
|
result = source.copy(target, on_error=errors.append)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertEqual(len(errors), 1)
|
self.assertEqual(len(errors), 1)
|
||||||
self.assertIsInstance(errors[0], NotADirectoryError)
|
self.assertIsInstance(errors[0], FileNotFoundError)
|
||||||
|
|
||||||
def test_copytree_ignore_false(self):
|
def test_copy_dir_ignore_false(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirC'
|
source = base / 'dirC'
|
||||||
target = base / 'copyC'
|
target = base / 'copyC'
|
||||||
|
@ -1929,7 +1927,8 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
def ignore_false(path):
|
def ignore_false(path):
|
||||||
ignores.append(path)
|
ignores.append(path)
|
||||||
return False
|
return False
|
||||||
source.copytree(target, ignore=ignore_false)
|
result = source.copy(target, ignore=ignore_false)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertEqual(set(ignores), {
|
self.assertEqual(set(ignores), {
|
||||||
source / 'dirD',
|
source / 'dirD',
|
||||||
source / 'dirD' / 'fileD',
|
source / 'dirD' / 'fileD',
|
||||||
|
@ -1945,7 +1944,7 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertTrue(target.joinpath('fileC').read_text(),
|
self.assertTrue(target.joinpath('fileC').read_text(),
|
||||||
"this is file C\n")
|
"this is file C\n")
|
||||||
|
|
||||||
def test_copytree_ignore_true(self):
|
def test_copy_dir_ignore_true(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'dirC'
|
source = base / 'dirC'
|
||||||
target = base / 'copyC'
|
target = base / 'copyC'
|
||||||
|
@ -1953,7 +1952,8 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
def ignore_true(path):
|
def ignore_true(path):
|
||||||
ignores.append(path)
|
ignores.append(path)
|
||||||
return True
|
return True
|
||||||
source.copytree(target, ignore=ignore_true)
|
result = source.copy(target, ignore=ignore_true)
|
||||||
|
self.assertEqual(result, target)
|
||||||
self.assertEqual(set(ignores), {
|
self.assertEqual(set(ignores), {
|
||||||
source / 'dirD',
|
source / 'dirD',
|
||||||
source / 'fileC',
|
source / 'fileC',
|
||||||
|
@ -1965,7 +1965,7 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertFalse(target.joinpath('novel.txt').exists())
|
self.assertFalse(target.joinpath('novel.txt').exists())
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_copytree_dangling_symlink(self):
|
def test_copy_dangling_symlink(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'source'
|
source = base / 'source'
|
||||||
target = base / 'target'
|
target = base / 'target'
|
||||||
|
@ -1973,10 +1973,11 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
source.mkdir()
|
source.mkdir()
|
||||||
source.joinpath('link').symlink_to('nonexistent')
|
source.joinpath('link').symlink_to('nonexistent')
|
||||||
|
|
||||||
self.assertRaises(FileNotFoundError, source.copytree, target)
|
self.assertRaises(FileNotFoundError, source.copy, target)
|
||||||
|
|
||||||
target2 = base / 'target2'
|
target2 = base / 'target2'
|
||||||
source.copytree(target2, follow_symlinks=False)
|
result = source.copy(target2, follow_symlinks=False)
|
||||||
|
self.assertEqual(result, target2)
|
||||||
self.assertTrue(target2.joinpath('link').is_symlink())
|
self.assertTrue(target2.joinpath('link').is_symlink())
|
||||||
self.assertEqual(target2.joinpath('link').readlink(), self.cls('nonexistent'))
|
self.assertEqual(target2.joinpath('link').readlink(), self.cls('nonexistent'))
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
Add :meth:`pathlib.Path.copy`, which copies the content of one file to another,
|
Add :meth:`pathlib.Path.copy`, which copies a file or directory to another.
|
||||||
like :func:`shutil.copyfile`.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Add :meth:`pathlib.Path.copytree`, which recursively copies a directory.
|
|
Loading…
Add table
Add a link
Reference in a new issue