diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 4a339358251..f66d36a32cb 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1539,50 +1539,33 @@ Creating files and directories Copying, renaming and deleting ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. method:: Path.copy(target, *, follow_symlinks=True, preserve_metadata=False) +.. method:: Path.copy(target, *, follow_symlinks=True, dirs_exist_ok=False, \ + preserve_metadata=False, ignore=None, on_error=None) - Copy the contents of this file to the *target* file. If *target* specifies - a file that already exists, it will be replaced. + Copy this file or directory tree to the given *target*, and return a new + :class:`!Path` instance pointing to *target*. - If *follow_symlinks* is false, and this file is a symbolic link, *target* - will be created as a symbolic link. If *follow_symlinks* is true and this - file is a symbolic link, *target* will be a copy of the symlink target. + If the source is a file, the target will be replaced if it is an existing + file. If the source is a symlink and *follow_symlinks* is true (the + default), the symlink's target is copied. Otherwise, the symlink is + recreated at the destination. - If *preserve_metadata* is false (the default), only the file data is - guaranteed to be copied. Set *preserve_metadata* to true to ensure that the - file mode (permissions), flags, last access and modification times, and - extended attributes are copied where supported. This argument has no effect - on Windows, where metadata is always preserved when copying. + If the source is a directory and *dirs_exist_ok* is false (the default), a + :exc:`FileExistsError` is raised if the target is an existing directory. + If *dirs_exists_ok* is true, the copying operation will overwrite + existing files within the destination tree with corresponding files + from the source tree. - .. versionadded:: 3.14 - - -.. method:: Path.copytree(target, *, follow_symlinks=True, \ - preserve_metadata=False, dirs_exist_ok=False, \ - ignore=None, on_error=None) - - Recursively copy this directory tree to the given destination. - - If a symlink is encountered in the source tree, and *follow_symlinks* is - true (the default), the symlink's target is copied. Otherwise, the symlink - is recreated in the destination tree. - - If *preserve_metadata* is false (the default), only the directory structure + If *preserve_metadata* is false (the default), only directory structures and file data are guaranteed to be copied. Set *preserve_metadata* to true to ensure that file and directory permissions, flags, last access and modification times, and extended attributes are copied where supported. - This argument has no effect on Windows, where metadata is always preserved - when copying. - - If the destination is an existing directory and *dirs_exist_ok* is false - (the default), a :exc:`FileExistsError` is raised. Otherwise, the copying - operation will continue if it encounters existing directories, and files - within the destination tree will be overwritten by corresponding files from - the source tree. + This argument has no effect when copying files on Windows (where + metadata is always preserved). If *ignore* is given, it should be a callable accepting one argument: a - file or directory path within the source tree. The callable may return true - to suppress copying of the path. + source file or directory path. The callable may return true to suppress + copying of the path. If *on_error* is given, it should be a callable accepting one argument: an instance of :exc:`OSError`. The callable may re-raise the exception or do diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 27594c3dea8..3f53f6b9400 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -146,10 +146,8 @@ pathlib * Add methods to :class:`pathlib.Path` to recursively copy or remove files: - * :meth:`~pathlib.Path.copy` copies the content of one file to another, like - :func:`shutil.copyfile`. - * :meth:`~pathlib.Path.copytree` copies one directory tree to another, like - :func:`shutil.copytree`. + * :meth:`~pathlib.Path.copy` copies a file or directory tree to a given + destination. * :meth:`~pathlib.Path.delete` removes a file or directory tree. (Contributed by Barney Gale in :gh:`73991`.) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 2298a249529..5da3acd3199 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -5,8 +5,8 @@ paths with operations that have semantics appropriate for different operating systems. """ -from ._os import * -from ._local import * +from pathlib._abc import * +from pathlib._local import * -__all__ = (_os.__all__ + +__all__ = (_abc.__all__ + _local.__all__) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 8c799196e47..500846d19cf 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -16,7 +16,16 @@ import operator import posixpath 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 ._os import UnsupportedOperation, copyfileobj +from pathlib._os import copyfileobj + + +__all__ = ["UnsupportedOperation"] + + +class UnsupportedOperation(NotImplementedError): + """An exception that is raised when an unsupported operation is attempted. + """ + pass @functools.cache @@ -761,6 +770,13 @@ class PathBase(PurePathBase): """ raise UnsupportedOperation(self._unsupported_msg('symlink_to()')) + def _symlink_to_target_of(self, link): + """ + Make this path a symlink with the same target as the given link. This + is used by copy(). + """ + self.symlink_to(link.readlink()) + def hardlink_to(self, target): """ Make this path a hard link pointing to the same file as *target*. @@ -806,21 +822,12 @@ class PathBase(PurePathBase): metadata = self._read_metadata(keys, follow_symlinks=follow_symlinks) target._write_metadata(metadata, follow_symlinks=follow_symlinks) - def copy(self, target, *, follow_symlinks=True, preserve_metadata=False): + def _copy_file(self, target): """ - Copy the contents of this file to the given target. If this file is a - symlink and follow_symlinks is false, a symlink will be created at the - target. + Copy the contents of this file to the given target. """ - if not isinstance(target, PathBase): - target = self.with_segments(target) if self._samefile_safe(target): raise OSError(f"{self!r} and {target!r} are the same file") - if not follow_symlinks and self.is_symlink(): - target.symlink_to(self.readlink()) - if preserve_metadata: - self._copy_metadata(target, follow_symlinks=False) - return with self.open('rb') as source_f: try: with target.open('wb') as target_f: @@ -832,42 +839,39 @@ class PathBase(PurePathBase): f'Directory does not exist: {target}') from e else: raise - if preserve_metadata: - self._copy_metadata(target) - def copytree(self, target, *, follow_symlinks=True, - preserve_metadata=False, dirs_exist_ok=False, - ignore=None, on_error=None): + def copy(self, target, *, follow_symlinks=True, dirs_exist_ok=False, + preserve_metadata=False, ignore=None, on_error=None): """ - Recursively copy this directory tree to the given destination. + Recursively copy this file or directory tree to the given destination. """ if not isinstance(target, PathBase): target = self.with_segments(target) - if on_error is None: - def on_error(err): - raise err stack = [(self, target)] while stack: - source_dir, target_dir = stack.pop() + src, dst = stack.pop() try: - sources = source_dir.iterdir() - target_dir.mkdir(exist_ok=dirs_exist_ok) - if preserve_metadata: - source_dir._copy_metadata(target_dir) - for source in sources: - if ignore and ignore(source): - continue - try: - if source.is_dir(follow_symlinks=follow_symlinks): - stack.append((source, target_dir.joinpath(source.name))) - else: - source.copy(target_dir.joinpath(source.name), - follow_symlinks=follow_symlinks, - preserve_metadata=preserve_metadata) - except OSError as err: - on_error(err) + if not follow_symlinks and src.is_symlink(): + dst._symlink_to_target_of(src) + if preserve_metadata: + src._copy_metadata(dst, follow_symlinks=False) + elif src.is_dir(): + children = src.iterdir() + dst.mkdir(exist_ok=dirs_exist_ok) + for child in children: + if not (ignore and ignore(child)): + stack.append((child, dst.joinpath(child.name))) + if preserve_metadata: + src._copy_metadata(dst) + else: + src._copy_file(dst) + if preserve_metadata: + src._copy_metadata(dst) except OSError as err: + if on_error is None: + raise on_error(err) + return target def rename(self, target): """ diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 6e2f88c9342..8f5c58c16ef 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -18,9 +18,9 @@ try: except ImportError: grp = None -from ._os import (UnsupportedOperation, copyfile, file_metadata_keys, - read_file_metadata, write_file_metadata) -from ._abc import PurePathBase, PathBase +from pathlib._os import (copyfile, file_metadata_keys, read_file_metadata, + write_file_metadata) +from pathlib._abc import UnsupportedOperation, PurePathBase, PathBase __all__ = [ @@ -788,25 +788,18 @@ class Path(PathBase, PurePath): _write_metadata = write_file_metadata if copyfile: - def copy(self, target, *, follow_symlinks=True, preserve_metadata=False): + def _copy_file(self, target): """ - Copy the contents of this file to the given target. If this file is a - symlink and follow_symlinks is false, a symlink will be created at the - target. + Copy the contents of this file to the given target. """ try: target = os.fspath(target) except TypeError: if not isinstance(target, PathBase): raise + PathBase._copy_file(self, target) else: - try: - copyfile(os.fspath(self), target, follow_symlinks) - return - except UnsupportedOperation: - pass # Fall through to generic code. - PathBase.copy(self, target, follow_symlinks=follow_symlinks, - preserve_metadata=preserve_metadata) + copyfile(os.fspath(self), target) def chmod(self, mode, *, follow_symlinks=True): """ @@ -894,6 +887,14 @@ class Path(PathBase, PurePath): """ os.symlink(target, self, target_is_directory) + if os.name == 'nt': + def _symlink_to_target_of(self, link): + """ + Make this path a symlink with the same target as the given link. + This is used by copy(). + """ + self.symlink_to(link.readlink(), link.is_dir()) + if hasattr(os, "link"): def hardlink_to(self, target): """ diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 164ee8e9034..63dbe131bae 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -20,15 +20,6 @@ except ImportError: _winapi = None -__all__ = ["UnsupportedOperation"] - - -class UnsupportedOperation(NotImplementedError): - """An exception that is raised when an unsupported operation is attempted. - """ - pass - - def get_copy_blocksize(infd): """Determine blocksize for fastcopying on Linux. Hopefully the whole file will be copied in a single call. @@ -101,44 +92,12 @@ else: copyfd = None -if _winapi and hasattr(_winapi, 'CopyFile2') and hasattr(os.stat_result, 'st_file_attributes'): - def _is_dirlink(path): - try: - st = os.lstat(path) - except (OSError, ValueError): - return False - return (st.st_file_attributes & stat.FILE_ATTRIBUTE_DIRECTORY and - st.st_reparse_tag == stat.IO_REPARSE_TAG_SYMLINK) - - def copyfile(source, target, follow_symlinks): +if _winapi and hasattr(_winapi, 'CopyFile2'): + def copyfile(source, target): """ Copy from one file to another using CopyFile2 (Windows only). """ - if follow_symlinks: - _winapi.CopyFile2(source, target, 0) - else: - # Use COPY_FILE_COPY_SYMLINK to copy a file symlink. - flags = _winapi.COPY_FILE_COPY_SYMLINK - try: - _winapi.CopyFile2(source, target, flags) - return - except OSError as err: - # Check for ERROR_ACCESS_DENIED - if err.winerror == 5 and _is_dirlink(source): - pass - else: - raise - - # Add COPY_FILE_DIRECTORY to copy a directory symlink. - flags |= _winapi.COPY_FILE_DIRECTORY - try: - _winapi.CopyFile2(source, target, flags) - except OSError as err: - # Check for ERROR_INVALID_PARAMETER - if err.winerror == 87: - raise UnsupportedOperation(err) from None - else: - raise + _winapi.CopyFile2(source, target, 0) else: copyfile = None diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 9e922259cba..fa151b590d7 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -709,19 +709,19 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest): @unittest.skipIf(sys.platform == "win32" or sys.platform == "wasi", "directories are always readable on Windows and WASI") @unittest.skipIf(root_in_posix, "test fails with root privilege") - def test_copytree_no_read_permission(self): + def test_copy_dir_no_read_permission(self): base = self.cls(self.base) source = base / 'dirE' target = base / 'copyE' - self.assertRaises(PermissionError, source.copytree, target) + self.assertRaises(PermissionError, source.copy, target) self.assertFalse(target.exists()) errors = [] - source.copytree(target, on_error=errors.append) + source.copy(target, on_error=errors.append) self.assertEqual(len(errors), 1) self.assertIsInstance(errors[0], PermissionError) self.assertFalse(target.exists()) - def test_copytree_preserve_metadata(self): + def test_copy_dir_preserve_metadata(self): base = self.cls(self.base) source = base / 'dirC' if hasattr(os, 'chmod'): @@ -729,7 +729,7 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest): if hasattr(os, 'chflags') and hasattr(stat, 'UF_NODUMP'): os.chflags(source / 'fileC', stat.UF_NODUMP) target = base / 'copyA' - source.copytree(target, preserve_metadata=True) + source.copy(target, preserve_metadata=True) for subpath in ['.', 'fileC', 'dirD', 'dirD/fileD']: source_st = source.joinpath(subpath).stat() @@ -741,13 +741,13 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest): self.assertEqual(source_st.st_flags, target_st.st_flags) @os_helper.skip_unless_xattr - def test_copytree_preserve_metadata_xattrs(self): + def test_copy_dir_preserve_metadata_xattrs(self): base = self.cls(self.base) source = base / 'dirC' source_file = source.joinpath('dirD', 'fileD') os.setxattr(source_file, b'user.foo', b'42') target = base / 'copyA' - source.copytree(target, preserve_metadata=True) + source.copy(target, preserve_metadata=True) target_file = target.joinpath('dirD', 'fileD') self.assertEqual(os.getxattr(target_file, b'user.foo'), b'42') diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 443a4e989fb..629a1d4bdeb 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -5,8 +5,7 @@ import errno import stat import unittest -from pathlib._os import UnsupportedOperation -from pathlib._abc import ParserBase, PurePathBase, PathBase +from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase import posixpath from test.support import is_wasi @@ -1732,23 +1731,18 @@ class DummyPathTest(DummyPurePathTest): base = self.cls(self.base) source = base / 'fileA' target = base / 'copyA' - source.copy(target) + result = source.copy(target) + self.assertEqual(result, target) self.assertTrue(target.exists()) self.assertEqual(source.read_text(), target.read_text()) - def test_copy_directory(self): - base = self.cls(self.base) - source = base / 'dirA' - target = base / 'copyA' - with self.assertRaises(OSError): - source.copy(target) - @needs_symlinks def test_copy_symlink_follow_symlinks_true(self): base = self.cls(self.base) source = base / 'linkA' target = base / 'copyA' - source.copy(target) + result = source.copy(target) + self.assertEqual(result, target) self.assertTrue(target.exists()) self.assertFalse(target.is_symlink()) self.assertEqual(source.read_text(), target.read_text()) @@ -1758,7 +1752,8 @@ class DummyPathTest(DummyPurePathTest): base = self.cls(self.base) source = base / 'linkA' target = base / 'copyA' - source.copy(target, follow_symlinks=False) + result = source.copy(target, follow_symlinks=False) + self.assertEqual(result, target) self.assertTrue(target.exists()) self.assertTrue(target.is_symlink()) self.assertEqual(source.readlink(), target.readlink()) @@ -1768,20 +1763,22 @@ class DummyPathTest(DummyPurePathTest): base = self.cls(self.base) source = base / 'linkB' target = base / 'copyA' - source.copy(target, follow_symlinks=False) + result = source.copy(target, follow_symlinks=False) + self.assertEqual(result, target) self.assertTrue(target.exists()) self.assertTrue(target.is_symlink()) self.assertEqual(source.readlink(), target.readlink()) - def test_copy_to_existing_file(self): + def test_copy_file_to_existing_file(self): base = self.cls(self.base) source = base / 'fileA' target = base / 'dirB' / 'fileB' - source.copy(target) + result = source.copy(target) + self.assertEqual(result, target) self.assertTrue(target.exists()) self.assertEqual(source.read_text(), target.read_text()) - def test_copy_to_existing_directory(self): + def test_copy_file_to_existing_directory(self): base = self.cls(self.base) source = base / 'fileA' target = base / 'dirA' @@ -1789,12 +1786,13 @@ class DummyPathTest(DummyPurePathTest): source.copy(target) @needs_symlinks - def test_copy_to_existing_symlink(self): + def test_copy_file_to_existing_symlink(self): base = self.cls(self.base) source = base / 'dirB' / 'fileB' target = base / 'linkA' real_target = base / 'fileA' - source.copy(target) + result = source.copy(target) + self.assertEqual(result, target) self.assertTrue(target.exists()) self.assertTrue(target.is_symlink()) self.assertTrue(real_target.exists()) @@ -1802,32 +1800,35 @@ class DummyPathTest(DummyPurePathTest): self.assertEqual(source.read_text(), real_target.read_text()) @needs_symlinks - def test_copy_to_existing_symlink_follow_symlinks_false(self): + def test_copy_file_to_existing_symlink_follow_symlinks_false(self): base = self.cls(self.base) source = base / 'dirB' / 'fileB' target = base / 'linkA' real_target = base / 'fileA' - source.copy(target, follow_symlinks=False) + result = source.copy(target, follow_symlinks=False) + self.assertEqual(result, target) self.assertTrue(target.exists()) self.assertTrue(target.is_symlink()) self.assertTrue(real_target.exists()) self.assertFalse(real_target.is_symlink()) self.assertEqual(source.read_text(), real_target.read_text()) - def test_copy_empty(self): + def test_copy_file_empty(self): base = self.cls(self.base) source = base / 'empty' target = base / 'copyA' source.write_bytes(b'') - source.copy(target) + result = source.copy(target) + self.assertEqual(result, target) self.assertTrue(target.exists()) self.assertEqual(target.read_bytes(), b'') - def test_copytree_simple(self): + def test_copy_dir_simple(self): base = self.cls(self.base) source = base / 'dirC' target = base / 'copyC' - source.copytree(target) + result = source.copy(target) + self.assertEqual(result, target) self.assertTrue(target.is_dir()) self.assertTrue(target.joinpath('dirD').is_dir()) self.assertTrue(target.joinpath('dirD', 'fileD').is_file()) @@ -1837,7 +1838,7 @@ class DummyPathTest(DummyPurePathTest): self.assertTrue(target.joinpath('fileC').read_text(), "this is file C\n") - def test_copytree_complex(self, follow_symlinks=True): + def test_copy_dir_complex(self, follow_symlinks=True): def ordered_walk(path): for dirpath, dirnames, filenames in path.walk(follow_symlinks=follow_symlinks): dirnames.sort() @@ -1853,7 +1854,8 @@ class DummyPathTest(DummyPurePathTest): # Perform the copy target = base / 'copyC' - source.copytree(target, follow_symlinks=follow_symlinks) + result = source.copy(target, follow_symlinks=follow_symlinks) + self.assertEqual(result, target) # Compare the source and target trees source_walk = ordered_walk(source) @@ -1879,24 +1881,25 @@ class DummyPathTest(DummyPurePathTest): self.assertEqual(source_file.read_bytes(), target_file.read_bytes()) self.assertEqual(source_file.readlink(), target_file.readlink()) - def test_copytree_complex_follow_symlinks_false(self): - self.test_copytree_complex(follow_symlinks=False) + def test_copy_dir_complex_follow_symlinks_false(self): + self.test_copy_dir_complex(follow_symlinks=False) - def test_copytree_to_existing_directory(self): + def test_copy_dir_to_existing_directory(self): base = self.cls(self.base) source = base / 'dirC' target = base / 'copyC' target.mkdir() target.joinpath('dirD').mkdir() - self.assertRaises(FileExistsError, source.copytree, target) + self.assertRaises(FileExistsError, source.copy, target) - def test_copytree_to_existing_directory_dirs_exist_ok(self): + def test_copy_dir_to_existing_directory_dirs_exist_ok(self): base = self.cls(self.base) source = base / 'dirC' target = base / 'copyC' target.mkdir() target.joinpath('dirD').mkdir() - source.copytree(target, dirs_exist_ok=True) + result = source.copy(target, dirs_exist_ok=True) + self.assertEqual(result, target) self.assertTrue(target.is_dir()) self.assertTrue(target.joinpath('dirD').is_dir()) self.assertTrue(target.joinpath('dirD', 'fileD').is_file()) @@ -1906,22 +1909,17 @@ class DummyPathTest(DummyPurePathTest): self.assertTrue(target.joinpath('fileC').read_text(), "this is file C\n") - def test_copytree_file(self): + def test_copy_missing_on_error(self): base = self.cls(self.base) - source = base / 'fileA' - target = base / 'copyA' - self.assertRaises(NotADirectoryError, source.copytree, target) - - def test_copytree_file_on_error(self): - base = self.cls(self.base) - source = base / 'fileA' + source = base / 'foo' target = base / 'copyA' errors = [] - source.copytree(target, on_error=errors.append) + result = source.copy(target, on_error=errors.append) + self.assertEqual(result, target) self.assertEqual(len(errors), 1) - self.assertIsInstance(errors[0], NotADirectoryError) + self.assertIsInstance(errors[0], FileNotFoundError) - def test_copytree_ignore_false(self): + def test_copy_dir_ignore_false(self): base = self.cls(self.base) source = base / 'dirC' target = base / 'copyC' @@ -1929,7 +1927,8 @@ class DummyPathTest(DummyPurePathTest): def ignore_false(path): ignores.append(path) return False - source.copytree(target, ignore=ignore_false) + result = source.copy(target, ignore=ignore_false) + self.assertEqual(result, target) self.assertEqual(set(ignores), { source / 'dirD', source / 'dirD' / 'fileD', @@ -1945,7 +1944,7 @@ class DummyPathTest(DummyPurePathTest): self.assertTrue(target.joinpath('fileC').read_text(), "this is file C\n") - def test_copytree_ignore_true(self): + def test_copy_dir_ignore_true(self): base = self.cls(self.base) source = base / 'dirC' target = base / 'copyC' @@ -1953,7 +1952,8 @@ class DummyPathTest(DummyPurePathTest): def ignore_true(path): ignores.append(path) return True - source.copytree(target, ignore=ignore_true) + result = source.copy(target, ignore=ignore_true) + self.assertEqual(result, target) self.assertEqual(set(ignores), { source / 'dirD', source / 'fileC', @@ -1965,7 +1965,7 @@ class DummyPathTest(DummyPurePathTest): self.assertFalse(target.joinpath('novel.txt').exists()) @needs_symlinks - def test_copytree_dangling_symlink(self): + def test_copy_dangling_symlink(self): base = self.cls(self.base) source = base / 'source' target = base / 'target' @@ -1973,10 +1973,11 @@ class DummyPathTest(DummyPurePathTest): source.mkdir() source.joinpath('link').symlink_to('nonexistent') - self.assertRaises(FileNotFoundError, source.copytree, target) + self.assertRaises(FileNotFoundError, source.copy, target) target2 = base / 'target2' - source.copytree(target2, follow_symlinks=False) + result = source.copy(target2, follow_symlinks=False) + self.assertEqual(result, target2) self.assertTrue(target2.joinpath('link').is_symlink()) self.assertEqual(target2.joinpath('link').readlink(), self.cls('nonexistent')) diff --git a/Misc/NEWS.d/next/Library/2024-05-15-01-36-08.gh-issue-73991.CGknDf.rst b/Misc/NEWS.d/next/Library/2024-05-15-01-36-08.gh-issue-73991.CGknDf.rst index c2953c65b27..d8e3bdf59ed 100644 --- a/Misc/NEWS.d/next/Library/2024-05-15-01-36-08.gh-issue-73991.CGknDf.rst +++ b/Misc/NEWS.d/next/Library/2024-05-15-01-36-08.gh-issue-73991.CGknDf.rst @@ -1,2 +1 @@ -Add :meth:`pathlib.Path.copy`, which copies the content of one file to another, -like :func:`shutil.copyfile`. +Add :meth:`pathlib.Path.copy`, which copies a file or directory to another. diff --git a/Misc/NEWS.d/next/Library/2024-06-19-03-09-11.gh-issue-73991.lU_jK9.rst b/Misc/NEWS.d/next/Library/2024-06-19-03-09-11.gh-issue-73991.lU_jK9.rst deleted file mode 100644 index 60a1b68d5bb..00000000000 --- a/Misc/NEWS.d/next/Library/2024-06-19-03-09-11.gh-issue-73991.lU_jK9.rst +++ /dev/null @@ -1 +0,0 @@ -Add :meth:`pathlib.Path.copytree`, which recursively copies a directory.