mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 23:21:29 +00:00 
			
		
		
		
	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
					
				
					 6 changed files with 96 additions and 4 deletions
				
			
		| 
						 | 
				
			
			@ -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 @@ 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -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().
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue