diff --git a/Lib/shutil.py b/Lib/shutil.py index 8d8fe145567822..a69fb4fb14097e 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -75,6 +75,9 @@ class Error(OSError): pass +class ErrorGroup(Error): + """Raised when multiple exceptions have been caught""" + class SameFileError(Error): """Raised when source and destination are the same file.""" @@ -590,12 +593,9 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function, copytree(srcobj, dstname, symlinks, ignore, copy_function, ignore_dangling_symlinks, dirs_exist_ok) else: - # Will raise a SpecialFileError for unsupported file types copy_function(srcobj, dstname) - # catch the Error from the recursive copytree so that we can - # continue with other files - except Error as err: - errors.extend(err.args[0]) + except ErrorGroup as err_group: + errors.extend(err_group.args[0]) except OSError as why: errors.append((srcname, dstname, str(why))) try: @@ -605,7 +605,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function, if getattr(why, 'winerror', None) is None: errors.append((src, dst, str(why))) if errors: - raise Error(errors) + raise ErrorGroup(errors) return dst def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index ebb6cf88336249..308fd334276f2f 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1099,6 +1099,39 @@ def test_copytree_subdirectory(self): rv = shutil.copytree(src_dir, dst_dir) self.assertEqual(['pol'], os.listdir(rv)) + def test_copytree_to_itself_gives_sensible_error_message(self): + base_dir = self.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir, ignore_errors=True) + src_dir = os.path.join(base_dir, "src") + os.makedirs(src_dir) + create_file((src_dir, "somefilename"), "somecontent") + self._assert_are_the_same_file_is_raised(src_dir, src_dir) + + @os_helper.skip_unless_symlink + def test_copytree_to_backpointing_symlink_gives_sensible_error_message(self): + base_dir = self.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir, ignore_errors=True) + src_dir = os.path.join(base_dir, "src") + target_dir = os.path.join(base_dir, "target") + os.makedirs(src_dir) + os.makedirs(target_dir) + some_file = os.path.join(src_dir, "somefilename") + create_file(some_file, "somecontent") + os.symlink(some_file, os.path.join(target_dir, "somefilename")) + self._assert_are_the_same_file_is_raised(src_dir, target_dir) + + def _assert_are_the_same_file_is_raised(self, src_dir, target_dir): + try: + shutil.copytree(src_dir, target_dir, dirs_exist_ok=True) + self.fail("shutil.Error should have been raised") + except Error as error: + self.assertEqual(len(error.args[0]), 1) + if sys.platform == "win32": + self.assertIn("it is being used by another process", error.args[0][0][2]) + else: + self.assertIn("are the same file", error.args[0][0][2]) + + class TestCopy(BaseTest, unittest.TestCase): ### shutil.copymode diff --git a/Misc/NEWS.d/next/Library/2024-12-28-16-25-42.gh-issue-102931.55o5kb.rst b/Misc/NEWS.d/next/Library/2024-12-28-16-25-42.gh-issue-102931.55o5kb.rst new file mode 100644 index 00000000000000..d87d0ce59bfc3d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-28-16-25-42.gh-issue-102931.55o5kb.rst @@ -0,0 +1,2 @@ +Make exception from :func:`shutil.copytree` readable when a +:exc:`shutil.SameFileError` is raised.