mirror of
https://github.com/python/cpython.git
synced 2025-09-27 10:50:04 +00:00
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.
This commit is contained in:
parent
0a8e57248b
commit
bc2aa81662
3 changed files with 51 additions and 45 deletions
|
@ -318,6 +318,15 @@ for :func:`property`, :func:`classmethod`, and :func:`staticmethod`::
|
||||||
self.bit_rate = round(bit_rate / 1000.0, 1)
|
self.bit_rate = round(bit_rate / 1000.0, 1)
|
||||||
self.duration = ceil(duration)
|
self.duration = ceil(duration)
|
||||||
|
|
||||||
|
io
|
||||||
|
--
|
||||||
|
|
||||||
|
In development mode (:option:`-X` ``env``) and in debug build, the
|
||||||
|
:class:`io.IOBase` finalizer now logs the exception if the ``close()`` method
|
||||||
|
fails. The exception is ignored silently by default in release build.
|
||||||
|
(Contributed by Victor Stinner in :issue:`18748`.)
|
||||||
|
|
||||||
|
|
||||||
gc
|
gc
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,10 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
def open(file, mode="r", buffering=-1, encoding=None, errors=None,
|
def open(file, mode="r", buffering=-1, encoding=None, errors=None,
|
||||||
newline=None, closefd=True, opener=None):
|
newline=None, closefd=True, opener=None):
|
||||||
|
@ -378,6 +382,9 @@ class IOBase(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Destructor. Calls close()."""
|
"""Destructor. Calls close()."""
|
||||||
|
if _IOBASE_EMITS_UNRAISABLE:
|
||||||
|
self.close()
|
||||||
|
else:
|
||||||
# The try/except block is in case this is called at program
|
# The try/except block is in case this is called at program
|
||||||
# exit time, when it's possible that globals have already been
|
# exit time, when it's possible that globals have already been
|
||||||
# deleted, and then the close() call might fail. Since
|
# deleted, and then the close() call might fail. Since
|
||||||
|
|
|
@ -67,9 +67,9 @@ MEMORY_SANITIZER = (
|
||||||
'--with-memory-sanitizer' in _config_args
|
'--with-memory-sanitizer' in _config_args
|
||||||
)
|
)
|
||||||
|
|
||||||
# Does io.IOBase logs unhandled exceptions on calling close()?
|
# Does io.IOBase finalizer log the exception if the close() method fails?
|
||||||
# They are silenced by default in release build.
|
# The exception is ignored silently by default in release build.
|
||||||
DESTRUCTOR_LOG_ERRORS = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
|
IOBASE_EMITS_UNRAISABLE = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
|
||||||
|
|
||||||
|
|
||||||
def _default_chunk_size():
|
def _default_chunk_size():
|
||||||
|
@ -1098,23 +1098,18 @@ class CommonBufferedTests:
|
||||||
# Test that the exception state is not modified by a destructor,
|
# Test that the exception state is not modified by a destructor,
|
||||||
# even if close() fails.
|
# even if close() fails.
|
||||||
rawio = self.CloseFailureIO()
|
rawio = self.CloseFailureIO()
|
||||||
def f():
|
try:
|
||||||
|
with support.catch_unraisable_exception() as cm:
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
self.tp(rawio).xyzzy
|
self.tp(rawio).xyzzy
|
||||||
with support.captured_output("stderr") as s:
|
|
||||||
self.assertRaises(AttributeError, f)
|
if not IOBASE_EMITS_UNRAISABLE:
|
||||||
s = s.getvalue().strip()
|
self.assertIsNone(cm.unraisable)
|
||||||
if s:
|
elif cm.unraisable is not None:
|
||||||
# The destructor *may* have printed an unraisable error, check it
|
self.assertEqual(cm.unraisable.exc_type, OSError)
|
||||||
lines = s.splitlines()
|
finally:
|
||||||
if DESTRUCTOR_LOG_ERRORS:
|
# Explicitly break reference cycle
|
||||||
self.assertEqual(len(lines), 5)
|
cm = None
|
||||||
self.assertTrue(lines[0].startswith("Exception ignored in: "), lines)
|
|
||||||
self.assertEqual(lines[1], "Traceback (most recent call last):", lines)
|
|
||||||
self.assertEqual(lines[4], 'OSError:', lines)
|
|
||||||
else:
|
|
||||||
self.assertEqual(len(lines), 1)
|
|
||||||
self.assertTrue(lines[-1].startswith("Exception OSError: "), lines)
|
|
||||||
self.assertTrue(lines[-1].endswith(" ignored"), lines)
|
|
||||||
|
|
||||||
def test_repr(self):
|
def test_repr(self):
|
||||||
raw = self.MockRawIO()
|
raw = self.MockRawIO()
|
||||||
|
@ -2859,23 +2854,18 @@ class TextIOWrapperTest(unittest.TestCase):
|
||||||
# Test that the exception state is not modified by a destructor,
|
# Test that the exception state is not modified by a destructor,
|
||||||
# even if close() fails.
|
# even if close() fails.
|
||||||
rawio = self.CloseFailureIO()
|
rawio = self.CloseFailureIO()
|
||||||
def f():
|
try:
|
||||||
|
with support.catch_unraisable_exception() as cm:
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
self.TextIOWrapper(rawio).xyzzy
|
self.TextIOWrapper(rawio).xyzzy
|
||||||
with support.captured_output("stderr") as s:
|
|
||||||
self.assertRaises(AttributeError, f)
|
if not IOBASE_EMITS_UNRAISABLE:
|
||||||
s = s.getvalue().strip()
|
self.assertIsNone(cm.unraisable)
|
||||||
if s:
|
elif cm.unraisable is not None:
|
||||||
# The destructor *may* have printed an unraisable error, check it
|
self.assertEqual(cm.unraisable.exc_type, OSError)
|
||||||
lines = s.splitlines()
|
finally:
|
||||||
if DESTRUCTOR_LOG_ERRORS:
|
# Explicitly break reference cycle
|
||||||
self.assertEqual(len(lines), 5)
|
cm = None
|
||||||
self.assertTrue(lines[0].startswith("Exception ignored in: "), lines)
|
|
||||||
self.assertEqual(lines[1], "Traceback (most recent call last):", lines)
|
|
||||||
self.assertEqual(lines[4], 'OSError:', lines)
|
|
||||||
else:
|
|
||||||
self.assertEqual(len(lines), 1)
|
|
||||||
self.assertTrue(lines[-1].startswith("Exception OSError: "), lines)
|
|
||||||
self.assertTrue(lines[-1].endswith(" ignored"), lines)
|
|
||||||
|
|
||||||
# Systematic tests of the text I/O API
|
# Systematic tests of the text I/O API
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue