Skip to content

Commit 93ea2dd

Browse files
CAM-Gerlachicanhasmath
authored andcommitted
bpo-29982: Add "ignore_cleanup_errors" param to tempfile.TemporaryDirectory() (pythonGH-24793)
1 parent cbb69e4 commit 93ea2dd

File tree

4 files changed

+147
-13
lines changed

4 files changed

+147
-13
lines changed

Doc/library/tempfile.rst

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,12 @@ The module defines the following user-callable items:
105105
the truncate method now accepts a ``size`` argument.
106106

107107

108-
.. function:: TemporaryDirectory(suffix=None, prefix=None, dir=None)
108+
.. function:: TemporaryDirectory(suffix=None, prefix=None, dir=None, ignore_cleanup_errors=False)
109109

110110
This function securely creates a temporary directory using the same rules as :func:`mkdtemp`.
111111
The resulting object can be used as a context manager (see
112112
:ref:`tempfile-examples`). On completion of the context or destruction
113-
of the temporary directory object the newly created temporary directory
113+
of the temporary directory object, the newly created temporary directory
114114
and all its contents are removed from the filesystem.
115115

116116
The directory name can be retrieved from the :attr:`name` attribute of the
@@ -119,10 +119,19 @@ The module defines the following user-callable items:
119119
the :keyword:`with` statement, if there is one.
120120

121121
The directory can be explicitly cleaned up by calling the
122-
:func:`cleanup` method.
122+
:func:`cleanup` method. If *ignore_cleanup_errors* is true, any unhandled
123+
exceptions during explicit or implicit cleanup (such as a
124+
:exc:`PermissionError` removing open files on Windows) will be ignored,
125+
and the remaining removable items deleted on a "best-effort" basis.
126+
Otherwise, errors will be raised in whatever context cleanup occurs
127+
(the :func:`cleanup` call, exiting the context manager, when the object
128+
is garbage-collected or during interpreter shutdown).
123129

124130
.. versionadded:: 3.2
125131

132+
.. versionchanged:: 3.10
133+
Added *ignore_cleanup_errors* parameter.
134+
126135

127136
.. function:: mkstemp(suffix=None, prefix=None, dir=None, text=False)
128137

Lib/tempfile.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ def writelines(self, iterable):
774774
return rv
775775

776776

777-
class TemporaryDirectory(object):
777+
class TemporaryDirectory:
778778
"""Create and return a temporary directory. This has the same
779779
behavior as mkdtemp but can be used as a context manager. For
780780
example:
@@ -786,15 +786,49 @@ class TemporaryDirectory(object):
786786
in it are removed.
787787
"""
788788

789-
def __init__(self, suffix=None, prefix=None, dir=None):
789+
def __init__(self, suffix=None, prefix=None, dir=None,
790+
ignore_cleanup_errors=False):
790791
self.name = mkdtemp(suffix, prefix, dir)
792+
self._ignore_cleanup_errors = ignore_cleanup_errors
791793
self._finalizer = _weakref.finalize(
792794
self, self._cleanup, self.name,
793-
warn_message="Implicitly cleaning up {!r}".format(self))
795+
warn_message="Implicitly cleaning up {!r}".format(self),
796+
ignore_errors=self._ignore_cleanup_errors)
794797

795798
@classmethod
796-
def _cleanup(cls, name, warn_message):
797-
_shutil.rmtree(name)
799+
def _rmtree(cls, name, ignore_errors=False):
800+
def onerror(func, path, exc_info):
801+
if issubclass(exc_info[0], PermissionError):
802+
def resetperms(path):
803+
try:
804+
_os.chflags(path, 0)
805+
except AttributeError:
806+
pass
807+
_os.chmod(path, 0o700)
808+
809+
try:
810+
if path != name:
811+
resetperms(_os.path.dirname(path))
812+
resetperms(path)
813+
814+
try:
815+
_os.unlink(path)
816+
# PermissionError is raised on FreeBSD for directories
817+
except (IsADirectoryError, PermissionError):
818+
cls._rmtree(path, ignore_errors=ignore_errors)
819+
except FileNotFoundError:
820+
pass
821+
elif issubclass(exc_info[0], FileNotFoundError):
822+
pass
823+
else:
824+
if not ignore_errors:
825+
raise
826+
827+
_shutil.rmtree(name, onerror=onerror)
828+
829+
@classmethod
830+
def _cleanup(cls, name, warn_message, ignore_errors=False):
831+
cls._rmtree(name, ignore_errors=ignore_errors)
798832
_warnings.warn(warn_message, ResourceWarning)
799833

