This commit is contained in:
wangxiaolei 2025-12-23 14:12:23 +05:30 committed by GitHub
commit db0266790a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 72 additions and 10 deletions

View file

@ -1590,16 +1590,23 @@ Copying, moving and deleting
.. versionadded:: 3.14
.. method:: Path.copy_into(target_dir, *, follow_symlinks=True, \
.. method:: Path.copy_into(target_dir, *, exist_ok=True, follow_symlinks=True, \
preserve_metadata=False)
Copy this file or directory tree into the given *target_dir*, which should
be an existing directory. Other arguments are handled identically to
:meth:`Path.copy`. Returns a new :class:`!Path` instance pointing to the
be an existing directory. If *exist_ok* is true (the default), copying a file
overwrites an existing file with the same name and copying a directory merges
its contents into an existing directory with the same name. If *exist_ok* is
false, :exc:`FileExistsError` is raised if *target_dir* already contains an
entry with the same name as the source. Other arguments are handled identically
to :meth:`Path.copy`. Returns a new :class:`!Path` instance pointing to the
copy.
.. versionadded:: 3.14
.. versionchanged:: 3.15
The *exist_ok* parameter is added to allow copying duplicate directories.
.. method:: Path.rename(target)

View file

@ -1301,18 +1301,36 @@ class Path(PurePath):
target._copy_from(self, **kwargs)
return target.joinpath() # Empty join to ensure fresh metadata.
def copy_into(self, target_dir, **kwargs):
def copy_into(self, target_dir, exist_ok=True, **kwargs):
"""
Copy this file or directory tree into the given existing directory.
"""
name = self.name
if not name:
raise ValueError(f"{self!r} has an empty name")
elif hasattr(target_dir, 'with_segments'):
target = target_dir / name
else:
target = self.with_segments(target_dir, name)
return self.copy(target, **kwargs)
parent = target_dir if hasattr(target_dir, "with_segments") else self.with_segments(target_dir)
is_dir = getattr(parent, "is_dir", None)
if callable(is_dir) and not is_dir():
raise ValueError(f"{parent!r} is not a directory")
dest = parent / name
if not exist_ok and dest.exists():
raise FileExistsError(EEXIST, "File exists", str(dest))
if self.is_dir():
if dest.exists():
if not dest.is_dir():
raise ValueError(f"{dest!r} is not a directory")
else:
dest.mkdir()
for child in self.iterdir():
child.copy_into(dest, exist_ok=exist_ok, **kwargs)
return dest.joinpath()
return self.copy(dest, **kwargs)
def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False):
"""

View file

@ -386,13 +386,22 @@ class _ReadablePath(_JoinablePath):
target._copy_from(self, **kwargs)
return target.joinpath() # Empty join to ensure fresh metadata.
def copy_into(self, target_dir, **kwargs):
def copy_into(self, target_dir, exist_ok=True, **kwargs):
"""
Copy this file or directory tree into the given existing directory.
"""
name = self.name
if not name:
raise ValueError(f"{self!r} has an empty name")
target = target_dir / name
if hasattr(target, 'info') and self.info.is_dir() and target.info.exists() and target.info.is_dir():
if not exist_ok:
raise FileExistsError("File exists", str(target))
for child in self.iterdir():
child.copy_into(target, exist_ok=exist_ok, **kwargs)
return target.joinpath()
return self.copy(target_dir / name, **kwargs)

View file

@ -146,6 +146,33 @@ class CopyTestBase:
self.target_ground.create_dir(target_dir)
self.assertRaises(ValueError, source.copy_into, target_dir)
def test_copy_dir_into_existing_dir_merges_when_exist_ok_true(self):
if isinstance(self.target_root, WritableZipPath):
self.skipTest('needs local target')
source = self.source_root / 'dirC'
target_dir = self.target_root
preexisting = target_dir / 'dirC'
self.target_ground.create_dir(preexisting)
self.target_ground.create_file(preexisting / 'pre.txt', b'pre\n')
result = source.copy_into(target_dir)
self.assertEqual(result, preexisting)
self.assertTrue(self.target_ground.isfile(preexisting / 'pre.txt'))
self.assertTrue(self.target_ground.isfile(preexisting / 'fileC'))
self.assertTrue(self.target_ground.isdir(preexisting / 'dirD'))
self.assertTrue(self.target_ground.isfile(preexisting / 'dirD' / 'fileD'))
def test_copy_dir_into_existing_dir_exist_ok_false(self):
if isinstance(self.target_root, WritableZipPath):
self.skipTest('needs local target')
source = self.source_root / 'dirC'
target_dir = self.target_root
self.target_ground.create_dir(target_dir / 'dirC')
with self.assertRaises(FileExistsError):
source.copy_into(target_dir, exist_ok=False)
class ZipToZipPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = ZipPathGround(ReadableZipPath)

View file

@ -0,0 +1 @@
Add ``exist_ok`` parameter to :meth:`!pathlib.Path.copy_into`.