Skip to content

Commit 533be29

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 8eebe4e commit 533be29

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
@@ -67,6 +67,9 @@
6767
class Error(OSError):
6868
pass
6969

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

@@ -527,12 +530,9 @@ 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 Error as err:
535-
errors.extend(err.args[0])
534+
except ErrorGroup as err_group:
535+
errors.extend(err_group.args[0])
536536
except OSError as why:
537537
errors.append((srcname, dstname, str(why)))
538538
try:
@@ -542,7 +542,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
542542
if getattr(why, 'winerror', None) is None:
543543
errors.append((src, dst, str(why)))
544544
if errors:
545-
raise Error(errors)
545+
raise ErrorGroup(errors)
546546
return dst
547547

548548
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: 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)