mirror of
https://github.com/python/cpython.git
synced 2025-07-24 03:35:53 +00:00
GH-73991: Add pathlib.Path.move()
(#122073)
Add a `Path.move()` method that moves a file or directory tree, and returns a new `Path` instance pointing to the target. This method is similar to `shutil.move()`, except that it doesn't accept a *copy_function* argument, and it doesn't check whether the destination is an existing directory.
This commit is contained in:
parent
aa905925e1
commit
625d0705b9
6 changed files with 225 additions and 4 deletions
|
@ -1536,8 +1536,8 @@ Creating files and directories
|
||||||
available. In previous versions, :exc:`NotImplementedError` was raised.
|
available. In previous versions, :exc:`NotImplementedError` was raised.
|
||||||
|
|
||||||
|
|
||||||
Copying, renaming and deleting
|
Copying, moving and deleting
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. method:: Path.copy(target, *, follow_symlinks=True, dirs_exist_ok=False, \
|
.. method:: Path.copy(target, *, follow_symlinks=True, dirs_exist_ok=False, \
|
||||||
preserve_metadata=False, ignore=None, on_error=None)
|
preserve_metadata=False, ignore=None, on_error=None)
|
||||||
|
@ -1616,6 +1616,23 @@ Copying, renaming and deleting
|
||||||
Added return value, return the new :class:`!Path` instance.
|
Added return value, return the new :class:`!Path` instance.
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: Path.move(target)
|
||||||
|
|
||||||
|
Move this file or directory tree to the given *target*, and return a new
|
||||||
|
:class:`!Path` instance pointing to *target*.
|
||||||
|
|
||||||
|
If the *target* doesn't exist it will be created. If both this path and the
|
||||||
|
*target* are existing files, then the target is overwritten. If both paths
|
||||||
|
point to the same file or directory, or the *target* is a non-empty
|
||||||
|
directory, then :exc:`OSError` is raised.
|
||||||
|
|
||||||
|
If both paths are on the same filesystem, the move is performed with
|
||||||
|
:func:`os.replace`. Otherwise, this path is copied (preserving metadata and
|
||||||
|
symlinks) and then deleted.
|
||||||
|
|
||||||
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
|
|
||||||
.. method:: Path.unlink(missing_ok=False)
|
.. method:: Path.unlink(missing_ok=False)
|
||||||
|
|
||||||
Remove this file or symbolic link. If the path points to a directory,
|
Remove this file or symbolic link. If the path points to a directory,
|
||||||
|
|
|
@ -185,10 +185,13 @@ os
|
||||||
pathlib
|
pathlib
|
||||||
-------
|
-------
|
||||||
|
|
||||||
* Add methods to :class:`pathlib.Path` to recursively copy or remove files:
|
* 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
|
* :meth:`~pathlib.Path.copy` copies a file or directory tree to a given
|
||||||
destination.
|
destination.
|
||||||
|
* :meth:`~pathlib.Path.move` moves a file or directory tree to a given
|
||||||
|
destination.
|
||||||
* :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`.)
|
||||||
|
|
|
@ -14,7 +14,7 @@ resemble pathlib's PurePath and Path respectively.
|
||||||
import functools
|
import functools
|
||||||
import operator
|
import operator
|
||||||
import posixpath
|
import posixpath
|
||||||
from errno import EINVAL
|
from errno import EINVAL, EXDEV
|
||||||
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 pathlib._os import copyfileobj
|
from pathlib._os import copyfileobj
|
||||||
|
@ -928,6 +928,25 @@ class PathBase(PurePathBase):
|
||||||
"""
|
"""
|
||||||
raise UnsupportedOperation(self._unsupported_msg('replace()'))
|
raise UnsupportedOperation(self._unsupported_msg('replace()'))
|
||||||
|
|
||||||
|
def move(self, target):
|
||||||
|
"""
|
||||||
|
Recursively move this file or directory tree to the given destination.
|
||||||
|
"""
|
||||||
|
self._ensure_different_file(target)
|
||||||
|
try:
|
||||||
|
return self.replace(target)
|
||||||
|
except UnsupportedOperation:
|
||||||
|
pass
|
||||||
|
except TypeError:
|
||||||
|
if not isinstance(target, PathBase):
|
||||||
|
raise
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno != EXDEV:
|
||||||
|
raise
|
||||||
|
target = self.copy(target, follow_symlinks=False, preserve_metadata=True)
|
||||||
|
self.delete()
|
||||||
|
return target
|
||||||
|
|
||||||
def chmod(self, mode, *, follow_symlinks=True):
|
def chmod(self, mode, *, follow_symlinks=True):
|
||||||
"""
|
"""
|
||||||
Change the permissions of the path, like os.chmod().
|
Change the permissions of the path, like os.chmod().
|
||||||
|
|
|
@ -45,6 +45,19 @@ delete_use_fd_functions = (
|
||||||
{os.open, os.stat, os.unlink, os.rmdir} <= os.supports_dir_fd and
|
{os.open, os.stat, os.unlink, os.rmdir} <= os.supports_dir_fd and
|
||||||
os.listdir in os.supports_fd and os.stat in os.supports_follow_symlinks)
|
os.listdir in os.supports_fd and os.stat in os.supports_follow_symlinks)
|
||||||
|
|
||||||
|
def patch_replace(old_test):
|
||||||
|
def new_replace(self, target):
|
||||||
|
raise OSError(errno.EXDEV, "Cross-device link", self, target)
|
||||||
|
|
||||||
|
def new_test(self):
|
||||||
|
old_replace = self.cls.replace
|
||||||
|
self.cls.replace = new_replace
|
||||||
|
try:
|
||||||
|
old_test(self)
|
||||||
|
finally:
|
||||||
|
self.cls.replace = old_replace
|
||||||
|
return new_test
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tests for the pure classes.
|
# Tests for the pure classes.
|
||||||
#
|
#
|
||||||
|
@ -799,6 +812,55 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
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')
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
def test_move_file_other_fs(self):
|
||||||
|
self.test_move_file()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
def test_move_file_to_file_other_fs(self):
|
||||||
|
self.test_move_file_to_file()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
def test_move_file_to_dir_other_fs(self):
|
||||||
|
self.test_move_file_to_dir()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
def test_move_dir_other_fs(self):
|
||||||
|
self.test_move_dir()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
def test_move_dir_to_dir_other_fs(self):
|
||||||
|
self.test_move_dir_to_dir()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
def test_move_dir_into_itself_other_fs(self):
|
||||||
|
self.test_move_dir_into_itself()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_file_symlink_other_fs(self):
|
||||||
|
self.test_move_file_symlink()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_file_symlink_to_itself_other_fs(self):
|
||||||
|
self.test_move_file_symlink_to_itself()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_dir_symlink_other_fs(self):
|
||||||
|
self.test_move_dir_symlink()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_dir_symlink_to_itself_other_fs(self):
|
||||||
|
self.test_move_dir_symlink_to_itself()
|
||||||
|
|
||||||
|
@patch_replace
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_dangling_symlink_other_fs(self):
|
||||||
|
self.test_move_dangling_symlink()
|
||||||
|
|
||||||
def test_resolve_nonexist_relative_issue38671(self):
|
def test_resolve_nonexist_relative_issue38671(self):
|
||||||
p = self.cls('non', 'exist')
|
p = self.cls('non', 'exist')
|
||||||
|
|
||||||
|
|
|
@ -2072,6 +2072,125 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
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'))
|
||||||
|
|
||||||
|
def test_move_file(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'fileA'
|
||||||
|
source_text = source.read_text()
|
||||||
|
target = base / 'fileA_moved'
|
||||||
|
result = source.move(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
|
self.assertFalse(source.exists())
|
||||||
|
self.assertTrue(target.exists())
|
||||||
|
self.assertEqual(source_text, target.read_text())
|
||||||
|
|
||||||
|
def test_move_file_to_file(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'fileA'
|
||||||
|
source_text = source.read_text()
|
||||||
|
target = base / 'dirB' / 'fileB'
|
||||||
|
result = source.move(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
|
self.assertFalse(source.exists())
|
||||||
|
self.assertTrue(target.exists())
|
||||||
|
self.assertEqual(source_text, target.read_text())
|
||||||
|
|
||||||
|
def test_move_file_to_dir(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'fileA'
|
||||||
|
target = base / 'dirB'
|
||||||
|
self.assertRaises(OSError, source.move, target)
|
||||||
|
|
||||||
|
def test_move_file_to_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'fileA'
|
||||||
|
self.assertRaises(OSError, source.move, source)
|
||||||
|
|
||||||
|
def test_move_dir(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'dirC'
|
||||||
|
target = base / 'dirC_moved'
|
||||||
|
result = source.move(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
|
self.assertFalse(source.exists())
|
||||||
|
self.assertTrue(target.is_dir())
|
||||||
|
self.assertTrue(target.joinpath('dirD').is_dir())
|
||||||
|
self.assertTrue(target.joinpath('dirD', 'fileD').is_file())
|
||||||
|
self.assertEqual(target.joinpath('dirD', 'fileD').read_text(),
|
||||||
|
"this is file D\n")
|
||||||
|
self.assertTrue(target.joinpath('fileC').is_file())
|
||||||
|
self.assertTrue(target.joinpath('fileC').read_text(),
|
||||||
|
"this is file C\n")
|
||||||
|
|
||||||
|
def test_move_dir_to_dir(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'dirC'
|
||||||
|
target = base / 'dirB'
|
||||||
|
self.assertRaises(OSError, source.move, target)
|
||||||
|
self.assertTrue(source.exists())
|
||||||
|
self.assertTrue(target.exists())
|
||||||
|
|
||||||
|
def test_move_dir_to_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'dirC'
|
||||||
|
self.assertRaises(OSError, source.move, source)
|
||||||
|
self.assertTrue(source.exists())
|
||||||
|
|
||||||
|
def test_move_dir_into_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'dirC'
|
||||||
|
target = base / 'dirC' / 'bar'
|
||||||
|
self.assertRaises(OSError, source.move, target)
|
||||||
|
self.assertTrue(source.exists())
|
||||||
|
self.assertFalse(target.exists())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_file_symlink(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkA'
|
||||||
|
source_readlink = source.readlink()
|
||||||
|
target = base / 'linkA_moved'
|
||||||
|
result = source.move(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
|
self.assertFalse(source.exists())
|
||||||
|
self.assertTrue(target.is_symlink())
|
||||||
|
self.assertEqual(source_readlink, target.readlink())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_file_symlink_to_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkA'
|
||||||
|
self.assertRaises(OSError, source.move, source)
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_dir_symlink(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkB'
|
||||||
|
source_readlink = source.readlink()
|
||||||
|
target = base / 'linkB_moved'
|
||||||
|
result = source.move(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
|
self.assertFalse(source.exists())
|
||||||
|
self.assertTrue(target.is_symlink())
|
||||||
|
self.assertEqual(source_readlink, target.readlink())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_dir_symlink_to_itself(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkB'
|
||||||
|
self.assertRaises(OSError, source.move, source)
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_move_dangling_symlink(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'brokenLink'
|
||||||
|
source_readlink = source.readlink()
|
||||||
|
target = base / 'brokenLink_moved'
|
||||||
|
result = source.move(target)
|
||||||
|
self.assertEqual(result, target)
|
||||||
|
self.assertFalse(source.exists())
|
||||||
|
self.assertTrue(target.is_symlink())
|
||||||
|
self.assertEqual(source_readlink, target.readlink())
|
||||||
|
|
||||||
def test_iterdir(self):
|
def test_iterdir(self):
|
||||||
P = self.cls
|
P = self.cls
|
||||||
p = P(self.base)
|
p = P(self.base)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Add :meth:`pathlib.Path.move`, which moves a file or directory tree.
|
Loading…
Add table
Add a link
Reference in a new issue