Skip to content

Commit 9106878

Browse files
committed
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'] ```
1 parent f3759d2 commit 9106878

File tree

3 files changed

+41
-6
lines changed

3 files changed

+41
-6
lines changed

Lib/shutil.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@
7575
class Error(OSError):
7676
pass
7777

78+
class ErrorGroup(Error):
79+
"""Raised when multiple exceptions have been caught"""
80+
7881
class SameFileError(Error):
7982
"""Raised when source and destination are the same file."""
8083

@@ -590,12 +593,9 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
590593
copytree(srcobj, dstname, symlinks, ignore, copy_function,
591594
ignore_dangling_symlinks, dirs_exist_ok)
592595
else:
593-
# Will raise a SpecialFileError for unsupported file types
594596
copy_function(srcobj, dstname)
595-
# catch the Error from the recursive copytree so that we can
596-
# continue with other files
597-
except Error as err:
598-
errors.extend(err.args[0])
597+
except ErrorGroup as err_group:
598+
errors.extend(err_group.args[0])
599599
except OSError as why:
600600
errors.append((srcname, dstname, str(why)))
601601
try:
@@ -605,7 +605,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
605605
if getattr(why, 'winerror', None) is None:
606606
errors.append((src, dst, str(why)))
607607
if errors:
608-
raise Error(errors)
608+
raise ErrorGroup(errors)
609609
return dst
610610

611611
def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,

Lib/test/test_shutil.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,39 @@ def test_copytree_subdirectory(self):
10991099
rv = shutil.copytree(src_dir, dst_dir)
11001100
self.assertEqual(['pol'], os.listdir(rv))
11011101

1102+
def test_copytree_to_itself_gives_sensible_error_message(self):
1103+
base_dir = self.mkdtemp()
1104+
self.addCleanup(shutil.rmtree, base_dir, ignore_errors=True)
1105+
src_dir = os.path.join(base_dir, "src")
1106+
os.makedirs(src_dir)
1107+
create_file((src_dir, "somefilename"), "somecontent")
1108+
self._assert_are_the_same_file_is_raised(src_dir, src_dir)
1109+
1110+
@os_helper.skip_unless_symlink
1111+
def test_copytree_to_backpointing_symlink_gives_sensible_error_message(self):
1112+
base_dir = self.mkdtemp()
1113+
self.addCleanup(shutil.rmtree, base_dir, ignore_errors=True)
1114+
src_dir = os.path.join(base_dir, "src")
1115+
target_dir = os.path.join(base_dir, "target")
1116+
os.makedirs(src_dir)
1117+
os.makedirs(target_dir)
1118+
some_file = os.path.join(src_dir, "somefilename")
1119+
create_file(some_file, "somecontent")
1120+
os.symlink(some_file, os.path.join(target_dir, "somefilename"))
1121+
self._assert_are_the_same_file_is_raised(src_dir, target_dir)
1122+
1123+
def _assert_are_the_same_file_is_raised(self, src_dir, target_dir):
1124+
try:
1125+
shutil.copytree(src_dir, target_dir, dirs_exist_ok=True)
1126+
self.fail("shutil.Error should have been raised")
1127+
except Error as error:
1128+
self.assertEqual(len(error.args[0]), 1)
1129+
if sys.platform == "win32":
1130+
self.assertIn("it is being used by another process", error.args[0][0][2])
1131+
else:
1132+
self.assertIn("are the same file", error.args[0][0][2])
1133+
1134+
11021135
class TestCopy(BaseTest, unittest.TestCase):
11031136

11041137
### shutil.copymode
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make exception from :func:`shutil.copytree` readable when a
2+
:exc:`shutil.SameFileError` is raised.

0 commit comments

Comments
 (0)