8000 bpo-29982: Add "ignore_cleanup_errors" param to tempfile.TemporaryDir… · python/cpython@bd2fa3c · GitHub
[go: up one dir, main page]

Skip to content

Commit bd2fa3c

Browse files
authored
bpo-29982: Add "ignore_cleanup_errors" param to tempfile.TemporaryDirectory() (GH-24793)
1 parent d48848c commit bd2fa3c

File tree

4 files changed

+117
-15
lines changed

4 files changed

+117
-15
lines changed

Doc/library/tempfile.rst

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,12 @@ The module defines the following user-callable items:
118118
Added *errors* parameter.
119119

120120

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

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

129129
The directory name can be retrieved from the :attr:`name` attribute of the
@@ -132,12 +132,21 @@ The module defines the following user-callable items:
132132
the :keyword:`with` statement, if there is one.
133133

134134
The directory can be explicitly cleaned up by calling the
135-
:func:`cleanup` method.
135+
:func:`cleanup` method. If *ignore_cleanup_errors* is true, any unhandled
136+
exceptions during explicit or implicit cleanup (such as a
137+
:exc:`PermissionError` removing open files on Windows) will be ignored,
138+
and the remaining removable items deleted on a "best-effort" basis.
139+
Otherwise, errors will be raised in whatever context cleanup occurs
140+
(the :func:`cleanup` call, exiting the context manager, when the object
141+
is garbage-collected or during interpreter shutdown).
136142

137143
.. audit-event:: tempfile.mkdtemp fullpath tempfile.TemporaryDirectory
138144

139145
.. versionadded:: 3.2
140146

147+
.. versionchanged:: 3.10
148+
Added *ignore_cleanup_errors* parameter.
149+
141150

142151
.. function:: mkstemp(suffix=None, prefix=None, dir=None, text=False)
143152

Lib/tempfile.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ def writelines(self, iterable):
768768
return rv
769769

770770

771-
class TemporaryDirectory(object):
771+
class TemporaryDirectory:
772772
"""Create and return a temporary directory. This has the same
773773
behavior as mkdtemp but can be used as a context manager. For
774774
example:
@@ -780,14 +780,17 @@ class TemporaryDirectory(object):
780780
in it are removed.
781781
"""
782782

783-
def __init__(self, suffix=None, prefix=None, dir=None):
783+
def __init__(self, suffix=None, prefix=None, dir=None,
784+
ignore_cleanup_errors=False):
784785
self.name = mkdtemp(suffix, prefix, dir)
786+
self._ignore_cleanup_errors = ignore_cleanup_errors
785787
self._finalizer = _weakref.finalize(
786788
self, self._cleanup, self.name,
787-
warn_message="Implicitly cleaning up {!r}".format(self))
789+
warn_message="Implicitly cleaning up {!r}".format(self),
790+
ignore_errors=self._ignore_cleanup_errors)
788791

789792
@classmethod
790-
def _rmtree(cls, name):
793+
def _rmtree(cls, name, ignore_errors=False):
791794
def onerror(func, path, exc_info):
792795
if issubclass(exc_info[0], PermissionError):
793796
def resetperms(path):
@@ -806,19 +809,20 @@ def resetperms(path):
806809
_os.unlink(path)
807810
# PermissionError is raised on FreeBSD for directories
808811
except (IsADirectoryError, PermissionError):
809-
cls._rmtree(path)
812+
cls._rmtree(path, ignore_errors=ignore_errors)
810813
except FileNotFoundError:
811814
pass
812815
elif issubclass(exc_info[0], FileNotFoundError):
813816
pass
814817
else:
815-
raise
818+
if not ignore_errors:
819+
raise
816820

817821
_shutil.rmtree(name, onerror=onerror)
818822

819823
@classmethod
820-
def _cleanup(cls, name, warn_message):
821-
cls._rmtree(name)
824+
def _cleanup(cls, name, warn_message, ignore_errors=False):
825+
cls._rmtree(name, ignore_errors=ignore_errors)
822826
_warnings.warn(warn_message, ResourceWarning)
823827

824828
def __repr__(self):
@@ -831,7 +835,7 @@ def __exit__(self, exc, value, tb):
831835
self.cleanup()
832836

833837
def cleanup(self):
834-
if self._finalizer.detach():
835-
self._rmtree(self.name)
838+
if self._finalizer.detach() or _os.path.exists(self.name):
839+
self._rmtree(self.name, ignore_errors=self._ignore_cleanup_errors)
836840

837841
__class_getitem__ = classmethod(_types.GenericAlias)

Lib/test/test_tempfile.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,13 +1365,17 @@ def __exit__(self, *exc_info):
13651365
d.clear()
13661366
d.update(c)
13671367

1368+
13681369
class TestTemporaryDirectory(BaseTestCase):
13691370
"""Test TemporaryDirectory()."""
13701371

