Skip to content

GH-73991: Add pathlib.Path.copy_into() and move_into() #123314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1575,6 +1575,18 @@ Copying, moving and deleting
.. versionadded:: 3.14


.. method:: Path.copy_into(target_dir, *, follow_symlinks=True, \
dirs_exist_ok=False, preserve_metadata=False, \
ignore=None, on_error=None)

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
copy.

.. versionadded:: 3.14


.. method:: Path.rename(target)

Rename this file or directory to the given *target*, and return a new
Expand Down Expand Up @@ -1633,6 +1645,15 @@ Copying, moving and deleting
.. versionadded:: 3.14


.. method:: Path.move_into(target_dir)

Move this file or directory tree into the given *target_dir*, which should
be an existing directory. Returns a new :class:`!Path` instance pointing to
the moved path.

.. versionadded:: 3.14


.. method:: Path.unlink(missing_ok=False)

Remove this file or symbolic link. If the path points to a directory,
Expand Down
8 changes: 4 additions & 4 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ pathlib
* Add methods to :class:`pathlib.Path` to recursively copy, move, or remove
files and directories:

* :meth:`~pathlib.Path.copy` copies a file or directory tree to a given
destination.
* :meth:`~pathlib.Path.move` moves a file or directory tree to a given
destination.
* :meth:`~pathlib.Path.copy` copies a file or directory tree to a destination.
* :meth:`~pathlib.Path.copy_into` copies *into* a destination directory.
* :meth:`~pathlib.Path.move` moves a file or directory tree to a destination.
* :meth:`~pathlib.Path.move_into` moves *into* a destination directory.
* :meth:`~pathlib.Path.delete` removes a file or directory tree.

(Contributed by Barney Gale in :gh:`73991`.)
Expand Down
31 changes: 31 additions & 0 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,24 @@ def copy(self, target, *, follow_symlinks=True, dirs_exist_ok=False,
on_error(err)
return target

def copy_into(self, target_dir, *, follow_symlinks=True,
dirs_exist_ok=False, preserve_metadata=False, ignore=None,
on_error=None):
"""
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 isinstance(target_dir, PathBase):
target = target_dir / name
else:
target = self.with_segments(target_dir, name)
return self.copy(target, follow_symlinks=follow_symlinks,
dirs_exist_ok=dirs_exist_ok,
preserve_metadata=preserve_metadata, ignore=ignore,
on_error=on_error)

def rename(self, target):
"""
Rename this path to the target path.
Expand Down Expand Up @@ -947,6 +965,19 @@ def move(self, target):
self.delete()
return target

def move_into(self, target_dir):
"""
Move 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 isinstance(target_dir, PathBase):
target = target_dir / name
else:
target = self.with_segments(target_dir, name)
return self.move(target)

def chmod(self, mode, *, follow_symlinks=True):
"""
Change the permissions of the path, like os.chmod().
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,14 @@ def test_move_dir_symlink_to_itself_other_fs(self):
def test_move_dangling_symlink_other_fs(self):
self.test_move_dangling_symlink()

@patch_replace
def test_move_into_other_os(self):
self.test_move_into()

@patch_replace
def test_move_into_empty_name_other_os(self):
self.test_move_into_empty_name()

def test_resolve_nonexist_relative_issue38671(self):
p = self.cls('non', 'exist')

Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2072,6 +2072,20 @@ def test_copy_dangling_symlink(self):
self.assertTrue(target2.joinpath('link').is_symlink())
self.assertEqual(target2.joinpath('link').readlink(), self.cls('nonexistent'))

def test_copy_into(self):
base = self.cls(self.base)
source = base / 'fileA'
target_dir = base / 'dirA'
result = source.copy_into(target_dir)
self.assertEqual(result, target_dir / 'fileA')
self.assertTrue(result.exists())
self.assertEqual(source.read_text(), result.read_text())

def test_copy_into_empty_name(self):
source = self.cls('')
target_dir = self.base
self.assertRaises(ValueError, source.copy_into, target_dir)

def test_move_file(self):
base = self.cls(self.base)
source = base / 'fileA'
Expand Down Expand Up @@ -2191,6 +2205,22 @@ def test_move_dangling_symlink(self):
self.assertTrue(target.is_symlink())
self.assertEqual(source_readlink, target.readlink())

def test_move_into(self):
base = self.cls(self.base)
source = base / 'fileA'
source_text = source.read_text()
target_dir = base / 'dirA'
result = source.move_into(target_dir)
self.assertEqual(result, target_dir / 'fileA')
self.assertFalse(source.exists())
self.assertTrue(result.exists())
self.assertEqual(source_text, result.read_text())

def test_move_into_empty_name(self):
source = self.cls('')
target_dir = self.base
self.assertRaises(ValueError, source.move_into, target_dir)

def test_iterdir(self):
P = self.cls
p = P(self.base)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :meth:`pathlib.Path.copy_into` and :meth:`~pathlib.Path.move_into`,
which copy and move files and directories into *existing* directories.
Loading