Skip to content

Commit 6e9603b

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 c810ed7 commit 6e9603b

File tree

3 files changed

+43
-5
lines changed

3 files changed

+43
-5
lines changed

Lib/shutil.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
class Error(OSError):
6868
pass
6969

70+
class ErrorGroup(Error):
71+
"""Used when gathering multiple exceptions"""
72+
7073
class SameFileError(Error):
7174
"""Raised when source and destination are the same file."""
7275

@@ -527,12 +530,11 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
527530
copytree(srcobj, dstname, symlinks, ignore, copy_function,
528531
ignore_dangling_symlinks, dirs_exist_ok)
529532
else:
530-
# Will raise a SpecialFileError for unsupported file types
531533
copy_function(srcobj, dstname)
532-
# catch the Error from the recursive copytree so that we can
533-
# continue with other files
534+
except ErrorGroup as err_group:
535+
errors.extend(err_group.args[0])
534536
except Error as err:
535-
errors.extend(err.args[0])
537+
errors.append((srcname, dstname, str(err)))
536538
except OSError as why:
537539
errors.append((srcname, dstname, str(why)))
538540
try:
@@ -542,7 +544,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
542544
if getattr(why, 'winerror', None) is None:
543545
errors.append((src, dst, str(why)))
544546
if errors:
545-
raise Error(errors)
547+
raise ErrorGroup(errors)
546548
return dst
547549

548550
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
@@ -1101,6 +1101,39 @@ def test_copytree_subdirectory(self):
11011101
rv = shutil.copytree(src_dir, dst_dir)
11021102
self.assertEqual(['pol'], os.listdir(rv))
11031103

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

11061139
### shutil.copymode
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Make exception from :func:`shutil.copytree` readable when a
2+
:exc:`shutil.SameFileError` is raised, by raising an explicit
3+
exceptions for groups of exceptions.

0 commit comments

Comments
 (0)