gh-62948: IOBase finalizer logs close() errors (#105104)

This commit is contained in:
Victor Stinner 2023-05-31 13:41:19 +02:00 committed by GitHub
parent 85e5d03163
commit 58a2e09816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 19 additions and 40 deletions

View File

@ -87,6 +87,15 @@ New Modules
Improved Modules Improved Modules
================ ================
io
--
The :class:`io.IOBase` finalizer now logs the ``close()`` method errors with
:data:`sys.unraisablehook`. Previously, errors were ignored silently by default,
and only logged in :ref:`Python Development Mode <devmode>` or on :ref:`Python
built on debug mode <debug-build>`.
(Contributed by Victor Stinner in :gh:`62948`.)
pathlib pathlib
------- -------

View File

@ -33,11 +33,8 @@ DEFAULT_BUFFER_SIZE = 8 * 1024 # bytes
# Rebind for compatibility # Rebind for compatibility
BlockingIOError = BlockingIOError BlockingIOError = BlockingIOError
# Does io.IOBase finalizer log the exception if the close() method fails?
# The exception is ignored silently by default in release build.
_IOBASE_EMITS_UNRAISABLE = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
# Does open() check its 'errors' argument? # Does open() check its 'errors' argument?
_CHECK_ERRORS = _IOBASE_EMITS_UNRAISABLE _CHECK_ERRORS = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
def text_encoding(encoding, stacklevel=2): def text_encoding(encoding, stacklevel=2):
@ -416,18 +413,9 @@ class IOBase(metaclass=abc.ABCMeta):
if closed: if closed:
return return
if _IOBASE_EMITS_UNRAISABLE: # If close() fails, the caller logs the exception with
self.close() # sys.unraisablehook. close() must be called at the end at __del__().
else: self.close()
# The try/except block is in case this is called at program
# exit time, when it's possible that globals have already been
# deleted, and then the close() call might fail. Since
# there's nothing we can do about such failures and they annoy
# the end users, we suppress the traceback.
try:
self.close()
except:
pass
### Inquiries ### ### Inquiries ###

View File

@ -66,10 +66,6 @@ else:
class EmptyStruct(ctypes.Structure): class EmptyStruct(ctypes.Structure):
pass pass
# Does io.IOBase finalizer log the exception if the close() method fails?
# The exception is ignored silently by default in release build.
IOBASE_EMITS_UNRAISABLE = (support.Py_DEBUG or sys.flags.dev_mode)
def _default_chunk_size(): def _default_chunk_size():
"""Get the default TextIOWrapper chunk size""" """Get the default TextIOWrapper chunk size"""
@ -1218,10 +1214,7 @@ class CommonBufferedTests:
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
self.tp(rawio).xyzzy self.tp(rawio).xyzzy
if not IOBASE_EMITS_UNRAISABLE: self.assertEqual(cm.unraisable.exc_type, OSError)
self.assertIsNone(cm.unraisable)
elif cm.unraisable is not None:
self.assertEqual(cm.unraisable.exc_type, OSError)
def test_repr(self): def test_repr(self):
raw = self.MockRawIO() raw = self.MockRawIO()
@ -3022,10 +3015,7 @@ class TextIOWrapperTest(unittest.TestCase):
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
self.TextIOWrapper(rawio, encoding="utf-8").xyzzy self.TextIOWrapper(rawio, encoding="utf-8").xyzzy
if not IOBASE_EMITS_UNRAISABLE: self.assertEqual(cm.unraisable.exc_type, OSError)
self.assertIsNone(cm.unraisable)
elif cm.unraisable is not None:
self.assertEqual(cm.unraisable.exc_type, OSError)
# Systematic tests of the text I/O API # Systematic tests of the text I/O API

View File

@ -0,0 +1,4 @@
The :class:`io.IOBase` finalizer now logs the ``close()`` method errors with
:data:`sys.unraisablehook`. Previously, errors were ignored silently by default,
and only logged in :ref:`Python Development Mode <devmode>` or on
:ref:`Python built on debug mode <debug-build>`. Patch by Victor Stinner.

View File

@ -319,20 +319,8 @@ iobase_finalize(PyObject *self)
if (PyObject_SetAttr(self, &_Py_ID(_finalizing), Py_True)) if (PyObject_SetAttr(self, &_Py_ID(_finalizing), Py_True))
PyErr_Clear(); PyErr_Clear();
res = PyObject_CallMethodNoArgs((PyObject *)self, &_Py_ID(close)); res = PyObject_CallMethodNoArgs((PyObject *)self, &_Py_ID(close));
/* Silencing I/O errors is bad, but printing spurious tracebacks is
equally as bad, and potentially more frequent (because of
shutdown issues). */
if (res == NULL) { if (res == NULL) {
#ifndef Py_DEBUG
if (_Py_GetConfig()->dev_mode) {
PyErr_WriteUnraisable(self);
}
else {
PyErr_Clear();
}
#else
PyErr_WriteUnraisable(self); PyErr_WriteUnraisable(self);
#endif
} }
else { else {
Py_DECREF(res); Py_DECREF(res);