mirror of https://github.com/python/cpython
GH-73991: Add `pathlib.Path.copy_into()` and `move_into()` (#123314)
These two methods accept an *existing* directory path, onto which we join the source path's base name to form the final target path. A possible alternative implementation is to check for directories in `copy()` and `move()` and adjust the target path, which is done in several `shutil` functions. This behaviour is helpful in a shell context, but less so in a stored program that explicitly specifies destinations. For example, a user that calls `Path('foo.py').copy('bar.py')` might not imagine that `bar.py/foo.py` would be created, but under the alternative implementation this will happen if `bar.py` is an existing directory.
This commit is contained in:
parent
dbc1752d41
commit
c68a93c582
|
@ -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
|
||||
|
@ -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,
|
||||
|
|
|
@ -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`.)
|
||||
|
|
|
@ -904,6 +904,24 @@ class PathBase(PurePathBase):
|
|||
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.
|
||||
|
@ -947,6 +965,19 @@ class PathBase(PurePathBase):
|
|||
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().
|
||||
|
|
|
@ -861,6 +861,14 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
|||
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')
|
||||
|
||||
|
|
|
@ -2072,6 +2072,20 @@ class DummyPathTest(DummyPurePathTest):
|
|||
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'
|
||||
|
@ -2191,6 +2205,22 @@ class DummyPathTest(DummyPurePathTest):
|
|||
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)
|
||||
|
|
|
@ -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…
Reference in New Issue