Skip to content

Commit 58a2e09

Browse files
authored
gh-62948: IOBase finalizer logs close() errors (#105104)
1 parent 85e5d03 commit 58a2e09

File tree

5 files changed

+19
-40
lines changed

5 files changed

+19
-40
lines changed

Doc/whatsnew/3.13.rst

+9
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ New Modules
8787
Improved Modules
8888
================
8989

90+
io
91+
--
92+
93+
The :class:`io.IOBase` finalizer now logs the ``close()`` method errors with
94+
:data:`sys.unraisablehook`. Previously, errors were ignored silently by default,
95+
and only logged in :ref:`Python Development Mode <devmode>` or on :ref:`Python
96+
built on debug mode <debug-build>`.
97+
(Contributed by Victor Stinner in :gh:`62948`.)
98+
9099
pathlib
91100
-------
92101

Lib/_pyio.py

+4-16
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,8 @@
3333
# Rebind for compatibility
3434
BlockingIOError = BlockingIOError
3535

36-
# Does io.IOBase finalizer log the exception if the close() method fails?
37-
# The exception is ignored silently by default in release build.
38-
_IOBASE_EMITS_UNRAISABLE = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
3936
# Does open() check its 'errors' argument?
40-
_CHECK_ERRORS = _IOBASE_EMITS_UNRAISABLE
37+
_CHECK_ERRORS = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
4138

4239

4340
def text_encoding(encoding, stacklevel=2):
@@ -416,18 +413,9 @@ def __del__(self):
416413
if closed:
417414
return
418415

419-
if _IOBASE_EMITS_UNRAISABLE:
420-
self.close()
421-
else:
422-
# The try/except block is in case this is called at program
423-
# exit time, when it's possible that globals have already been
424-
# deleted, and then the close() call might fail. Since
425-
# there's nothing we can do about such failures and they annoy
426-
# the end users, we suppress the traceback.
427-
try:
428-
self.close()
429-
except:
430-
pass
416+
# If close() fails, the caller logs the exception with
417+
# sys.unraisablehook. close() must be called at the end at __del__().
418+
self.close()
431419

432420
### Inquiries ###
433421

Lib/test/test_io.py

+2-12
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ def byteslike(*pos, **kw):
6666
class EmptyStruct(ctypes.Structure):
6767
pass
6868

69-
# Does io.IOBase finalizer log the exception if the close() method fails?
70-
# The exception is ignored silently by default in release build.
71-
IOBASE_EMITS_UNRAISABLE = (support.Py_DEBUG or sys.flags.dev_mode)
72-
7369

7470
def _default_chunk_size():
7571
"""Get the default TextIOWrapper chunk size"""
@@ -1218,10 +1214,7 @@ def test_error_through_destructor(self):
12181214
with self.assertRaises(AttributeError):
12191215
self.tp(rawio).xyzzy
12201216

1221-
if not IOBASE_EMITS_UNRAISABLE:
1222-
self.assertIsNone(cm.unraisable)
1223-
elif cm.unraisable is not None:
1224-
self.assertEqual(cm.unraisable.exc_type, OSError)
1217+
self.assertEqual(cm.unraisable.exc_type, OSError)
12251218

12261219
def test_repr(self):
12271220
raw = self.MockRawIO()
@@ -3022,10 +3015,7 @@ def test_error_through_destructor(self):
30223015
with self.assertRaises(AttributeError):
30233016
self.TextIOWrapper(rawio, encoding="utf-8").xyzzy
30243017

3025-
if not IOBASE_EMITS_UNRAISABLE:
3026-
self.assertIsNone(cm.unraisable)
3027-
elif cm.unraisable is not None:
3028-
self.assertEqual(cm.unraisable.exc_type, OSError)
3018+
self.assertEqual(cm.unraisable.exc_type, OSError)
30293019

30303020
# Systematic tests of the text I/O API
30313021

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

Modules/_io/iobase.c

-12
Original file line numberDiff line numberDiff line change
@@ -319,20 +319,8 @@ iobase_finalize(PyObject *self)
319319
if (PyObject_SetAttr(self, &_Py_ID(_finalizing), Py_True))
320320
PyErr_Clear();
321321
res = PyObject_CallMethodNoArgs((PyObject *)self, &_Py_ID(close));
322-
/* Silencing I/O errors is bad, but printing spurious tracebacks is
323-
equally as bad, and potentially more frequent (because of
324-
shutdown issues). */
325322
if (res == NULL) {
326-
#ifndef Py_DEBUG
327-
if (_Py_GetConfig()->dev_mode) {
328-
PyErr_WriteUnraisable(self);
329-
}
330-
else {
331-
PyErr_Clear();
332-
}
333-
#else
334323
PyErr_WriteUnraisable(self);
335-
#endif
336324
}
337325
else {
338326
Py_DECREF(res);

0 commit comments

Comments
 (0)