800834
def __repr__(self):
@@ -807,5 +841,7 @@ def __exit__(self, exc, value, tb):
807841
self.cleanup()
808842

809843
def cleanup(self):
810-
if self._finalizer.detach():
811-
_shutil.rmtree(self.name)
844+
if self._finalizer.detach() or _os.path.exists(self.name):
845+
self._rmtree(self.name, ignore_errors=self._ignore_cleanup_errors)
846+
847+
__class_getitem__ = classmethod(_types.GenericAlias)

Lib/test/test_tempfile.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,13 +1312,17 @@ def __exit__(self, *exc_info):
13121312
d.clear()
13131313
d.update(c)
13141314

1315+
13151316
class TestTemporaryDirectory(BaseTestCase):
13161317
"""Test TemporaryDirectory()."""
13171318

1318-
def do_create(self, dir=None, pre="", suf="", recurse=1):
1319+
def do_create(self, dir=None, pre="", suf="", recurse=1, dirs=1, files=1,
1320+
ignore_cleanup_errors=False):
13191321
if dir is None:
13201322
dir = tempfile.gettempdir()
1321-
tmp = tempfile.TemporaryDirectory(dir=dir, prefix=pre, suffix=suf)
1323+
tmp = tempfile.TemporaryDirectory(
1324+
dir=dir, prefix=pre, suffix=suf,
1325+
ignore_cleanup_errors=ignore_cleanup_errors)
13221326
self.nameCheck(tmp.name, dir, pre, suf)
13231327
# Create a subdirectory and some files
13241328
if recurse:
@@ -1351,7 +1355,31 @@ def test_explicit_cleanup(self):
13511355
finally:
13521356
os.rmdir(dir)
13531357

1354-
@support.skip_unless_symlink
1358+
def test_explict_cleanup_ignore_errors(self):
1359+
"""Test that cleanup doesn't return an error when ignoring them."""
1360+
with tempfile.TemporaryDirectory() as working_dir:
1361+
temp_dir = self.do_create(
1362+
dir=working_dir, ignore_cleanup_errors=True)
1363+
temp_path = pathlib.Path(temp_dir.name)
1364+
self.assertTrue(temp_path.exists(),
1365+
f"TemporaryDirectory {temp_path!s} does not exist")
1366+
with open(temp_path / "a_file.txt", "w+t") as open_file:
1367+
open_file.write("Hello world!\n")
1368+
temp_dir.cleanup()
1369+
self.assertEqual(len(list(temp_path.glob("*"))),
1370+
int(sys.platform.startswith("win")),
1371+
"Unexpected number of files in "
1372+
f"TemporaryDirectory {temp_path!s}")
1373+
self.assertEqual(
1374+
temp_path.exists(),
1375+
sys.platform.startswith("win"),
1376+
f"TemporaryDirectory {temp_path!s} existance state unexpected")
1377+
temp_dir.cleanup()
1378+
self.assertFalse(
1379+
temp_path.exists(),
1380+
f"TemporaryDirectory {temp_path!s} exists after cleanup")
1381+
1382+
@os_helper.skip_unless_symlink
13551383
def test_cleanup_with_symlink_to_a_directory(self):
13561384
# cleanup() should not follow symlinks to directories (issue #12464)
13571385
d1 = self.do_create()
@@ -1385,6 +1413,27 @@ def test_del_on_collection(self):
13851413
finally:
13861414
os.rmdir(dir)
13871415

