Issue #19077: tempfile.TemporaryDirectory cleanup is now most likely
successful when called during nulling out of modules during shutdown. Misleading exception no longer raised when resource warning is emitted during shutdown.
This commit is contained in:
parent
b9915973f3
commit
99e033b02e
|
@ -27,11 +27,12 @@ __all__ = [
|
||||||
|
|
||||||
# Imports.
|
# Imports.
|
||||||
|
|
||||||
|
import atexit as _atexit
|
||||||
import functools as _functools
|
import functools as _functools
|
||||||
import warnings as _warnings
|
import warnings as _warnings
|
||||||
import sys as _sys
|
|
||||||
import io as _io
|
import io as _io
|
||||||
import os as _os
|
import os as _os
|
||||||
|
import shutil as _shutil
|
||||||
import errno as _errno
|
import errno as _errno
|
||||||
from random import Random as _Random
|
from random import Random as _Random
|
||||||
|
|
||||||
|
@ -355,10 +356,13 @@ class _TemporaryFileCloser:
|
||||||
underlying file object, without adding a __del__ method to the
|
underlying file object, without adding a __del__ method to the
|
||||||
temporary file."""
|
temporary file."""
|
||||||
|
|
||||||
|
# Set here since __del__ checks it
|
||||||
|
file = None
|
||||||
|
close_called = False
|
||||||
|
|
||||||
def __init__(self, file, name, delete=True):
|
def __init__(self, file, name, delete=True):
|
||||||
self.file = file
|
self.file = file
|
||||||
self.name = name
|
self.name = name
|
||||||
self.close_called = False
|
|
||||||
self.delete = delete
|
self.delete = delete
|
||||||
|
|
||||||
# NT provides delete-on-close as a primitive, so we don't need
|
# NT provides delete-on-close as a primitive, so we don't need
|
||||||
|
@ -370,14 +374,13 @@ class _TemporaryFileCloser:
|
||||||
# that this must be referenced as self.unlink, because the
|
# that this must be referenced as self.unlink, because the
|
||||||
# name TemporaryFileWrapper may also get None'd out before
|
# name TemporaryFileWrapper may also get None'd out before
|
||||||
# __del__ is called.
|
# __del__ is called.
|
||||||
unlink = _os.unlink
|
|
||||||
|
|
||||||
def close(self):
|
def close(self, unlink=_os.unlink):
|
||||||
if not self.close_called:
|
if not self.close_called and self.file is not None:
|
||||||
self.close_called = True
|
self.close_called = True
|
||||||
self.file.close()
|
self.file.close()
|
||||||
if self.delete:
|
if self.delete:
|
||||||
self.unlink(self.name)
|
unlink(self.name)
|
||||||
|
|
||||||
# Need to ensure the file is deleted on __del__
|
# Need to ensure the file is deleted on __del__
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
@ -677,9 +680,11 @@ class TemporaryDirectory(object):
|
||||||
in it are removed.
|
in it are removed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Handle mkdtemp raising an exception
|
||||||
|
name = None
|
||||||
|
_closed = False
|
||||||
|
|
||||||
def __init__(self, suffix="", prefix=template, dir=None):
|
def __init__(self, suffix="", prefix=template, dir=None):
|
||||||
self._closed = False
|
|
||||||
self.name = None # Handle mkdtemp raising an exception
|
|
||||||
self.name = mkdtemp(suffix, prefix, dir)
|
self.name = mkdtemp(suffix, prefix, dir)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -688,23 +693,24 @@ class TemporaryDirectory(object):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def cleanup(self, _warn=False):
|
def cleanup(self, _warn=False, _warnings=_warnings):
|
||||||
if self.name and not self._closed:
|
if self.name and not self._closed:
|
||||||
try:
|
try:
|
||||||
self._rmtree(self.name)
|
_shutil.rmtree(self.name)
|
||||||
except (TypeError, AttributeError) as ex:
|
except (TypeError, AttributeError) as ex:
|
||||||
# Issue #10188: Emit a warning on stderr
|
if "None" not in '%s' % (ex,):
|
||||||
# if the directory could not be cleaned
|
|
||||||
# up due to missing globals
|
|
||||||
if "None" not in str(ex):
|
|
||||||
raise
|
raise
|
||||||
print("ERROR: {!r} while cleaning up {!r}".format(ex, self,),
|
self._rmtree(self.name)
|
||||||
file=_sys.stderr)
|
|
||||||
return
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
if _warn:
|
if _warn and _warnings.warn:
|
||||||
self._warn("Implicitly cleaning up {!r}".format(self),
|
try:
|
||||||
ResourceWarning)
|
_warnings.warn("Implicitly cleaning up {!r}".format(self),
|
||||||
|
ResourceWarning)
|
||||||
|
except:
|
||||||
|
if _is_running:
|
||||||
|
raise
|
||||||
|
# Don't raise an exception if modules needed for emitting
|
||||||
|
# a warning are already cleaned in shutdown process.
|
||||||
|
|
||||||
def __exit__(self, exc, value, tb):
|
def __exit__(self, exc, value, tb):
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
@ -713,36 +719,27 @@ class TemporaryDirectory(object):
|
||||||
# Issue a ResourceWarning if implicit cleanup needed
|
# Issue a ResourceWarning if implicit cleanup needed
|
||||||
self.cleanup(_warn=True)
|
self.cleanup(_warn=True)
|
||||||
|
|
||||||
# XXX (ncoghlan): The following code attempts to make
|
def _rmtree(self, path, _OSError=OSError, _sep=_os.path.sep,
|
||||||
# this class tolerant of the module nulling out process
|
_listdir=_os.listdir, _remove=_os.remove, _rmdir=_os.rmdir):
|
||||||
# that happens during CPython interpreter shutdown
|
|
||||||
# Alas, it doesn't actually manage it. See issue #10188
|
|
||||||
_listdir = staticmethod(_os.listdir)
|
|
||||||
_path_join = staticmethod(_os.path.join)
|
|
||||||
_isdir = staticmethod(_os.path.isdir)
|
|
||||||
_islink = staticmethod(_os.path.islink)
|
|
||||||
_remove = staticmethod(_os.remove)
|
|
||||||
_rmdir = staticmethod(_os.rmdir)
|
|
||||||
_os_error = OSError
|
|
||||||
_warn = _warnings.warn
|
|
||||||
|
|
||||||
def _rmtree(self, path):
|
|
||||||
# Essentially a stripped down version of shutil.rmtree. We can't
|
# Essentially a stripped down version of shutil.rmtree. We can't
|
||||||
# use globals because they may be None'ed out at shutdown.
|
# use globals because they may be None'ed out at shutdown.
|
||||||
for name in self._listdir(path):
|
if not isinstance(path, str):
|
||||||
fullname = self._path_join(path, name)
|
_sep = _sep.encode()
|
||||||
try:
|
|
||||||
isdir = self._isdir(fullname) and not self._islink(fullname)
|
|
||||||
except self._os_error:
|
|
||||||
isdir = False
|
|
||||||
if isdir:
|
|
||||||
self._rmtree(fullname)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self._remove(fullname)
|
|
||||||
except self._os_error:
|
|
||||||
pass
|
|
||||||
try:
|
try:
|
||||||
self._rmdir(path)
|
for name in _listdir(path):
|
||||||
except self._os_error:
|
fullname = path + _sep + name
|
||||||
|
try:
|
||||||
|
_remove(fullname)
|
||||||
|
except _OSError:
|
||||||
|
self._rmtree(fullname)
|
||||||
|
_rmdir(path)
|
||||||
|
except _OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
_is_running = True
|
||||||
|
|
||||||
|
def _on_shutdown():
|
||||||
|
global _is_running
|
||||||
|
_is_running = False
|
||||||
|
|
||||||
|
_atexit.register(_on_shutdown)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import contextlib
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from test import support
|
from test import support, script_helper
|
||||||
|
|
||||||
|
|
||||||
if hasattr(os, 'stat'):
|
if hasattr(os, 'stat'):
|
||||||
|
@ -1073,7 +1073,8 @@ class TestTemporaryDirectory(BaseTestCase):
|
||||||
self.nameCheck(tmp.name, dir, pre, suf)
|
self.nameCheck(tmp.name, dir, pre, suf)
|
||||||
# Create a subdirectory and some files
|
# Create a subdirectory and some files
|
||||||
if recurse:
|
if recurse:
|
||||||
self.do_create(tmp.name, pre, suf, recurse-1)
|
d1 = self.do_create(tmp.name, pre, suf, recurse-1)
|
||||||
|
d1.name = None
|
||||||
with open(os.path.join(tmp.name, "test.txt"), "wb") as f:
|
with open(os.path.join(tmp.name, "test.txt"), "wb") as f:
|
||||||
f.write(b"Hello world!")
|
f.write(b"Hello world!")
|
||||||
return tmp
|
return tmp
|
||||||
|
@ -1105,7 +1106,7 @@ class TestTemporaryDirectory(BaseTestCase):
|
||||||
def test_cleanup_with_symlink_to_a_directory(self):
|
def test_cleanup_with_symlink_to_a_directory(self):
|
||||||
# cleanup() should not follow symlinks to directories (issue #12464)
|
# cleanup() should not follow symlinks to directories (issue #12464)
|
||||||
d1 = self.do_create()
|
d1 = self.do_create()
|
||||||
d2 = self.do_create()
|
d2 = self.do_create(recurse=0)
|
||||||
|
|
||||||
# Symlink d1/foo -> d2
|
# Symlink d1/foo -> d2
|
||||||
os.symlink(d2.name, os.path.join(d1.name, "foo"))
|
os.symlink(d2.name, os.path.join(d1.name, "foo"))
|
||||||
|
@ -1135,60 +1136,49 @@ class TestTemporaryDirectory(BaseTestCase):
|
||||||
finally:
|
finally:
|
||||||
os.rmdir(dir)
|
os.rmdir(dir)
|
||||||
|
|
||||||
@unittest.expectedFailure # See issue #10188
|
|
||||||
def test_del_on_shutdown(self):
|
def test_del_on_shutdown(self):
|
||||||
# A TemporaryDirectory may be cleaned up during shutdown
|
# A TemporaryDirectory may be cleaned up during shutdown
|
||||||
# Make sure it works with the relevant modules nulled out
|
|
||||||
with self.do_create() as dir:
|
with self.do_create() as dir:
|
||||||
d = self.do_create(dir=dir)
|
for mod in ('os', 'shutil', 'sys', 'tempfile', 'warnings'):
|
||||||
# Mimic the nulling out of modules that
|
code = """if True:
|
||||||
# occurs during system shutdown
|
import os
|
||||||
modules = [os, os.path]
|
import shutil
|
||||||
if has_stat:
|
import sys
|
||||||
modules.append(stat)
|
import tempfile
|
||||||
# Currently broken, so suppress the warning
|
import warnings
|
||||||
# that is otherwise emitted on stdout
|
|
||||||
with support.captured_stderr() as err:
|
tmp = tempfile.TemporaryDirectory(dir={dir!r})
|
||||||
with NulledModules(*modules):
|
sys.stdout.buffer.write(tmp.name.encode())
|
||||||
d.cleanup()
|
|
||||||
# Currently broken, so stop spurious exception by
|
tmp2 = os.path.join(tmp.name, 'test_dir')
|
||||||
# indicating the object has already been closed
|
os.mkdir(tmp2)
|
||||||
d._closed = True
|
with open(os.path.join(tmp2, "test.txt"), "w") as f:
|
||||||
# And this assert will fail, as expected by the
|
f.write("Hello world!")
|
||||||
# unittest decorator...
|
|
||||||
self.assertFalse(os.path.exists(d.name),
|
{mod}.tmp = tmp
|
||||||
"TemporaryDirectory %s exists after cleanup" % d.name)
|
|
||||||
|
warnings.filterwarnings("always", category=ResourceWarning)
|
||||||
|
""".format(dir=dir, mod=mod)
|
||||||
|
rc, out, err = script_helper.assert_python_ok("-c", code)
|
||||||
|
tmp_name = out.decode().strip()
|
||||||
|
self.assertFalse(os.path.exists(tmp_name),
|
||||||
|
"TemporaryDirectory %s exists after cleanup" % tmp_name)
|
||||||
|
err = err.decode('utf-8', 'backslashreplace')
|
||||||
|
self.assertNotIn("Exception ", err)
|
||||||
|
|
||||||
def test_warnings_on_cleanup(self):
|
def test_warnings_on_cleanup(self):
|
||||||
# Two kinds of warning on shutdown
|
# ResourceWarning will be triggered by __del__
|
||||||
# Issue 10888: may write to stderr if modules are nulled out
|
|
||||||
# ResourceWarning will be triggered by __del__
|
|
||||||
with self.do_create() as dir:
|
with self.do_create() as dir:
|
||||||
if os.sep != '\\':
|
d = self.do_create(dir=dir, recurse=3)
|
||||||
# Embed a backslash in order to make sure string escaping
|
name = d.name
|
||||||
# in the displayed error message is dealt with correctly
|
|
||||||
suffix = '\\check_backslash_handling'
|
|
||||||
else:
|
|
||||||
suffix = ''
|
|
||||||
d = self.do_create(dir=dir, suf=suffix)
|
|
||||||
|
|
||||||
#Check for the Issue 10888 message
|
|
||||||
modules = [os, os.path]
|
|
||||||
if has_stat:
|
|
||||||
modules.append(stat)
|
|
||||||
with support.captured_stderr() as err:
|
|
||||||
with NulledModules(*modules):
|
|
||||||
d.cleanup()
|
|
||||||
message = err.getvalue().replace('\\\\', '\\')
|
|
||||||
self.assertIn("while cleaning up", message)
|
|
||||||
self.assertIn(d.name, message)
|
|
||||||
|
|
||||||
# Check for the resource warning
|
# Check for the resource warning
|
||||||
with support.check_warnings(('Implicitly', ResourceWarning), quiet=False):
|
with support.check_warnings(('Implicitly', ResourceWarning), quiet=False):
|
||||||
warnings.filterwarnings("always", category=ResourceWarning)
|
warnings.filterwarnings("always", category=ResourceWarning)
|
||||||
d.__del__()
|
del d
|
||||||
self.assertFalse(os.path.exists(d.name),
|
support.gc_collect()
|
||||||
"TemporaryDirectory %s exists after __del__" % d.name)
|
self.assertFalse(os.path.exists(name),
|
||||||
|
"TemporaryDirectory %s exists after __del__" % name)
|
||||||
|
|
||||||
def test_multiple_close(self):
|
def test_multiple_close(self):
|
||||||
# Can be cleaned-up many times without error
|
# Can be cleaned-up many times without error
|
||||||
|
|
|
@ -50,6 +50,11 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #19077: tempfile.TemporaryDirectory cleanup is now most likely
|
||||||
|
successful when called during nulling out of modules during shutdown.
|
||||||
|
Misleading exception no longer raised when resource warning is emitted
|
||||||
|
during shutdown.
|
||||||
|
|
||||||
- Issue #20367: Fix behavior of concurrent.futures.as_completed() for
|
- Issue #20367: Fix behavior of concurrent.futures.as_completed() for
|
||||||
duplicate arguments. Patch by Glenn Langford.
|
duplicate arguments. Patch by Glenn Langford.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue