Skip to content

Commit 4423504

Browse files
authored
bpo-18748: io.IOBase destructor now logs close() errors in dev mode (GH-12786)
In development mode (-X dev) and in debug build, the io.IOBase destructor now logs close() exceptions. These exceptions are silent by default in release mode.
1 parent 9d949f7 commit 4423504

File tree

4 files changed

+46
-10
lines changed

4 files changed

+46
-10
lines changed

Doc/using/cmdline.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ Miscellaneous options
437437
* Enable :ref:`asyncio debug mode <asyncio-debug-mode>`.
438438
* Set the :attr:`~sys.flags.dev_mode` attribute of :attr:`sys.flags` to
439439
``True``
440+
* :class:`io.IOBase` destructor logs ``close()`` exceptions.
440441

441442
* ``-X utf8`` enables UTF-8 mode for operating system interfaces, overriding
442443
the default locale-aware mode. ``-X utf8=0`` explicitly disables UTF-8
@@ -465,7 +466,8 @@ Miscellaneous options
465466
The ``-X importtime``, ``-X dev`` and ``-X utf8`` options.
466467

467468
.. versionadded:: 3.8
468-
The ``-X pycache_prefix`` option.
469+
The ``-X pycache_prefix`` option. The ``-X dev`` option now logs
470+
``close()`` exceptions in :class:`io.IOBase` destructor.
469471

470472

471473
Options you shouldn't use

Lib/test/test_io.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ class EmptyStruct(ctypes.Structure):
6767
'--with-memory-sanitizer' in _config_args
6868
)
6969

70+
# Does io.IOBase logs unhandled exceptions on calling close()?
71+
# They are silenced by default in release build.
72+
DESTRUCTOR_LOG_ERRORS = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
73+
74+
7075
def _default_chunk_size():
7176
"""Get the default TextIOWrapper chunk size"""
7277
with open(__file__, "r", encoding="latin-1") as f:
@@ -1097,9 +1102,16 @@ def f():
10971102
s = s.getvalue().strip()
10981103
if s:
10991104
# The destructor *may* have printed an unraisable error, check it
1100-
self.assertEqual(len(s.splitlines()), 1)
1101-
self.assertTrue(s.startswith("Exception OSError: "), s)
1102-
self.assertTrue(s.endswith(" ignored"), s)
1105+
lines = s.splitlines()
1106+
if DESTRUCTOR_LOG_ERRORS:
1107+
self.assertEqual(len(lines), 5)
1108+
self.assertTrue(lines[0].startswith("Exception ignored in: "), lines)
1109+
self.assertEqual(lines[1], "Traceback (most recent call last):", lines)
1110+
self.assertEqual(lines[4], 'OSError:', lines)
1111+
else:
1112+
self.assertEqual(len(lines), 1)
1113+
self.assertTrue(lines[-1].startswith("Exception OSError: "), lines)
1114+
self.assertTrue(lines[-1].endswith(" ignored"), lines)
11031115

11041116
def test_repr(self):
11051117
raw = self.MockRawIO()
@@ -2833,9 +2845,16 @@ def f():
28332845
s = s.getvalue().strip()
28342846
if s:
28352847
# The destructor *may* have printed an unraisable error, check it
2836-
self.assertEqual(len(s.splitlines()), 1)
2837-
self.assertTrue(s.startswith("Exception OSError: "), s)
2838-
self.assertTrue(s.endswith(" ignored"), s)
2848+
lines = s.splitlines()
2849+
if DESTRUCTOR_LOG_ERRORS:
2850+
self.assertEqual(len(lines), 5)
2851+
self.assertTrue(lines[0].startswith("Exception ignored in: "), lines)
2852+
self.assertEqual(lines[1], "Traceback (most recent call last):", lines)
2853+
self.assertEqual(lines[4], 'OSError:', lines)
2854+
else:
2855+
self.assertEqual(len(lines), 1)
2856+
self.assertTrue(lines[-1].startswith("Exception OSError: "), lines)
2857+
self.assertTrue(lines[-1].endswith(" ignored"), lines)
28392858

28402859
# Systematic tests of the text I/O API
28412860

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In development mode (:option:`-X` ``dev``) and in debug build, the
2+
:class:`io.IOBase` destructor now logs ``close()`` exceptions. These exceptions
3+
are silent by default in release mode.

Modules/_io/iobase.c

+15-3
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,22 @@ iobase_finalize(PyObject *self)
286286
/* Silencing I/O errors is bad, but printing spurious tracebacks is
287287
equally as bad, and potentially more frequent (because of
288288
shutdown issues). */
289-
if (res == NULL)
290-
PyErr_Clear();
291-
else
289+
if (res == NULL) {
290+
#ifndef Py_DEBUG
291+
const _PyCoreConfig *config = &_PyInterpreterState_GET_UNSAFE()->core_config;
292+
if (config->dev_mode) {
293+
PyErr_WriteUnraisable(self);
294+
}
295+
else {
296+
PyErr_Clear();
297+
}
298+
#else
299+
PyErr_WriteUnraisable(self);
300+
#endif
301+
}
302+
else {
292303
Py_DECREF(res);
304+
}
293305
}
294306

295307
/* Restore the saved exception. */

0 commit comments

Comments
 (0)