mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +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
 | 
					   .. 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)
 | 
					.. method:: Path.rename(target)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   Rename this file or directory to the given *target*, and return a new
 | 
					   Rename this file or directory to the given *target*, and return a new
 | 
				
			||||||
| 
						 | 
					@ -1633,6 +1645,15 @@ Copying, moving and deleting
 | 
				
			||||||
   .. versionadded:: 3.14
 | 
					   .. 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)
 | 
					.. 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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -188,10 +188,10 @@ pathlib
 | 
				
			||||||
* Add methods to :class:`pathlib.Path` to recursively copy, move, or remove
 | 
					* Add methods to :class:`pathlib.Path` to recursively copy, move, or remove
 | 
				
			||||||
  files and directories:
 | 
					  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 destination.
 | 
				
			||||||
    destination.
 | 
					  * :meth:`~pathlib.Path.copy_into` copies *into* a destination directory.
 | 
				
			||||||
  * :meth:`~pathlib.Path.move` moves a file or directory tree to a given
 | 
					  * :meth:`~pathlib.Path.move` moves a file or directory tree to a destination.
 | 
				
			||||||
    destination.
 | 
					  * :meth:`~pathlib.Path.move_into` moves *into* a destination directory.
 | 
				
			||||||
  * :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`.)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -904,6 +904,24 @@ def copy(self, target, *, follow_symlinks=True, dirs_exist_ok=False,
 | 
				
			||||||
                on_error(err)
 | 
					                on_error(err)
 | 
				
			||||||
        return target
 | 
					        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):
 | 
					    def rename(self, target):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Rename this path to the target path.
 | 
					        Rename this path to the target path.
 | 
				
			||||||
| 
						 | 
					@ -947,6 +965,19 @@ def move(self, target):
 | 
				
			||||||
        self.delete()
 | 
					        self.delete()
 | 
				
			||||||
        return target
 | 
					        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):
 | 
					    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().
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -861,6 +861,14 @@ def test_move_dir_symlink_to_itself_other_fs(self):
 | 
				
			||||||
    def test_move_dangling_symlink_other_fs(self):
 | 
					    def test_move_dangling_symlink_other_fs(self):
 | 
				
			||||||
        self.test_move_dangling_symlink()
 | 
					        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):
 | 
					    def test_resolve_nonexist_relative_issue38671(self):
 | 
				
			||||||
        p = self.cls('non', 'exist')
 | 
					        p = self.cls('non', 'exist')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2072,6 +2072,20 @@ def test_copy_dangling_symlink(self):
 | 
				
			||||||
        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_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):
 | 
					    def test_move_file(self):
 | 
				
			||||||
        base = self.cls(self.base)
 | 
					        base = self.cls(self.base)
 | 
				
			||||||
        source = base / 'fileA'
 | 
					        source = base / 'fileA'
 | 
				
			||||||
| 
						 | 
					@ -2191,6 +2205,22 @@ def test_move_dangling_symlink(self):
 | 
				
			||||||
        self.assertTrue(target.is_symlink())
 | 
					        self.assertTrue(target.is_symlink())
 | 
				
			||||||
        self.assertEqual(source_readlink, target.readlink())
 | 
					        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):
 | 
					    def test_iterdir(self):
 | 
				
			||||||
        P = self.cls
 | 
					        P = self.cls
 | 
				
			||||||
        p = P(self.base)
 | 
					        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