1416+
@support.cpython_only
1417+
def test_del_on_collection_ignore_errors(self):
1418+
"""Test that ignoring errors works when TemporaryDirectory is gced."""
1419+
with tempfile.TemporaryDirectory() as working_dir:
1420+
temp_dir = self.do_create(
1421+
dir=working_dir, ignore_cleanup_errors=True)
1422+
temp_path = pathlib.Path(temp_dir.name)
1423+
self.assertTrue(temp_path.exists(),
1424+
f"TemporaryDirectory {temp_path!s} does not exist")
1425+
with open(temp_path / "a_file.txt", "w+t") as open_file:
1426+
open_file.write("Hello world!\n")
1427+
del temp_dir
1428+
self.assertEqual(len(list(temp_path.glob("*"))),
1429+
int(sys.platform.startswith("win")),
1430+
"Unexpected number of files in "
1431+
f"TemporaryDirectory {temp_path!s}")
1432+
self.assertEqual(
1433+
temp_path.exists(),
1434+
sys.platform.startswith("win"),
1435+
f"TemporaryDirectory {temp_path!s} existance state unexpected")
1436+
13881437
def test_del_on_shutdown(self):
13891438
# A TemporaryDirectory may be cleaned up during shutdown
13901439
with self.do_create() as dir:
@@ -1417,6 +1466,43 @@ def test_del_on_shutdown(self):
14171466
self.assertNotIn("Exception ", err)
14181467
self.assertIn("ResourceWarning: Implicitly cleaning up", err)
14191468

1469+
def test_del_on_shutdown_ignore_errors(self):
1470+
"""Test ignoring errors works when a tempdir is gc'ed on shutdown."""
1471+
with tempfile.TemporaryDirectory() as working_dir:
1472+
code = """if True:
1473+
import pathlib
1474+
import sys
1475+
import tempfile
1476+
import warnings
1477+
1478+
temp_dir = tempfile.TemporaryDirectory(
1479+
dir={working_dir!r}, ignore_cleanup_errors=True)
1480+
sys.stdout.buffer.write(temp_dir.name.encode())
1481+
1482+
temp_dir_2 = pathlib.Path(temp_dir.name) / "test_dir"
1483+
temp_dir_2.mkdir()
1484+
with open(temp_dir_2 / "test0.txt", "w") as test_file:
1485+
test_file.write("Hello world!")
1486+
open_file = open(temp_dir_2 / "open_file.txt", "w")
1487+
open_file.write("Hello world!")
1488+
1489+
warnings.filterwarnings("always", category=ResourceWarning)
1490+
""".format(working_dir=working_dir)
1491+
__, out, err = script_helper.assert_python_ok("-c", code)
1492+
temp_path = pathlib.Path(out.decode().strip())
1493+
self.assertEqual(len(list(temp_path.glob("*"))),
1494+
int(sys.platform.startswith("win")),
1495+
"Unexpected number of files in "
1496+
f"TemporaryDirectory {temp_path!s}")
1497+
self.assertEqual(
1498+
temp_path.exists(),
1499+
sys.platform.startswith("win"),
1500+
f"TemporaryDirectory {temp_path!s} existance state unexpected")
1501+
err = err.decode('utf-8', 'backslashreplace')
1502+
self.assertNotIn("Exception", err)
1503+
self.assertNotIn("Error", err)
1504+
self.assertIn("ResourceWarning: Implicitly cleaning up", err)
1505+
14201506
def test_exit_on_shutdown(self):
14211507
# Issue #22427
14221508
with self.do_create() as dir:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add optional parameter *ignore_cleanup_errors* to
2+
:func:`tempfile.TemporaryDirectory` and allow multiple :func:`cleanup` attempts.
3+
Contributed by C.A.M. Gerlach.

0 commit comments

Comments
 (0)