Skip to content

Commit 17978ed

Browse files
committed
gh-143052: add exist_ok in copy_info func, allow merge same dir
1 parent 5989095 commit 17978ed

File tree

5 files changed

+66
-9
lines changed

5 files changed

+66
-9
lines changed

Doc/library/pathlib.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,12 +1590,16 @@ Copying, moving and deleting
15901590
.. versionadded:: 3.14
15911591

15921592

1593-
.. method:: Path.copy_into(target_dir, *, follow_symlinks=True, \
1593+
.. method:: Path.copy_into(target_dir, *, exist_ok=True, follow_symlinks=True, \
15941594
preserve_metadata=False)
15951595

15961596
Copy this file or directory tree into the given *target_dir*, which should
1597-
be an existing directory. Other arguments are handled identically to
1598-
:meth:`Path.copy`. Returns a new :class:`!Path` instance pointing to the
1597+
be an existing directory. If *exist_ok* is true (the default), copying a file
1598+
overwrites an existing file with the same name and copying a directory merges
1599+
its contents into an existing directory with the same name. If *exist_ok* is
1600+
false, :exc:`FileExistsError` is raised if *target_dir* already contains an
1601+
entry with the same name as the source. Other arguments are handled identically
1602+
to :meth:`Path.copy`. Returns a new :class:`!Path` instance pointing to the
15991603
copy.
16001604

16011605
.. versionadded:: 3.14

Lib/pathlib/__init__.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,17 +1301,33 @@ def copy(self, target, **kwargs):
13011301
target._copy_from(self, **kwargs)
13021302
return target.joinpath() # Empty join to ensure fresh metadata.
13031303

1304-
def copy_into(self, target_dir, **kwargs):
1304+
def copy_into(self, target_dir, exist_ok=True, **kwargs):
13051305
"""
13061306
Copy this file or directory tree into the given existing directory.
13071307
"""
13081308
name = self.name
13091309
if not name:
13101310
raise ValueError(f"{self!r} has an empty name")
1311-
elif hasattr(target_dir, 'with_segments'):
1312-
target = target_dir / name
1313-
else:
1314-
target = self.with_segments(target_dir, name)
1311+
1312+
target = (target_dir / name) if hasattr(target_dir, "with_segments") \
1313+
else self.with_segments(target_dir, name)
1314+
1315+
if self.info.is_file():
1316+
if not exist_ok and target.info.is_file():
1317+
raise FileExistsError(EEXIST, "File exists", str(target))
1318+
return self.copy(target, **kwargs)
1319+
1320+
if self.info.is_dir():
1321+
if target.info.exists() and target.info.is_dir():
1322+
if not exist_ok:
1323+
raise FileExistsError(EEXIST, "File exists", str(target))
1324+
1325+
for child in self.iterdir():
1326+
child.copy_into(target, exist_ok=exist_ok, **kwargs)
1327+
return target.joinpath()
1328+
1329+
return self.copy(target, **kwargs)
1330+
13151331
return self.copy(target, **kwargs)
13161332

13171333
def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False):

Lib/pathlib/types.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,13 +386,22 @@ def copy(self, target, **kwargs):
386386
target._copy_from(self, **kwargs)
387387
return target.joinpath() # Empty join to ensure fresh metadata.
388388

389-
def copy_into(self, target_dir, **kwargs):
389+
def copy_into(self, target_dir, exist_ok=True, **kwargs):
390390
"""
391391
Copy this file or directory tree into the given existing directory.
392392
"""
393393
name = self.name
394394
if not name:
395395
raise ValueError(f"{self!r} has an empty name")
396+
397+
target = target_dir / name
398+
if hasattr(target, 'info') and self.info.is_dir() and target.info.exists() and target.info.is_dir():
399+
if not exist_ok:
400+
raise FileExistsError("File exists", str(target))
401+
for child in self.iterdir():
402+
child.copy_into(target, exist_ok=exist_ok, **kwargs)
403+
return target.joinpath()
404+
396405
return self.copy(target_dir / name, **kwargs)
397406

398407

Lib/test/test_pathlib/test_copy.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,33 @@ def test_copy_into_empty_name(self):
146146
self.target_ground.create_dir(target_dir)
147147
self.assertRaises(ValueError, source.copy_into, target_dir)
148148

149+
def test_copy_dir_into_existing_dir_merges_when_exist_ok_true(self):
150+
if isinstance(self.target_root, WritableZipPath):
151+
self.skipTest('needs local target')
152+
source = self.source_root / 'dirC'
153+
target_dir = self.target_root
154+
preexisting = target_dir / 'dirC'
155+
self.target_ground.create_dir(preexisting)
156+
self.target_ground.create_file(preexisting / 'pre.txt', b'pre\n')
157+
158+
result = source.copy_into(target_dir)
159+
self.assertEqual(result, preexisting)
160+
161+
self.assertTrue(self.target_ground.isfile(preexisting / 'pre.txt'))
162+
163+
self.assertTrue(self.target_ground.isfile(preexisting / 'fileC'))
164+
self.assertTrue(self.target_ground.isdir(preexisting / 'dirD'))
165+
self.assertTrue(self.target_ground.isfile(preexisting / 'dirD' / 'fileD'))
166+
167+
def test_copy_dir_into_existing_dir_exist_ok_false(self):
168+
if isinstance(self.target_root, WritableZipPath):
169+
self.skipTest('needs local target')
170+
source = self.source_root / 'dirC'
171+
target_dir = self.target_root
172+
self.target_ground.create_dir(target_dir / 'dirC')
173+
with self.assertRaises(FileExistsError):
174+
source.copy_into(target_dir, exist_ok=False)
175+
149176

150177
class ZipToZipPathCopyTest(CopyTestBase, unittest.TestCase):
151178
source_ground = ZipPathGround(ReadableZipPath)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
copy_into add exist_ok allow overwrite same directory.

0 commit comments

Comments
 (0)