Skip to content

Commit bc2aa81

Browse files
authored
bpo-18748: _pyio.IOBase emits unraisable exception (GH-13512)
In development (-X dev) mode and in a debug build, IOBase finalizer of the _pyio module now logs the exception if the close() method fails. The exception is ignored silently by default in release build. test_io: test_error_through_destructor() now uses support.catch_unraisable_exception() rather than capturing stderr.
1 parent 0a8e572 commit bc2aa81

File tree

3 files changed

+51
-45
lines changed

3 files changed

+51
-45
lines changed

Doc/whatsnew/3.8.rst

+9
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,15 @@ for :func:`property`, :func:`classmethod`, and :func:`staticmethod`::
318318
self.bit_rate = round(bit_rate / 1000.0, 1)
319319
self.duration = ceil(duration)
320320

321+
io
322+
--
323+
324+
In development mode (:option:`-X` ``env``) and in debug build, the
325+
:class:`io.IOBase` finalizer now logs the exception if the ``close()`` method
326+
fails. The exception is ignored silently by default in release build.
327+
(Contributed by Victor Stinner in :issue:`18748`.)
328+
329+
321330
gc
322331
--
323332

Lib/_pyio.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
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)
39+
3640

3741
def open(file, mode="r", buffering=-1, encoding=None, errors=None,
3842
newline=None, closefd=True, opener=None):
@@ -378,15 +382,18 @@ def close(self):
378382

379383
def __del__(self):
380384
"""Destructor. Calls close()."""
381-
# The try/except block is in case this is called at program
382-
# exit time, when it's possible that globals have already been
383-
# deleted, and then the close() call might fail. Since
384-
# there's nothing we can do about such failures and they annoy
385-
# the end users, we suppress the traceback.
386-
try:
385+
if _IOBASE_EMITS_UNRAISABLE:
387386
self.close()
388-
except:
389-
pass
387+
else:
388+
# The try/except block is in case this is called at program
389+
# exit time, when it's possible that globals have already been
390+
# deleted, and then the close() call might fail. Since
391+
# there's nothing we can do about such failures and they annoy
392+
# the end users, we suppress the traceback.
393+
try:
394+
self.close()
395+
except:
396+
pass
390397

391398
### Inquiries ###
392399

Lib/test/test_io.py

+27-37
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ 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)
70+
# Does io.IOBase finalizer log the exception if the close() method fails?
71+
# The exception is ignored silently by default in release build.
72+
IOBASE_EMITS_UNRAISABLE = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
7373

7474

7575
def _default_chunk_size():
@@ -1098,23 +1098,18 @@ def test_error_through_destructor(self):
10981098
# Test that the exception state is not modified by a destructor,
10991099
# even if close() fails.
11001100
rawio = self.CloseFailureIO()
1101-
def f():
1102-
self.tp(rawio).xyzzy
1103-
with support.captured_output("stderr") as s:
1104-
self.assertRaises(AttributeError, f)
1105-
s = s.getvalue().strip()
1106-
if s:
1107-
# The destructor *may* have printed an unraisable error, check it
1108-
lines = s.splitlines()
1109-
if DESTRUCTOR_LOG_ERRORS:
1110-
self.assertEqual(len(lines), 5)
1111-
self.assertTrue(lines[0].startswith("Exception ignored in: "), lines)
1112-
self.assertEqual(lines[1], "Traceback (most recent call last):", lines)
1113-
self.assertEqual(lines[4], 'OSError:', lines)
1114-
else:
1115-
self.assertEqual(len(lines), 1)
1116-
self.assertTrue(lines[-1].startswith("Exception OSError: "), lines)
1117-
self.assertTrue(lines[-1].endswith(" ignored"), lines)
1101+
try:
1102+
with support.catch_unraisable_exception() as cm:
1103+
with self.assertRaises(AttributeError):
1104+
self.tp(rawio).xyzzy
1105+
1106+
if not IOBASE_EMITS_UNRAISABLE:
1107+
self.assertIsNone(cm.unraisable)
1108+
elif cm.unraisable is not None:
1109+
self.assertEqual(cm.unraisable.exc_type, OSError)
1110+
finally:
1111+
# Explicitly break reference cycle
1112+
cm = None
11181113

11191114
def test_repr(self):
11201115
raw = self.MockRawIO()
@@ -2859,23 +2854,18 @@ def test_error_through_destructor(self):
28592854
# Test that the exception state is not modified by a destructor,
28602855
# even if close() fails.
28612856
rawio = self.CloseFailureIO()
2862-
def f():
2863-
self.TextIOWrapper(rawio).xyzzy
2864-
with support.captured_output("stderr") as s:
2865-
self.assertRaises(AttributeError, f)
2866-
s = s.getvalue().strip()
2867-
if s:
2868-
# The destructor *may* have printed an unraisable error, check it
2869-
lines = s.splitlines()
2870-
if DESTRUCTOR_LOG_ERRORS:
2871-
self.assertEqual(len(lines), 5)
2872-
self.assertTrue(lines[0].startswith("Exception ignored in: "), lines)
2873-
self.assertEqual(lines[1], "Traceback (most recent call last):", lines)
2874-
self.assertEqual(lines[4], 'OSError:', lines)
2875-
else:
2876-
self.assertEqual(len(lines), 1)
2877-
self.assertTrue(lines[-1].startswith("Exception OSError: "), lines)
2878-
self.assertTrue(lines[-1].endswith(" ignored"), lines)
2857+
try:
2858+
with support.catch_unraisable_exception() as cm:
2859+
with self.assertRaises(AttributeError):
2860+
self.TextIOWrapper(rawio).xyzzy
2861+
2862+
if not IOBASE_EMITS_UNRAISABLE:
2863+
self.assertIsNone(cm.unraisable)
2864+
elif cm.unraisable is not None:
2865+
self.assertEqual(cm.unraisable.exc_type, OSError)
2866+
finally:
2867+
# Explicitly break reference cycle
2868+
cm = None
28792869

28802870
# Systematic tests of the text I/O API
28812871

0 commit comments

Comments
 (0)