diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 766cc63a98a1c0d..bb0a035a4b96fcc 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -863,7 +863,7 @@ def __init__(self, suffix=None, prefix=None, dir=None, ignore_errors=self._ignore_cleanup_errors) @classmethod - def _rmtree(cls, name, ignore_errors=False): + def _rmtree(cls, name, ignore_errors=False, retrying=None): def without_following_symlinks(func, path, *args): # Pass follow_symlinks=False, unless not supported on this platform. if func in _os.supports_follow_symlinks: @@ -889,7 +889,9 @@ def onerror(func, path, exc_info): _os.unlink(path) # PermissionError is raised on FreeBSD for directories except (IsADirectoryError, PermissionError): - cls._rmtree(path, ignore_errors=ignore_errors) + if path == retrying: + raise # already tried (and failed) to fix this once + cls._rmtree(path, ignore_errors=ignore_errors, retrying=path) except FileNotFoundError: pass elif issubclass(exc_info[0], FileNotFoundError): diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index c022e2e0f8d1144..ab42e720964895a 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1691,6 +1691,30 @@ def patched_unlink(path, **kwargs): "Mode of the directory pointed to by a symlink changed") d2.cleanup() + def test_cleanup_with_error_deleting_directory(self): + # cleanup() should not recurse infinitely on PermissionError when deleting directory + d = self.do_create() + + # There are a variety of reasons why the OS may raise a PermissionError, + # but provoking those reliably and cross-platform is not straightforward, + # so raise the error synthetically instead. + real_rmdir = os.rmdir + error_was_raised = False + def patched_rmdir(path, **kwargs): + nonlocal error_was_raised + # rmdir may be called with full path or path relative to 'fd' kwarg. + if path.endswith("dir0"): + error_was_raised = True + raise PermissionError() + real_rmdir(path, **kwargs) + + with mock.patch("tempfile._os.rmdir", patched_rmdir): + # This call to cleanup() should fail with PermissionError, not recurse infinitely. + self.assertRaises(PermissionError, d.cleanup) + + self.assertTrue(error_was_raised, "did not see expected 'rmdir' call") + d.cleanup() + @support.cpython_only def test_del_on_collection(self): # A TemporaryDirectory is deleted when garbage collected