From 533be297314a60f9903975d66b980036b5ca4313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Berland?= Date: Sat, 28 Dec 2024 10:40:57 +0100 Subject: [PATCH] Use explicit exception when shutil._copytree should group exceptions This resolves a bug where a non-recursive error is assumed to be a list of exceptions and presenting the error as: ``` shutil.Error: ['<', 'D', 'i', 'r', 'E', 'n', 't', 'r', 'y', ' ', ..., 's', 'a', 'm', 'e', ' ', 'f', 'i', 'l', 'e'] ``` --- Lib/shutil.py | 12 +++---- Lib/test/test_shutil.py | 33 +++++++++++++++++++ ...-12-28-16-25-42.gh-issue-102931.55o5kb.rst | 2 ++ 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-12-28-16-25-42.gh-issue-102931.55o5kb.rst diff --git a/Lib/shutil.py b/Lib/shutil.py index 171489ca41f2a7..a536740ecde211 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -67,6 +67,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.""" @@ -527,12 +530,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: @@ -542,7 +542,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 1f18b1f09b5858..9c20228eb0e6a5 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1101,6 +1101,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.