1371-
def do_create(self, dir=None, pre="", suf="", recurse=1, dirs=1, files=1):
1372+
def do_create(self, dir=None, pre="", suf="", recurse=1, dirs=1, files=1,
1373+
ignore_cleanup_errors=False):
13721374
if dir is None:
13731375
dir = tempfile.gettempdir()
1374-
tmp = tempfile.TemporaryDirectory(dir=dir, prefix=pre, suffix=suf)
1376+
tmp = tempfile.TemporaryDirectory(
1377+
dir=dir, prefix=pre, suffix=suf,
1378+
ignore_cleanup_errors=ignore_cleanup_errors)
13751379
self.nameCheck(tmp.name, dir, pre, suf)
13761380
self.do_create2(tmp.name, recurse, dirs, files)
13771381
return tmp
@@ -1410,6 +1414,30 @@ def test_explicit_cleanup(self):
14101414
finally:
14111415
os.rmdir(dir)
14121416

1417+
def test_explict_cleanup_ignore_errors(self):
1418+
"""Test that cleanup doesn't return an error when ignoring them."""
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+
temp_dir.cleanup()
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+
temp_dir.cleanup()
1437+
self.assertFalse(
1438+
temp_path.exists(),
1439+
f"TemporaryDirectory {temp_path!s} exists after cleanup")
1440+
14131441
@os_helper.skip_unless_symlink
14141442
def test_cleanup_with_symlink_to_a_directory(self):
14151443
# cleanup() should not follow symlinks to directories (issue #12464)
@@ -1444,6 +1472,27 @@ def test_del_on_collection(self):
14441472
finally:
14451473
os.rmdir(dir)
14461474

1475+
@support.cpython_only
1476+
def test_del_on_collection_ignore_errors(self):
1477+
"""Test that ignoring errors works when TemporaryDirectory is gced."""
1478+
with tempfile.TemporaryDirectory() as working_dir:
1479+
temp_dir = self.do_create(
1480+
dir=working_dir, ignore_cleanup_errors=True)
1481+
temp_path = pathlib.Path(temp_dir.name)
1482+
self.assertTrue(temp_path.exists(),
1483+
f"TemporaryDirectory {temp_path!s} does not exist")
1484+
with open(temp_path / "a_file.txt", "w+t") as open_file:
1485+
open_file.write("Hello world!\n")
1486+
del temp_dir
1487+
self.assertEqual(len(list(temp_path.glob("*"))),
1488+
int(sys.platform.startswith("win")),
1489+
"Unexpected number of files in "
1490+
f"TemporaryDirectory {temp_path!s}")
1491+
self.assertEqual(
1492+
temp_path.exists(),
1493+
sys.platform.startswith("win"),
1494+
f"TemporaryDirectory {temp_path!s} existance state unexpected")
1495+
14471496
def test_del_on_shutdown(self):
14481497
# A TemporaryDirectory may be cleaned up during shutdown
14491498
with self.do_create() as dir:
@@ -1476,6 +1525,43 @@ def test_del_on_shutdown(self):
14761525
self.assertNotIn("Exception ", err)
14771526
self.assertIn("ResourceWarning: Implicitly cleaning up", err)
14781527

1528+
def test_del_on_shutdown_ignore_errors(self):
1529+
"""Test ignoring errors works when a tempdir is gc'ed on shutdown."""
1530+
with tempfile.TemporaryDirectory() as working_dir:
1531+
code = """if True:
1532+
import pathlib
1533+
import sys
1534+
import tempfile
1535+
import warnings
1536+
1537+
temp_dir = tempfile.TemporaryDirectory(
1538+
dir={working_dir!r}, ignore_cleanup_errors=True)
1539+
sys.stdout.buffer.write(temp_dir.name.encode())
1540+
1541+
temp_dir_2 = pathlib.Path(temp_dir.name) / "test_dir"
1542+
temp_dir_2.mkdir()
1543+
with open(temp_dir_2 / "test0.txt", "w") as test_file:
1544+
test_file.write("Hello world!")
1545+
open_file = open(temp_dir_2 / "open_file.txt", "w")
1546+
open_file.write("Hello world!")
1547+
1548+
warnings.filterwarnings("always", category=ResourceWarning)
1549+
""".format(working_dir=working_dir)
1550+
__, out, err = script_helper.assert_python_ok("-c", code)
1551+
temp_path = pathlib.Path(out.decode().strip())
1552+
self.assertEqual(len(list(temp_path.glob("*"))),
1553+
int(sys.platform.startswith("win")),
1554+
"Unexpected number of files in "
1555+
f"TemporaryDirectory {temp_path!s}")
1556+
self.assertEqual(
1557+
temp_path.exists(),
1558+
sys.platform.startswith("win"),
1559+
f"TemporaryDirectory {temp_path!s} existance state unexpected")
1560+
err = err.decode('utf-8', 'backslashreplace')
1561+
self.assertNotIn("Exception", err)
1562+
self.assertNotIn("Error", err)
1563+
self.assertIn("ResourceWarning: Implicitly cleaning up", err)
1564+
14791565
def test_exit_on_shutdown(self):
14801566
# Issue #22427
14811567
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)
0